separato in thread diversi la ricezione, invio e aggiornamenti grafica
This commit is contained in:
parent
8c439b60c3
commit
73a7817f5c
@ -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()
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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."
|
||||
|
||||
Loading…
Reference in New Issue
Block a user