separato in thread diversi la ricezione, invio e aggiornamenti grafica

This commit is contained in:
VALLONGOL 2025-10-31 15:37:03 +01:00
parent 8c439b60c3
commit 73a7817f5c
4 changed files with 157 additions and 186 deletions

View File

@ -52,7 +52,7 @@ class SFPCommunicator(CommunicatorInterface):
# Unified payload router # Unified payload router
self.payload_router = DebugPayloadRouter( self.payload_router = DebugPayloadRouter(
simulation_hub=simulation_hub, update_queue=update_queue simulation_hub=simulation_hub
) )
def _save_json_payload_to_temp(self, content: str, prefix: str): def _save_json_payload_to_temp(self, content: str, prefix: str):
@ -188,15 +188,6 @@ class SFPCommunicator(CommunicatorInterface):
self.logger.exception("Error while shutting down transport") self.logger.exception("Error while shutting down transport")
self.transport = None self.transport = None
# Clear the update queue to prevent processing of stale data.
if self.update_queue:
self.logger.info("Clearing GUI update queue of stale messages.")
while not self.update_queue.empty():
try:
self.update_queue.get_nowait()
except Exception: # Can be Empty, but catch all to be safe
break
self.config = None self.config = None
self._destination = None self._destination = None
self._notify_connection_state_changed() self._notify_connection_state_changed()

View File

