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
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):
@ -188,15 +188,6 @@ class SFPCommunicator(CommunicatorInterface):
self.logger.exception("Error while shutting down transport")
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._destination = None
self._notify_connection_state_changed()

View File

@ -26,14 +26,12 @@ class SimulationEngine(threading.Thread):
def __init__(
self,
communicator: Optional[CommunicatorInterface],
update_queue: Optional[Queue],
simulation_hub: Optional[SimulationStateHub] = None,
):
super().__init__(daemon=True, name="SimulationEngineThread")
self.logger = get_logger(__name__)
self.communicator = communicator
self.update_queue = update_queue
self.simulation_hub = simulation_hub # Hub for data analysis
self.time_multiplier = 1.0
self.update_interval_s = 1.0
@ -111,26 +109,11 @@ class SimulationEngine(threading.Thread):
simulated_delta_time = delta_time * self.time_multiplier
self.scenario.update_state(simulated_delta_time)
updated_targets = self.scenario.get_all_targets()
if self.scenario.is_finished():
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
# --- High-Frequency State Logging ---
tick_timestamp = time.monotonic()
active_targets = [t for t in self.scenario.get_all_targets() if t.active]
# --- Communication, Data Hub, and GUI Update Step ---
if (current_time - self._last_update_time) >= self.update_interval_s:
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 = (
@ -139,9 +122,17 @@ class SimulationEngine(threading.Thread):
getattr(target, "_pos_z_ft", 0.0),
)
self.simulation_hub.add_simulated_state(
target.target_id, timestamp_for_batch, state_tuple
target.target_id, tick_timestamp, state_tuple
)
if self.scenario.is_finished():
self.logger.info("Scenario finished. Stopping engine.")
break
# --- Throttled Communication Step ---
if (current_time - self._last_update_time) >= self.update_interval_s:
self._last_update_time = current_time
if self.communicator and self.communicator.is_open:
commands_to_send = []
@ -162,7 +153,7 @@ class SimulationEngine(threading.Thread):
getattr(target, "_pos_z_ft", 0.0),
)
append_sent_position(
timestamp_for_batch,
tick_timestamp,
target.target_id,
state_tuple[0],
state_tuple[1],
@ -182,7 +173,7 @@ class SimulationEngine(threading.Thread):
getattr(target, "_pos_z_ft", 0.0),
)
append_sent_position(
timestamp_for_batch,
tick_timestamp,
target.target_id,
state_tuple[0],
state_tuple[1],
@ -194,15 +185,6 @@ class SimulationEngine(threading.Thread):
if 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)
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_REFRESH_RATE_MS = 40
class MainView(tk.Tk):
@ -68,7 +69,6 @@ class MainView(tk.Tk):
# --- Simulation Engine ---
self.simulation_engine: Optional[SimulationEngine] = None
self.gui_update_queue = Queue()
self.is_simulation_running = tk.BooleanVar(value=False)
self.time_multiplier = 1.0
self.update_time = tk.DoubleVar(value=1.0)
@ -100,12 +100,9 @@ class MainView(tk.Tk):
self.protocol("WM_DELETE_WINDOW", self._on_closing)
self.logger.info("MainView initialized successfully.")
# 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")
# Start the new rendering loop
self.after(GUI_REFRESH_RATE_MS, self._gui_refresh_loop)
# Schedule periodic rate status updates (shows events/sec for real inputs and PPI updates)
try:
# Start after one second to allow initial state to settle
@ -594,9 +591,9 @@ class MainView(tk.Tk):
communicator = TFTPCommunicator()
config_data = config.get("tftp", {})
elif comm_type == "sfp":
# --- MODIFICATION: Pass the hub and GUI update queue to the communicator ---
# --- MODIFICATION: Do not pass update_queue ---
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)
config_data = config.get("sfp", {})
@ -832,8 +829,6 @@ class MainView(tk.Tk):
self.logger.error("Aborting simulation start due to radar reset failure.")
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
self.logger.info(
@ -851,14 +846,11 @@ class MainView(tk.Tk):
self.logger.info("Initial scenario state sent successfully.")
self.logger.info("Starting live simulation...")
self.is_simulation_running.set(True)
self._update_button_states()
self.scenario.reset_simulation()
self.simulation_engine = SimulationEngine(
communicator=self.target_communicator,
update_queue=self.gui_update_queue,
simulation_hub=self.simulation_hub,
)
@ -886,7 +878,9 @@ class MainView(tk.Tk):
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):
if not self.is_simulation_running.get() or not self.simulation_engine:
@ -918,29 +912,19 @@ class MainView(tk.Tk):
return
def _on_simulation_finished(self):
"""Handle the natural end-of-simulation event coming from the engine.
This should update the UI and inform the user, but must NOT disconnect the
communicator the connection stays active until the user explicitly disconnects.
"""
"""Handle the natural end-of-simulation event."""
self.logger.info("Handling simulation finished (engine signalled completion).")
# If engine still exists, stop it cleanly
try:
if self.simulation_engine and getattr(self.simulation_engine, "is_running", False):
if self.simulation_engine and self.simulation_engine.is_running():
try:
self.simulation_engine.stop()
except Exception:
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._update_button_states()
# Notify the user in English that the simulation finished
try:
messagebox.showinfo("Simulation Finished", "The live simulation has completed.")
except Exception:
@ -955,19 +939,19 @@ class MainView(tk.Tk):
self._update_all_views()
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:
for _ in range(MAX_UPDATES_PER_CYCLE):
try:
# We process one update at a time to keep the GUI responsive
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:
self.sim_elapsed_time = self.total_sim_time
self.sim_slider_var.set(1.0 if self.total_sim_time > 0 else 0.0)
@ -976,76 +960,45 @@ class MainView(tk.Tk):
self._update_simulation_progress_display()
elif isinstance(update, list):
# The engine normally enqueues a List[Target] (simulated targets).
# However, the simulation payload handler uses an empty list []
# as a lightweight notification that real states were added to
# the hub. Distinguish the two cases:
if len(update) == 0:
# Hub refresh notification (real data arrived).
# Only update the 'real' targets on the PPI display.
# self.logger.debug("MainView: received hub refresh. Updating real targets.")
display_data = self._build_display_data_from_hub()
self.ppi_widget.update_real_targets(display_data.get("real", []))
# Also propagate platform/antenna azimuth (if available) so the PPI
# can render the antenna orientation. The hub stores a (az_deg, ts)
# tuple via set_platform_azimuth().
try:
if (
hasattr(self, "simulation_hub")
and self.simulation_hub is not None
and hasattr(self.ppi_widget, "update_antenna_azimuth")
):
try:
# Prefer the new API name, fall back to the legacy
if hasattr(self.simulation_hub, "get_antenna_azimuth"):
az_deg, az_ts = (
self.simulation_hub.get_antenna_azimuth()
)
az_deg, az_ts = self.simulation_hub.get_antenna_azimuth()
else:
az_deg, az_ts = (
self.simulation_hub.get_platform_azimuth()
)
az_deg, az_ts = self.simulation_hub.get_platform_azimuth()
if az_deg is not None:
# pass the hub-provided timestamp if available
self.ppi_widget.update_antenna_azimuth(
az_deg, timestamp=az_ts
)
except Exception:
# don't allow GUI update failures to interrupt queue processing
self.logger.debug(
"Failed to propagate antenna azimuth to PPI",
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:
self.sim_elapsed_time += 0.0
# Update slider only if user is not interacting with it
if self.total_sim_time > 0 and not getattr(
self, "_slider_is_dragging", False
):
@ -1057,19 +1010,17 @@ class MainView(tk.Tk):
self._update_simulation_progress_display()
except Exception:
# Do not allow progress UI failures to interrupt GUI updates
self.logger.debug("Progress UI update failed", exc_info=True)
except Empty:
# If the queue is empty, we don't need to do anything
pass
# Queue is empty, we can stop processing for this cycle.
break
finally:
# Always continue polling the GUI update queue so we can show
# real-time server updates on the PPI even when the live
# simulation engine is not running.
# Always reschedule the next poll.
try:
self.after(GUI_QUEUE_POLL_INTERVAL_MS, self._process_gui_queue)
except Exception:
# This can happen on shutdown, just ignore.
pass
def _update_button_states(self):
@ -1643,3 +1594,52 @@ class MainView(tk.Tk):
self.analysis_window = AnalysisWindow(
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__(
self,
simulation_hub: Optional[SimulationStateHub] = None,
update_queue: Optional[Queue] = None,
):
self._log_prefix = "[DebugPayloadRouter]"
self._lock = threading.Lock()
@ -50,7 +49,6 @@ class DebugPayloadRouter:
self._persist = False
self._hub = simulation_hub
self._update_queue = update_queue
# Listeners for real-time target data broadcasts
self._ris_target_listeners: List[TargetListListener] = []
@ -73,7 +71,7 @@ class DebugPayloadRouter:
ord("r"): self._handle_ris_status,
}
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
@ -216,13 +214,13 @@ class DebugPayloadRouter:
"Failed to propagate heading to hub", exc_info=True
)
if self._update_queue:
try:
self._update_queue.put_nowait([])
except Full:
self._logger.warning(
f"{self._log_prefix} GUI update queue is full; dropped notification."
)
#if self._update_queue:
# try:
# self._update_queue.put_nowait([])
# except Full:
# self._logger.warning(
# f"{self._log_prefix} GUI update queue is full; dropped notification."
# )
except Exception:
self._logger.exception(
"DebugPayloadRouter: Failed to process RIS for Hub."