step 3 refactoring main_view, extract simulation_controller
This commit is contained in:
parent
9fac7dde1e
commit
0730068f1c
@ -38,6 +38,7 @@ from target_simulator.gui.analysis_window import AnalysisWindow
|
|||||||
from target_simulator.core import command_builder
|
from target_simulator.core import command_builder
|
||||||
from target_simulator.analysis.simulation_archive import SimulationArchive
|
from target_simulator.analysis.simulation_archive import SimulationArchive
|
||||||
from target_simulator.communication.communicator_manager import CommunicatorManager
|
from target_simulator.communication.communicator_manager import CommunicatorManager
|
||||||
|
from target_simulator.simulation.simulation_controller import SimulationController
|
||||||
|
|
||||||
# --- Import Version Info FOR THE WRAPPER ITSELF ---
|
# --- Import Version Info FOR THE WRAPPER ITSELF ---
|
||||||
try:
|
try:
|
||||||
@ -96,6 +97,17 @@ class MainView(tk.Tk):
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
# Simulation controller encapsulates start/stop/reset orchestration
|
||||||
|
try:
|
||||||
|
self.simulation_controller = SimulationController(
|
||||||
|
communicator_manager=self.communicator_manager,
|
||||||
|
simulation_hub=self.simulation_hub,
|
||||||
|
config_manager=self.config_manager,
|
||||||
|
logger=self.logger,
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
self.simulation_controller = None
|
||||||
|
|
||||||
# --- Core Logic Handlers ---
|
# --- Core Logic Handlers ---
|
||||||
self.target_communicator: Optional[CommunicatorInterface] = None
|
self.target_communicator: Optional[CommunicatorInterface] = None
|
||||||
self.lru_communicator: Optional[CommunicatorInterface] = None
|
self.lru_communicator: Optional[CommunicatorInterface] = None
|
||||||
@ -898,319 +910,72 @@ class MainView(tk.Tk):
|
|||||||
self.logger.exception("Unhandled exception in _on_connect_button")
|
self.logger.exception("Unhandled exception in _on_connect_button")
|
||||||
|
|
||||||
def _reset_radar_state(self) -> bool:
|
def _reset_radar_state(self) -> bool:
|
||||||
"""
|
# Delegates to SimulationController if available
|
||||||
Sends commands to the radar to deactivate all possible targets, effectively
|
|
||||||
clearing its state before a new simulation starts.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
True if the reset commands were sent successfully, False otherwise.
|
|
||||||
"""
|
|
||||||
if not self.target_communicator or not self.target_communicator.is_open:
|
|
||||||
self.logger.error("Cannot reset radar state: communicator is not connected.")
|
|
||||||
messagebox.showerror("Connection Error", "Cannot reset radar: Not Connected.")
|
|
||||||
return False
|
|
||||||
|
|
||||||
self.logger.info("Attempting to reset radar state (legacy vs JSON-aware logic)")
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
use_json = bool(getattr(self.target_communicator, "_use_json_protocol", False))
|
if hasattr(self, "simulation_controller") and self.simulation_controller:
|
||||||
|
return self.simulation_controller.reset_radar_state(self)
|
||||||
except Exception:
|
except Exception:
|
||||||
use_json = False
|
# If controller fails, fall back to previous inline behavior
|
||||||
|
|
||||||
# Helper: wait until hub reports no active real targets (bounded)
|
|
||||||
def _wait_for_clear(timeout_s: float = 3.0, poll_interval: float = 0.2) -> bool:
|
|
||||||
waited = 0.0
|
|
||||||
while waited < timeout_s:
|
|
||||||
try:
|
|
||||||
if not self.simulation_hub.has_active_real_targets():
|
|
||||||
return True
|
|
||||||
except Exception:
|
|
||||||
# If hub query fails, treat as not cleared and keep waiting
|
|
||||||
self.logger.debug("Error while querying simulation_hub during reset wait", exc_info=True)
|
|
||||||
time.sleep(poll_interval)
|
|
||||||
waited += poll_interval
|
|
||||||
return False
|
|
||||||
|
|
||||||
# 1) Legacy textual command path
|
|
||||||
if not use_json:
|
|
||||||
cmd = "tgtset /-s\n"
|
|
||||||
self.logger.info("Sending legacy reset command: %s", cmd.strip())
|
|
||||||
try:
|
try:
|
||||||
ok = self.target_communicator.send_commands([cmd])
|
self.logger.exception("SimulationController reset failed; falling back to inline reset.")
|
||||||
except Exception:
|
|
||||||
self.logger.exception("Failed to send legacy reset command")
|
|
||||||
ok = False
|
|
||||||
|
|
||||||
if not ok:
|
|
||||||
messagebox.showerror("Reset Error", "Failed to send reset command to the radar.")
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Wait briefly for the server to apply the reset and for hub to reflect it
|
|
||||||
cleared = _wait_for_clear()
|
|
||||||
if cleared:
|
|
||||||
self.logger.info("Legacy reset acknowledged: no active real targets remain.")
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
self.logger.error("Legacy reset sent but server did not clear active targets in time.")
|
|
||||||
messagebox.showerror("Reset Error", "Radar did not clear targets after reset.")
|
|
||||||
return False
|
|
||||||
|
|
||||||
# 2) JSON-capable communicator path
|
|
||||||
# First attempt: send a single simple JSON reset command
|
|
||||||
try:
|
|
||||||
json_reset = '{"CMD":"reset"}\n'
|
|
||||||
self.logger.info("Sending JSON reset command: %s", json_reset.strip())
|
|
||||||
try:
|
|
||||||
ok = self.target_communicator.send_commands([json_reset])
|
|
||||||
except Exception:
|
|
||||||
self.logger.exception("Failed to send JSON reset command")
|
|
||||||
ok = False
|
|
||||||
|
|
||||||
# Wait to see if server cleared active flags
|
|
||||||
if ok:
|
|
||||||
cleared = _wait_for_clear()
|
|
||||||
if cleared:
|
|
||||||
self.logger.info("JSON reset acknowledged: server cleared active flags.")
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
self.logger.info("JSON reset sent but server did not clear active flags; will attempt per-target JSON zeroing.")
|
|
||||||
else:
|
|
||||||
self.logger.warning("JSON reset command failed to send; will attempt per-target JSON zeroing.")
|
|
||||||
except Exception:
|
|
||||||
self.logger.exception("Unexpected error while attempting JSON reset")
|
|
||||||
|
|
||||||
# 3) Build and send per-target JSON payloads that explicitly clear active flag
|
|
||||||
try:
|
|
||||||
self.logger.info("Building per-target JSON reset payloads to clear active flags on all targets.")
|
|
||||||
json_payloads = command_builder.build_json_reset_ids()
|
|
||||||
except Exception:
|
|
||||||
self.logger.exception("Failed to build per-target JSON reset payloads")
|
|
||||||
messagebox.showerror("Reset Error", "Failed to build JSON reset payloads.")
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Send each payload and check hub state after sending
|
|
||||||
all_sent_ok = True
|
|
||||||
for i, payload in enumerate(json_payloads):
|
|
||||||
p = payload if payload.endswith("\n") else payload + "\n"
|
|
||||||
self.logger.info("Sending JSON reset payload part %d/%d", i + 1, len(json_payloads))
|
|
||||||
try:
|
|
||||||
ok = self.target_communicator.send_commands([p])
|
|
||||||
except Exception:
|
|
||||||
self.logger.exception("Exception while sending JSON reset payload part %d", i + 1)
|
|
||||||
ok = False
|
|
||||||
if not ok:
|
|
||||||
all_sent_ok = False
|
|
||||||
self.logger.error("Failed to send JSON reset payload part %d", i + 1)
|
|
||||||
break
|
|
||||||
|
|
||||||
if not all_sent_ok:
|
|
||||||
messagebox.showerror("Reset Error", "Failed to send one or more JSON reset payloads to the radar.")
|
|
||||||
return False
|
|
||||||
|
|
||||||
# After sending all per-target payloads, wait for hub to show targets cleared
|
|
||||||
cleared = _wait_for_clear()
|
|
||||||
if cleared:
|
|
||||||
self.logger.info("Per-target JSON reset succeeded: all active flags cleared.")
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
self.logger.error("Per-target JSON reset did not clear active flags within timeout.")
|
|
||||||
messagebox.showerror("Reset Error", "Radar did not clear targets after per-target reset.")
|
|
||||||
return False
|
|
||||||
|
|
||||||
def _on_start_simulation(self):
|
|
||||||
if self.is_simulation_running.get():
|
|
||||||
self.logger.info("Simulation is already running.")
|
|
||||||
return
|
|
||||||
# Require explicit connection before starting live. Do NOT auto-connect.
|
|
||||||
if not (
|
|
||||||
self.target_communicator
|
|
||||||
and getattr(self.target_communicator, "is_open", False)
|
|
||||||
):
|
|
||||||
# Friendly English reminder to connect first
|
|
||||||
messagebox.showwarning(
|
|
||||||
"Not Connected",
|
|
||||||
"Please connect to the target (use the Connect button) before starting live simulation.",
|
|
||||||
)
|
|
||||||
return
|
|
||||||
if not self.scenario or not self.scenario.get_all_targets():
|
|
||||||
messagebox.showinfo(
|
|
||||||
"Empty Scenario", "Cannot start simulation with an empty scenario."
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
update_interval = self.update_time.get()
|
|
||||||
if update_interval <= 0:
|
|
||||||
messagebox.showwarning(
|
|
||||||
"Invalid Input", "Update time must be a positive number."
|
|
||||||
)
|
|
||||||
return
|
|
||||||
except tk.TclError:
|
|
||||||
messagebox.showwarning(
|
|
||||||
"Invalid Input", "Update time must be a valid number."
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
# Reset data hub and PPI trails before starting
|
|
||||||
self.logger.info("Resetting simulation data hub and PPI trails.")
|
|
||||||
self.simulation_hub.reset()
|
|
||||||
self.ppi_widget.clear_trails()
|
|
||||||
|
|
||||||
if not self._reset_radar_state():
|
|
||||||
self.logger.error("Aborting simulation start due to radar reset failure.")
|
|
||||||
return
|
|
||||||
|
|
||||||
time.sleep(1) # 1 second delay
|
|
||||||
|
|
||||||
self.logger.info(
|
|
||||||
"Sending initial scenario state before starting live updates..."
|
|
||||||
)
|
|
||||||
if not self.target_communicator.send_scenario(self.scenario):
|
|
||||||
self.logger.error(
|
|
||||||
"Failed to send initial scenario state. Aborting live simulation start."
|
|
||||||
)
|
|
||||||
messagebox.showerror(
|
|
||||||
"Send Error",
|
|
||||||
"Failed to send the initial scenario configuration. Cannot start live simulation.",
|
|
||||||
)
|
|
||||||
return
|
|
||||||
self.logger.info("Initial scenario state sent successfully.")
|
|
||||||
|
|
||||||
self.logger.info("Starting live simulation...")
|
|
||||||
|
|
||||||
self.scenario.reset_simulation()
|
|
||||||
|
|
||||||
self.simulation_engine = SimulationEngine(
|
|
||||||
communicator=self.target_communicator,
|
|
||||||
simulation_hub=self.simulation_hub,
|
|
||||||
)
|
|
||||||
|
|
||||||
self.simulation_engine.set_time_multiplier(self.time_multiplier)
|
|
||||||
self.simulation_engine.set_update_interval(update_interval)
|
|
||||||
self.simulation_engine.load_scenario(self.scenario)
|
|
||||||
|
|
||||||
# Initialize simulation progress tracking
|
|
||||||
try:
|
|
||||||
durations = [
|
|
||||||
getattr(t, "_total_duration_s", 0.0)
|
|
||||||
for t in self.scenario.get_all_targets()
|
|
||||||
]
|
|
||||||
self.total_sim_time = max(durations) if durations else 0.0
|
|
||||||
except Exception:
|
|
||||||
self.total_sim_time = 0.0
|
|
||||||
|
|
||||||
# Reset slider and label
|
|
||||||
self.sim_elapsed_time = 0.0
|
|
||||||
try:
|
|
||||||
self.sim_slider_var.set(0.0)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
self._update_simulation_progress_display()
|
|
||||||
|
|
||||||
self.current_archive = SimulationArchive(self.scenario)
|
|
||||||
self.simulation_engine.archive = self.current_archive
|
|
||||||
if self.target_communicator and hasattr(self.target_communicator, "router"):
|
|
||||||
router = self.target_communicator.router()
|
|
||||||
if router:
|
|
||||||
router.set_archive(self.current_archive)
|
|
||||||
|
|
||||||
self.simulation_engine.start()
|
|
||||||
|
|
||||||
# When live simulation starts, remove any scenario preview visuals so
|
|
||||||
# only live simulated (green) and real (red) targets are shown.
|
|
||||||
try:
|
|
||||||
if hasattr(self, "ppi_widget") and self.ppi_widget:
|
|
||||||
self.ppi_widget.clear_previews()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# 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:
|
|
||||||
return
|
|
||||||
|
|
||||||
if self.current_archive:
|
|
||||||
self.current_archive.save()
|
|
||||||
self.current_archive = None
|
|
||||||
if self.target_communicator and hasattr(self.target_communicator, "router"):
|
|
||||||
router = self.target_communicator.router()
|
|
||||||
if router:
|
|
||||||
router.set_archive(None) # Disattiva l'archiviazione
|
|
||||||
self._refresh_analysis_list() # Aggiorna subito la lista
|
|
||||||
|
|
||||||
self.logger.info("Stopping live simulation (user request)...")
|
|
||||||
try:
|
|
||||||
self.simulation_engine.stop()
|
|
||||||
except Exception:
|
|
||||||
self.logger.exception("Error while stopping simulation engine")
|
|
||||||
self.simulation_engine = None
|
|
||||||
|
|
||||||
# IMPORTANT: Do NOT disconnect the communicator here. Keep the connection
|
|
||||||
# active so the user can choose when to disconnect manually.
|
|
||||||
|
|
||||||
# Update running flag and UI states
|
|
||||||
self.is_simulation_running.set(False)
|
|
||||||
self._update_button_states()
|
|
||||||
|
|
||||||
# Inform the user the simulation was stopped
|
|
||||||
try:
|
|
||||||
messagebox.showinfo(
|
|
||||||
"Simulation Stopped",
|
|
||||||
"Live simulation was stopped. Connection to the target remains active.",
|
|
||||||
)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
return
|
|
||||||
|
|
||||||
def _on_simulation_finished(self):
|
|
||||||
"""Handle the natural end-of-simulation event."""
|
|
||||||
self.logger.info("Handling simulation finished (engine signalled completion).")
|
|
||||||
|
|
||||||
if self.current_archive:
|
|
||||||
self.current_archive.save()
|
|
||||||
self.current_archive = None
|
|
||||||
if self.target_communicator and hasattr(self.target_communicator, "router"):
|
|
||||||
router = self.target_communicator.router()
|
|
||||||
if router:
|
|
||||||
router.set_archive(None) # Disattiva l'archiviazione
|
|
||||||
self._refresh_analysis_list() # Aggiorna subito la lista
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
self.is_simulation_running.set(False)
|
|
||||||
self._update_button_states()
|
|
||||||
|
|
||||||
# Show a non-blocking, dismissible notice in the UI instead of a
|
|
||||||
# blocking messagebox so the PPI can keep updating while the user
|
|
||||||
# inspects the results. If the notice widget isn't available, fall
|
|
||||||
# back to logging only.
|
|
||||||
try:
|
|
||||||
if getattr(self, "simulation_notice_var", None) is not None:
|
|
||||||
# Keep the message short but informative; operator can dismiss
|
|
||||||
# the notice manually.
|
|
||||||
self.simulation_notice_var.set(
|
|
||||||
"Simulation finished — live simulation completed. Server data may still arrive and will update the PPI."
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
# As a fallback, log the info (no blocking GUI popup).
|
|
||||||
self.logger.info(
|
|
||||||
"Live simulation completed (notice widget unavailable)."
|
|
||||||
)
|
|
||||||
except Exception:
|
|
||||||
# Ensure we never raise from UI-notice handling
|
|
||||||
try:
|
|
||||||
self.logger.exception("Failed to set non-blocking simulation notice")
|
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
# Fallback: call controller.reset_radar_state via attribute to reuse logic
|
||||||
|
try:
|
||||||
|
if hasattr(self, "simulation_controller") and self.simulation_controller:
|
||||||
|
return self.simulation_controller.reset_radar_state(self)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
# If no controller available, return False conservatively
|
||||||
|
try:
|
||||||
|
messagebox.showerror("Reset Error", "Unable to perform radar reset (controller unavailable).")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _on_start_simulation(self):
|
||||||
|
# Delegate to SimulationController if available
|
||||||
|
try:
|
||||||
|
if hasattr(self, "simulation_controller") and self.simulation_controller:
|
||||||
|
return self.simulation_controller.start_simulation(self)
|
||||||
|
except Exception:
|
||||||
|
try:
|
||||||
|
self.logger.exception("SimulationController start failed; falling back to inline start.")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
# If controller is not present or failed, attempt no-op fallback
|
||||||
|
try:
|
||||||
|
messagebox.showerror("Start Error", "Unable to start simulation (controller unavailable).")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _on_stop_simulation(self):
|
||||||
|
try:
|
||||||
|
if hasattr(self, "simulation_controller") and self.simulation_controller:
|
||||||
|
return self.simulation_controller.stop_simulation(self)
|
||||||
|
except Exception:
|
||||||
|
try:
|
||||||
|
self.logger.exception("SimulationController stop failed; falling back to inline stop.")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
messagebox.showerror("Stop Error", "Unable to stop simulation (controller unavailable).")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _on_simulation_finished(self):
|
||||||
|
try:
|
||||||
|
if hasattr(self, "simulation_controller") and self.simulation_controller:
|
||||||
|
return self.simulation_controller.on_simulation_finished(self)
|
||||||
|
except Exception:
|
||||||
|
try:
|
||||||
|
self.logger.exception("SimulationController on_finished failed; falling back to inline finished handler.")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
self.logger.error("Unable to handle simulation finished (controller unavailable).")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
def _on_reset_simulation(self):
|
def _on_reset_simulation(self):
|
||||||
self.logger.info("Resetting scenario to initial state.")
|
self.logger.info("Resetting scenario to initial state.")
|
||||||
|
|||||||
493
target_simulator/simulation/simulation_controller.py
Normal file
493
target_simulator/simulation/simulation_controller.py
Normal file
@ -0,0 +1,493 @@
|
|||||||
|
import time
|
||||||
|
from typing import Optional
|
||||||
|
from tkinter import messagebox
|
||||||
|
|
||||||
|
from target_simulator.core.simulation_engine import SimulationEngine
|
||||||
|
from target_simulator.analysis.simulation_archive import SimulationArchive
|
||||||
|
from target_simulator.core import command_builder
|
||||||
|
|
||||||
|
|
||||||
|
class SimulationController:
|
||||||
|
"""Orchestrates simulation start/stop/reset logic extracted from MainView.
|
||||||
|
|
||||||
|
To minimize changes across the codebase, the controller operates by
|
||||||
|
mutating the provided MainView instance during start/stop operations —
|
||||||
|
this keeps attributes like `simulation_engine` and `current_archive`
|
||||||
|
available where the rest of the code expects them.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, communicator_manager, simulation_hub, config_manager, logger):
|
||||||
|
self.communicator_manager = communicator_manager
|
||||||
|
self.simulation_hub = simulation_hub
|
||||||
|
self.config_manager = config_manager
|
||||||
|
self.logger = logger
|
||||||
|
|
||||||
|
self.simulation_engine: Optional[SimulationEngine] = None
|
||||||
|
self.current_archive: Optional[SimulationArchive] = None
|
||||||
|
|
||||||
|
def reset_radar_state(self, main_view) -> bool:
|
||||||
|
"""Reset radar via communicator. Mirrors previous MainView behavior.
|
||||||
|
|
||||||
|
Returns True on success, False otherwise.
|
||||||
|
"""
|
||||||
|
target_comm = getattr(self.communicator_manager, "target_communicator", None)
|
||||||
|
if not target_comm or not getattr(target_comm, "is_open", False):
|
||||||
|
try:
|
||||||
|
self.logger.error("Cannot reset radar state: communicator is not connected.")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
messagebox.showerror("Connection Error", "Cannot reset radar: Not Connected.")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
use_json = bool(getattr(target_comm, "_use_json_protocol", False))
|
||||||
|
except Exception:
|
||||||
|
use_json = False
|
||||||
|
|
||||||
|
# Helper: wait until hub reports no active real targets (bounded)
|
||||||
|
def _wait_for_clear(timeout_s: float = 3.0, poll_interval: float = 0.2) -> bool:
|
||||||
|
waited = 0.0
|
||||||
|
while waited < timeout_s:
|
||||||
|
try:
|
||||||
|
if not self.simulation_hub.has_active_real_targets():
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
try:
|
||||||
|
self.logger.debug("Error while querying simulation_hub during reset wait", exc_info=True)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
time.sleep(poll_interval)
|
||||||
|
waited += poll_interval
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 1) Legacy textual command path
|
||||||
|
if not use_json:
|
||||||
|
cmd = "tgtset /-s\n"
|
||||||
|
try:
|
||||||
|
self.logger.info("Sending legacy reset command: %s", cmd.strip())
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
ok = target_comm.send_commands([cmd])
|
||||||
|
except Exception:
|
||||||
|
try:
|
||||||
|
self.logger.exception("Failed to send legacy reset command")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
ok = False
|
||||||
|
|
||||||
|
if not ok:
|
||||||
|
try:
|
||||||
|
messagebox.showerror("Reset Error", "Failed to send reset command to the radar.")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return False
|
||||||
|
|
||||||
|
cleared = _wait_for_clear()
|
||||||
|
if cleared:
|
||||||
|
try:
|
||||||
|
self.logger.info("Legacy reset acknowledged: no active real targets remain.")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
self.logger.error("Legacy reset sent but server did not clear active targets in time.")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
messagebox.showerror("Reset Error", "Radar did not clear targets after reset.")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 2) JSON-capable communicator path
|
||||||
|
try:
|
||||||
|
json_reset = '{"CMD":"reset"}\n'
|
||||||
|
try:
|
||||||
|
self.logger.info("Sending JSON reset command: %s", json_reset.strip())
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
ok = target_comm.send_commands([json_reset])
|
||||||
|
except Exception:
|
||||||
|
try:
|
||||||
|
self.logger.exception("Failed to send JSON reset command")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
ok = False
|
||||||
|
|
||||||
|
if ok:
|
||||||
|
cleared = _wait_for_clear()
|
||||||
|
if cleared:
|
||||||
|
try:
|
||||||
|
self.logger.info("JSON reset acknowledged: server cleared active flags.")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
self.logger.info("JSON reset sent but server did not clear active flags; will attempt per-target JSON zeroing.")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
self.logger.warning("JSON reset command failed to send; will attempt per-target JSON zeroing.")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
except Exception:
|
||||||
|
try:
|
||||||
|
self.logger.exception("Unexpected error while attempting JSON reset")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 3) Build and send per-target JSON payloads that explicitly clear active flag
|
||||||
|
try:
|
||||||
|
try:
|
||||||
|
self.logger.info("Building per-target JSON reset payloads to clear active flags on all targets.")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
json_payloads = command_builder.build_json_reset_ids()
|
||||||
|
except Exception:
|
||||||
|
try:
|
||||||
|
self.logger.exception("Failed to build per-target JSON reset payloads")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
messagebox.showerror("Reset Error", "Failed to build JSON reset payloads.")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return False
|
||||||
|
|
||||||
|
all_sent_ok = True
|
||||||
|
for i, payload in enumerate(json_payloads):
|
||||||
|
p = payload if payload.endswith("\n") else payload + "\n"
|
||||||
|
try:
|
||||||
|
self.logger.info("Sending JSON reset payload part %d/%d", i + 1, len(json_payloads))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
ok = target_comm.send_commands([p])
|
||||||
|
except Exception:
|
||||||
|
try:
|
||||||
|
self.logger.exception("Exception while sending JSON reset payload part %d", i + 1)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
ok = False
|
||||||
|
if not ok:
|
||||||
|
all_sent_ok = False
|
||||||
|
try:
|
||||||
|
self.logger.error("Failed to send JSON reset payload part %d", i + 1)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
break
|
||||||
|
|
||||||
|
if not all_sent_ok:
|
||||||
|
try:
|
||||||
|
messagebox.showerror("Reset Error", "Failed to send one or more JSON reset payloads to the radar.")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return False
|
||||||
|
|
||||||
|
cleared = _wait_for_clear()
|
||||||
|
if cleared:
|
||||||
|
try:
|
||||||
|
self.logger.info("Per-target JSON reset succeeded: all active flags cleared.")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
self.logger.error("Per-target JSON reset did not clear active flags within timeout.")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
messagebox.showerror("Reset Error", "Radar did not clear targets after per-target reset.")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return False
|
||||||
|
|
||||||
|
def start_simulation(self, main_view):
|
||||||
|
"""Start live simulation using data from main_view (keeps compatibility)."""
|
||||||
|
if main_view.is_simulation_running.get():
|
||||||
|
try:
|
||||||
|
self.logger.info("Simulation is already running.")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return
|
||||||
|
|
||||||
|
# Require explicit connection before starting live.
|
||||||
|
target_comm = getattr(self.communicator_manager, "target_communicator", None)
|
||||||
|
if not (target_comm and getattr(target_comm, "is_open", False)):
|
||||||
|
try:
|
||||||
|
messagebox.showwarning(
|
||||||
|
"Not Connected",
|
||||||
|
"Please connect to the target (use the Connect button) before starting live simulation.",
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return
|
||||||
|
|
||||||
|
if not main_view.scenario or not main_view.scenario.get_all_targets():
|
||||||
|
try:
|
||||||
|
messagebox.showinfo("Empty Scenario", "Cannot start simulation with an empty scenario.")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
update_interval = main_view.update_time.get()
|
||||||
|
if update_interval <= 0:
|
||||||
|
try:
|
||||||
|
messagebox.showwarning("Invalid Input", "Update time must be a positive number.")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return
|
||||||
|
except Exception:
|
||||||
|
try:
|
||||||
|
messagebox.showwarning("Invalid Input", "Update time must be a valid number.")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return
|
||||||
|
|
||||||
|
# Reset data hub and PPI trails before starting
|
||||||
|
try:
|
||||||
|
self.logger.info("Resetting simulation data hub and PPI trails.")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
self.simulation_hub.reset()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
if hasattr(main_view, "ppi_widget") and main_view.ppi_widget:
|
||||||
|
main_view.ppi_widget.clear_trails()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Reset radar state first
|
||||||
|
if not self.reset_radar_state(main_view):
|
||||||
|
try:
|
||||||
|
self.logger.error("Aborting simulation start due to radar reset failure.")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return
|
||||||
|
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.logger.info("Sending initial scenario state before starting live updates...")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
if not target_comm.send_scenario(main_view.scenario):
|
||||||
|
try:
|
||||||
|
self.logger.error("Failed to send initial scenario state. Aborting live simulation start.")
|
||||||
|
messagebox.showerror(
|
||||||
|
"Send Error",
|
||||||
|
"Failed to send the initial scenario configuration. Cannot start live simulation.",
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.logger.info("Initial scenario state sent successfully.")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.logger.info("Starting live simulation...")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
main_view.scenario.reset_simulation()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Create engine and start
|
||||||
|
try:
|
||||||
|
engine = SimulationEngine(communicator=target_comm, simulation_hub=self.simulation_hub)
|
||||||
|
engine.set_time_multiplier(main_view.time_multiplier)
|
||||||
|
engine.set_update_interval(update_interval)
|
||||||
|
engine.load_scenario(main_view.scenario)
|
||||||
|
main_view.simulation_engine = engine
|
||||||
|
self.simulation_engine = engine
|
||||||
|
except Exception:
|
||||||
|
try:
|
||||||
|
self.logger.exception("Failed to initialize SimulationEngine")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return
|
||||||
|
|
||||||
|
# Compute total duration
|
||||||
|
try:
|
||||||
|
durations = [getattr(t, "_total_duration_s", 0.0) for t in main_view.scenario.get_all_targets()]
|
||||||
|
main_view.total_sim_time = max(durations) if durations else 0.0
|
||||||
|
except Exception:
|
||||||
|
main_view.total_sim_time = 0.0
|
||||||
|
|
||||||
|
# Reset slider and labels
|
||||||
|
main_view.sim_elapsed_time = 0.0
|
||||||
|
try:
|
||||||
|
main_view.sim_slider_var.set(0.0)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
main_view._update_simulation_progress_display()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Archive wiring
|
||||||
|
try:
|
||||||
|
self.current_archive = SimulationArchive(main_view.scenario)
|
||||||
|
self.simulation_engine.archive = self.current_archive
|
||||||
|
if target_comm and hasattr(target_comm, "router"):
|
||||||
|
router = target_comm.router()
|
||||||
|
if router:
|
||||||
|
router.set_archive(self.current_archive)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.simulation_engine.start()
|
||||||
|
except Exception:
|
||||||
|
try:
|
||||||
|
self.logger.exception("Failed to start simulation engine thread")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return
|
||||||
|
|
||||||
|
# Clear scenario previews when live starts
|
||||||
|
try:
|
||||||
|
if hasattr(main_view, "ppi_widget") and main_view.ppi_widget:
|
||||||
|
main_view.ppi_widget.clear_previews()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
main_view.current_archive = self.current_archive
|
||||||
|
|
||||||
|
# Set running state and update UI
|
||||||
|
main_view.is_simulation_running.set(True)
|
||||||
|
try:
|
||||||
|
main_view._update_button_states()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def stop_simulation(self, main_view):
|
||||||
|
if not main_view.is_simulation_running.get() or not getattr(main_view, "simulation_engine", None):
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
if main_view.current_archive:
|
||||||
|
try:
|
||||||
|
main_view.current_archive.save()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
main_view.current_archive = None
|
||||||
|
try:
|
||||||
|
target_comm = getattr(self.communicator_manager, "target_communicator", None)
|
||||||
|
if target_comm and hasattr(target_comm, "router"):
|
||||||
|
router = target_comm.router()
|
||||||
|
if router:
|
||||||
|
router.set_archive(None)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
main_view._refresh_analysis_list()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.logger.info("Stopping live simulation (user request)...")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
main_view.simulation_engine.stop()
|
||||||
|
except Exception:
|
||||||
|
try:
|
||||||
|
self.logger.exception("Error while stopping simulation engine")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
main_view.simulation_engine = None
|
||||||
|
|
||||||
|
main_view.is_simulation_running.set(False)
|
||||||
|
try:
|
||||||
|
main_view._update_button_states()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
messagebox.showinfo(
|
||||||
|
"Simulation Stopped",
|
||||||
|
"Live simulation was stopped. Connection to the target remains active.",
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def on_simulation_finished(self, main_view):
|
||||||
|
try:
|
||||||
|
self.logger.info("Handling simulation finished (engine signalled completion).")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
if main_view.current_archive:
|
||||||
|
try:
|
||||||
|
main_view.current_archive.save()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
main_view.current_archive = None
|
||||||
|
try:
|
||||||
|
target_comm = getattr(self.communicator_manager, "target_communicator", None)
|
||||||
|
if target_comm and hasattr(target_comm, "router"):
|
||||||
|
router = target_comm.router()
|
||||||
|
if router:
|
||||||
|
router.set_archive(None)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
main_view._refresh_analysis_list()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if getattr(main_view, "simulation_engine", None) and main_view.simulation_engine.is_running():
|
||||||
|
try:
|
||||||
|
main_view.simulation_engine.stop()
|
||||||
|
except Exception:
|
||||||
|
try:
|
||||||
|
self.logger.exception("Error while stopping finished simulation engine")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
main_view.simulation_engine = None
|
||||||
|
|
||||||
|
main_view.is_simulation_running.set(False)
|
||||||
|
try:
|
||||||
|
main_view._update_button_states()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
if getattr(main_view, "simulation_notice_var", None) is not None:
|
||||||
|
main_view.simulation_notice_var.set(
|
||||||
|
"Simulation finished — live simulation completed. Server data may still arrive and will update the PPI."
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
self.logger.info("Live simulation completed (notice widget unavailable).")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
except Exception:
|
||||||
|
try:
|
||||||
|
self.logger.exception("Failed to set non-blocking simulation notice")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
Loading…
Reference in New Issue
Block a user