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
|
# 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()
|
||||||
|
|||||||
@ -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()
|
||||||
|
|||||||
@ -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)
|
||||||
@ -100,12 +100,9 @@ class MainView(tk.Tk):
|
|||||||
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.")
|
||||||
|
|
||||||
# Always poll the GUI update queue so the main PPI receives real-time
|
# Start the new rendering loop
|
||||||
# hub updates even when the live simulation engine is not running.
|
self.after(GUI_REFRESH_RATE_MS, self._gui_refresh_loop)
|
||||||
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)
|
||||||
|
|||||||
@ -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."
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user