@ -26,14 +26,12 @@ class SimulationEngine(threading.Thread):
def __init__( def __init__(
self, self,
communicator: Optional[CommunicatorInterface], communicator: Optional[CommunicatorInterface],
update_queue: Optional[Queue],
simulation_hub: Optional[SimulationStateHub] = None, simulation_hub: Optional[SimulationStateHub] = None,
): ):
super().__init__(daemon=True, name="SimulationEngineThread") super().__init__(daemon=True, name="SimulationEngineThread")
self.logger = get_logger(__name__) self.logger = get_logger(__name__)
self.communicator = communicator self.communicator = communicator
self.update_queue = update_queue
self.simulation_hub = simulation_hub # Hub for data analysis self.simulation_hub = simulation_hub # Hub for data analysis
self.time_multiplier = 1.0 self.time_multiplier = 1.0
self.update_interval_s = 1.0 self.update_interval_s = 1.0
@ -111,37 +109,30 @@ class SimulationEngine(threading.Thread):
simulated_delta_time = delta_time * self.time_multiplier simulated_delta_time = delta_time * self.time_multiplier
self.scenario.update_state(simulated_delta_time) self.scenario.update_state(simulated_delta_time)
updated_targets = self.scenario.get_all_targets()
# --- High-Frequency State Logging ---
tick_timestamp = time.monotonic()
active_targets = [t for t in self.scenario.get_all_targets() if t.active]
if self.simulation_hub:
for target in active_targets:
state_tuple = (
getattr(target, "_pos_x_ft", 0.0),
getattr(target, "_pos_y_ft", 0.0),
getattr(target, "_pos_z_ft", 0.0),
)
self.simulation_hub.add_simulated_state(
target.target_id, tick_timestamp, state_tuple
)
if self.scenario.is_finished(): if self.scenario.is_finished():
self.logger.info("Scenario finished. Stopping engine.") self.logger.info("Scenario finished. Stopping engine.")
if self.update_queue:
self.update_queue.put_nowait(updated_targets)
self.update_queue.put_nowait("SIMULATION_FINISHED")
break break
# --- Communication, Data Hub, and GUI Update Step --- # --- Throttled Communication Step ---
if (current_time - self._last_update_time) >= self.update_interval_s: if (current_time - self._last_update_time) >= self.update_interval_s:
self._last_update_time = current_time self._last_update_time = current_time
# Prepare batch timestamp and active targets once per update
timestamp_for_batch = time.monotonic()
active_targets = [t for t in updated_targets if t.active]
# Always log simulated state for all active targets to the hub for analysis
# This should happen regardless of whether a communicator is present
# so that offline analysis (GUI only) still has simulated data.
if self.simulation_hub:
for target in active_targets:
state_tuple = (
getattr(target, "_pos_x_ft", 0.0),
getattr(target, "_pos_y_ft", 0.0),
getattr(target, "_pos_z_ft", 0.0),
)
self.simulation_hub.add_simulated_state(
target.target_id, timestamp_for_batch, state_tuple
)
if self.communicator and self.communicator.is_open: if self.communicator and self.communicator.is_open:
commands_to_send = [] commands_to_send = []
@ -162,7 +153,7 @@ class SimulationEngine(threading.Thread):
getattr(target, "_pos_z_ft", 0.0), getattr(target, "_pos_z_ft", 0.0),
) )
append_sent_position( append_sent_position(
timestamp_for_batch, tick_timestamp,
target.target_id, target.target_id,
state_tuple[0], state_tuple[0],
state_tuple[1], state_tuple[1],
@ -182,7 +173,7 @@ class SimulationEngine(threading.Thread):
getattr(target, "_pos_z_ft", 0.0), getattr(target, "_pos_z_ft", 0.0),
) )
append_sent_position( append_sent_position(
timestamp_for_batch, tick_timestamp,
target.target_id, target.target_id,
state_tuple[0], state_tuple[0],
state_tuple[1], state_tuple[1],
@ -194,15 +185,6 @@ class SimulationEngine(threading.Thread):
if commands_to_send: if commands_to_send:
self.communicator.send_commands(commands_to_send) self.communicator.send_commands(commands_to_send)
# Update the GUI queue
if self.update_queue:
try:
self.update_queue.put_nowait(updated_targets)
except Queue.Full:
self.logger.warning(
"GUI update queue is full. A frame was skipped."
)
time.sleep(TICK_INTERVAL_S) time.sleep(TICK_INTERVAL_S)
self._is_running_event.clear() self._is_running_event.clear()

View File

@ -35,6 +35,7 @@ from target_simulator.core import command_builder
GUI_QUEUE_POLL_INTERVAL_MS = 100 GUI_QUEUE_POLL_INTERVAL_MS = 100
GUI_REFRESH_RATE_MS = 40
class MainView(tk.Tk): class MainView(tk.Tk):
@ -68,7 +69,6 @@ class MainView(tk.Tk):
# --- Simulation Engine --- # --- Simulation Engine ---
self.simulation_engine: Optional[SimulationEngine] = None self.simulation_engine: Optional[SimulationEngine] = None
self.gui_update_queue = Queue()
self.is_simulation_running = tk.BooleanVar(value=False) self.is_simulation_running = tk.BooleanVar(value=False)
self.time_multiplier = 1.0 self.time_multiplier = 1.0
self.update_time = tk.DoubleVar(value=1.0) self.update_time = tk.DoubleVar(value=1.0)
@ -99,13 +99,10 @@ class MainView(tk.Tk):
self._update_window_title() self._update_window_title()
self.protocol("WM_DELETE_WINDOW", self._on_closing) self.protocol("WM_DELETE_WINDOW", self._on_closing)
self.logger.info("MainView initialized successfully.") self.logger.info("MainView initialized successfully.")
# Start the new rendering loop
self.after(GUI_REFRESH_RATE_MS, self._gui_refresh_loop)
# Always poll the GUI update queue so the main PPI receives real-time
# hub updates even when the live simulation engine is not running.
try:
self.after(GUI_QUEUE_POLL_INTERVAL_MS, self._process_gui_queue)
except Exception:
self.logger.exception("Failed to schedule GUI queue polling")
# Schedule periodic rate status updates (shows events/sec for real inputs and PPI updates) # Schedule periodic rate status updates (shows events/sec for real inputs and PPI updates)
try: try:
# Start after one second to allow initial state to settle # Start after one second to allow initial state to settle
@ -594,9 +591,9 @@ class MainView(tk.Tk):
communicator = TFTPCommunicator() communicator = TFTPCommunicator()
config_data = config.get("tftp", {}) config_data = config.get("tftp", {})
elif comm_type == "sfp": elif comm_type == "sfp":
# --- MODIFICATION: Pass the hub and GUI update queue to the communicator --- # --- MODIFICATION: Do not pass update_queue ---
communicator = SFPCommunicator( communicator = SFPCommunicator(
simulation_hub=self.simulation_hub, update_queue=self.gui_update_queue simulation_hub=self.simulation_hub
) )
communicator.add_connection_state_callback(self._on_connection_state_change) communicator.add_connection_state_callback(self._on_connection_state_change)
config_data = config.get("sfp", {}) config_data = config.get("sfp", {})
@ -832,8 +829,6 @@ class MainView(tk.Tk):
self.logger.error("Aborting simulation start due to radar reset failure.") self.logger.error("Aborting simulation start due to radar reset failure.")
return return
# MODIFICATION: Add a short delay to allow the server to process the reset
# before it receives the new scenario initialization commands.
time.sleep(1) # 1 second delay time.sleep(1) # 1 second delay
self.logger.info( self.logger.info(
@ -851,14 +846,11 @@ class MainView(tk.Tk):
self.logger.info("Initial scenario state sent successfully.") self.logger.info("Initial scenario state sent successfully.")
self.logger.info("Starting live simulation...") self.logger.info("Starting live simulation...")
self.is_simulation_running.set(True)
self._update_button_states()
self.scenario.reset_simulation() self.scenario.reset_simulation()
self.simulation_engine = SimulationEngine( self.simulation_engine = SimulationEngine(
communicator=self.target_communicator, communicator=self.target_communicator,
update_queue=self.gui_update_queue,
simulation_hub=self.simulation_hub, simulation_hub=self.simulation_hub,
) )
@ -886,7 +878,9 @@ class MainView(tk.Tk):
self.simulation_engine.start() self.simulation_engine.start()
self.after(GUI_QUEUE_POLL_INTERVAL_MS, self._process_gui_queue) # Set running state and update buttons AFTER starting the thread
self.is_simulation_running.set(True)
self._update_button_states()
def _on_stop_simulation(self): def _on_stop_simulation(self):
if not self.is_simulation_running.get() or not self.simulation_engine: if not self.is_simulation_running.get() or not self.simulation_engine:
@ -918,29 +912,19 @@ class MainView(tk.Tk):
return return
def _on_simulation_finished(self): def _on_simulation_finished(self):
"""Handle the natural end-of-simulation event coming from the engine. """Handle the natural end-of-simulation event."""
This should update the UI and inform the user, but must NOT disconnect the
communicator the connection stays active until the user explicitly disconnects.
"""
self.logger.info("Handling simulation finished (engine signalled completion).") self.logger.info("Handling simulation finished (engine signalled completion).")
# If engine still exists, stop it cleanly if self.simulation_engine and self.simulation_engine.is_running():
try: try:
if self.simulation_engine and getattr(self.simulation_engine, "is_running", False): self.simulation_engine.stop()
try: except Exception:
self.simulation_engine.stop() self.logger.exception("Error while stopping finished simulation engine")
except Exception: self.simulation_engine = None
self.logger.exception("Error while stopping finished simulation engine")
self.simulation_engine = None
except Exception:
self.logger.exception("Error while handling simulation finished cleanup")
# Mark as not running and update UI (but keep connection state as-is)
self.is_simulation_running.set(False) self.is_simulation_running.set(False)
self._update_button_states() self._update_button_states()
# Notify the user in English that the simulation finished
try: try:
messagebox.showinfo("Simulation Finished", "The live simulation has completed.") messagebox.showinfo("Simulation Finished", "The live simulation has completed.")
except Exception: except Exception:
@ -955,121 +939,88 @@ class MainView(tk.Tk):
self._update_all_views() self._update_all_views()
def _process_gui_queue(self): def _process_gui_queue(self):
"""
Processes a batch of updates from the GUI queue to keep the UI responsive
without getting stuck in an infinite loop.
"""
MAX_UPDATES_PER_CYCLE = 100 # Process up to 100 messages per call
try: try:
# We process one update at a time to keep the GUI responsive for _ in range(MAX_UPDATES_PER_CYCLE):
update = self.gui_update_queue.get_nowait()
# try:
# self.logger.debug(f"MainView: dequeued GUI update (type={type(update)}) from queue id={id(self.gui_update_queue)}")
# except Exception:
# pass
if update == "SIMULATION_FINISHED":
self.logger.info("Simulation finished signal received.")
# Ensure engine is stopped and UI reset (do not disconnect communicator)
self._on_simulation_finished()
# Reset progress UI to final state
try: try:
self.sim_elapsed_time = self.total_sim_time update = self.gui_update_queue.get_nowait()
self.sim_slider_var.set(1.0 if self.total_sim_time > 0 else 0.0)
except Exception:
pass
self._update_simulation_progress_display()
elif isinstance(update, list): if update == "SIMULATION_FINISHED":
# The engine normally enqueues a List[Target] (simulated targets). self.logger.info("Simulation finished signal received.")
# However, the simulation payload handler uses an empty list [] self._on_simulation_finished()
# as a lightweight notification that real states were added to try:
# the hub. Distinguish the two cases: self.sim_elapsed_time = self.total_sim_time
if len(update) == 0: self.sim_slider_var.set(1.0 if self.total_sim_time > 0 else 0.0)
# Hub refresh notification (real data arrived). except Exception:
# Only update the 'real' targets on the PPI display. pass
# self.logger.debug("MainView: received hub refresh. Updating real targets.") self._update_simulation_progress_display()
display_data = self._build_display_data_from_hub()
self.ppi_widget.update_real_targets(display_data.get("real", [])) elif isinstance(update, list):
# Also propagate platform/antenna azimuth (if available) so the PPI if len(update) == 0:
# can render the antenna orientation. The hub stores a (az_deg, ts) # Hub refresh notification (real data arrived).
# tuple via set_platform_azimuth(). display_data = self._build_display_data_from_hub()
try: self.ppi_widget.update_real_targets(display_data.get("real", []))
if (
hasattr(self, "simulation_hub")
and self.simulation_hub is not None
and hasattr(self.ppi_widget, "update_antenna_azimuth")
):
try: try:
# Prefer the new API name, fall back to the legacy if (
if hasattr(self.simulation_hub, "get_antenna_azimuth"): hasattr(self, "simulation_hub")
az_deg, az_ts = ( and self.simulation_hub is not None
self.simulation_hub.get_antenna_azimuth() and hasattr(self.ppi_widget, "update_antenna_azimuth")
) ):
else: if hasattr(self.simulation_hub, "get_antenna_azimuth"):
az_deg, az_ts = ( az_deg, az_ts = self.simulation_hub.get_antenna_azimuth()
self.simulation_hub.get_platform_azimuth() else:
) az_deg, az_ts = self.simulation_hub.get_platform_azimuth()
if az_deg is not None: if az_deg is not None:
# pass the hub-provided timestamp if available self.ppi_widget.update_antenna_azimuth(
self.ppi_widget.update_antenna_azimuth( az_deg, timestamp=az_ts
az_deg, timestamp=az_ts )
)
except Exception: except Exception:
# don't allow GUI update failures to interrupt queue processing
self.logger.debug( self.logger.debug(
"Failed to propagate antenna azimuth to PPI", "Failed to propagate antenna azimuth to PPI",
exc_info=True, exc_info=True,
) )
except Exception:
pass
else:
# This is an update with simulated targets from the engine.
# Only update the 'simulated' targets on the PPI and the target list.
simulated_targets: List[Target] = update
# self.logger.debug(f"MainView: received simulation update for {len(simulated_targets)} targets.")
# Update the target list view with detailed simulated data
self.target_list.update_target_list(simulated_targets)
# Update only the simulated targets on the PPI
self.ppi_widget.update_simulated_targets(simulated_targets)
# Update simulation progress bar
try:
# Use the engine's scenario simulated time as elapsed if available
if self.simulation_engine and self.simulation_engine.scenario:
# Derive elapsed as the max of target sim times
times = [
getattr(t, "_sim_time_s", 0.0)
for t in self.simulation_engine.scenario.get_all_targets()
]
self.sim_elapsed_time = max(times) if times else 0.0
else: else:
self.sim_elapsed_time += 0.0 # This is an update with simulated targets from the engine.
simulated_targets: List[Target] = update
self.target_list.update_target_list(simulated_targets)
self.ppi_widget.update_simulated_targets(simulated_targets)
# Update slider only if user is not interacting with it # Update simulation progress bar
if self.total_sim_time > 0 and not getattr( try:
self, "_slider_is_dragging", False if self.simulation_engine and self.simulation_engine.scenario:
): times = [
progress_frac = min( getattr(t, "_sim_time_s", 0.0)
1.0, for t in self.simulation_engine.scenario.get_all_targets()
max(0.0, self.sim_elapsed_time / self.total_sim_time), ]
) self.sim_elapsed_time = max(times) if times else 0.0
self.sim_slider_var.set(progress_frac)
self._update_simulation_progress_display() if self.total_sim_time > 0 and not getattr(
except Exception: self, "_slider_is_dragging", False
# Do not allow progress UI failures to interrupt GUI updates ):
self.logger.debug("Progress UI update failed", exc_info=True) progress_frac = min(
1.0,
max(0.0, self.sim_elapsed_time / self.total_sim_time),
)
self.sim_slider_var.set(progress_frac)
except Empty: self._update_simulation_progress_display()
# If the queue is empty, we don't need to do anything except Exception:
pass self.logger.debug("Progress UI update failed", exc_info=True)
except Empty:
# Queue is empty, we can stop processing for this cycle.
break
finally: finally:
# Always continue polling the GUI update queue so we can show # Always reschedule the next poll.
# real-time server updates on the PPI even when the live
# simulation engine is not running.
try: try:
self.after(GUI_QUEUE_POLL_INTERVAL_MS, self._process_gui_queue) self.after(GUI_QUEUE_POLL_INTERVAL_MS, self._process_gui_queue)
except Exception: except Exception:
# This can happen on shutdown, just ignore.
pass pass
def _update_button_states(self): def _update_button_states(self):
@ -1643,3 +1594,52 @@ class MainView(tk.Tk):
self.analysis_window = AnalysisWindow( self.analysis_window = AnalysisWindow(
self, analyzer=self.performance_analyzer, hub=self.simulation_hub self, analyzer=self.performance_analyzer, hub=self.simulation_hub
) )
def _gui_refresh_loop(self):
"""
Main GUI refresh loop. Runs at a fixed rate, pulls the latest data
from the hub, and updates the PPI display.
"""
# Check if the simulation has finished
sim_was_running = self.is_simulation_running.get()
sim_is_running_now = (
self.simulation_engine is not None and self.simulation_engine.is_running()
)
if sim_was_running and not sim_is_running_now:
self._on_simulation_finished()
# Update PPI with the latest data from the hub
display_data = self._build_display_data_from_hub()
self.ppi_widget.update_simulated_targets(display_data.get("simulated", []))
self.ppi_widget.update_real_targets(display_data.get("real", []))
# Update antenna azimuth
try:
if self.simulation_hub and hasattr(self.simulation_hub, "get_antenna_azimuth"):
az_deg, az_ts = self.simulation_hub.get_antenna_azimuth()
if az_deg is not None:
self.ppi_widget.update_antenna_azimuth(az_deg, timestamp=az_ts)
except Exception:
pass
# Update progress bar if the simulation is running
if sim_is_running_now:
try:
if self.simulation_engine and self.simulation_engine.scenario:
times = [
getattr(t, "_sim_time_s", 0.0)
for t in self.simulation_engine.scenario.get_all_targets()
]
self.sim_elapsed_time = max(times) if times else 0.0
if self.total_sim_time > 0 and not self._slider_is_dragging:
progress = min(1.0, self.sim_elapsed_time / self.total_sim_time)
self.sim_slider_var.set(progress)
self._update_simulation_progress_display()
except Exception:
self.logger.debug("Progress UI update failed", exc_info=True)
# Reschedule the next refresh cycle
self.after(GUI_REFRESH_RATE_MS, self._gui_refresh_loop)

View File

@ -39,7 +39,6 @@ class DebugPayloadRouter:
def __init__( def __init__(
self, self,
simulation_hub: Optional[SimulationStateHub] = None, simulation_hub: Optional[SimulationStateHub] = None,
update_queue: Optional[Queue] = None,
): ):
self._log_prefix = "[DebugPayloadRouter]" self._log_prefix = "[DebugPayloadRouter]"
self._lock = threading.Lock() self._lock = threading.Lock()
@ -50,7 +49,6 @@ class DebugPayloadRouter:
self._persist = False self._persist = False
self._hub = simulation_hub self._hub = simulation_hub
self._update_queue = update_queue
# Listeners for real-time target data broadcasts # Listeners for real-time target data broadcasts
self._ris_target_listeners: List[TargetListListener] = [] self._ris_target_listeners: List[TargetListListener] = []
@ -73,7 +71,7 @@ class DebugPayloadRouter:
ord("r"): self._handle_ris_status, ord("r"): self._handle_ris_status,
} }
logger.info( logger.info(
f"{self._log_prefix} Initialized (Hub: {self._hub is not None}, Queue: {self._update_queue is not None})." f"{self._log_prefix} Initialized (Hub: {self._hub is not None})."
) )
self._logger = logger self._logger = logger
@ -216,13 +214,13 @@ class DebugPayloadRouter:
"Failed to propagate heading to hub", exc_info=True "Failed to propagate heading to hub", exc_info=True
) )
if self._update_queue: #if self._update_queue:
try: # try:
self._update_queue.put_nowait([]) # self._update_queue.put_nowait([])
except Full: # except Full:
self._logger.warning( # self._logger.warning(
f"{self._log_prefix} GUI update queue is full; dropped notification." # f"{self._log_prefix} GUI update queue is full; dropped notification."
) # )
except Exception: except Exception:
self._logger.exception( self._logger.exception(
"DebugPayloadRouter: Failed to process RIS for Hub." "DebugPayloadRouter: Failed to process RIS for Hub."