S1005403_RisCC/target_simulator/simulation/simulation_controller.py

236 lines
11 KiB
Python

# target_simulator/simulation/simulation_controller.py
import time
import threading
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."""
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_no_ui(self, main_view) -> bool:
"""Runs the reset sequence without showing UI popups. Safe for threads."""
target_comm = getattr(self.communicator_manager, "target_communicator", None)
if not target_comm or not getattr(target_comm, "is_open", False):
self.logger.error("Cannot reset radar: communicator is not connected.")
return False
use_json = getattr(target_comm, "_use_json_protocol", False)
def _wait_for_clear(timeout_s: float) -> bool:
start_time = time.monotonic()
while time.monotonic() - start_time < timeout_s:
if not self.simulation_hub.has_active_real_targets():
return True
time.sleep(0.2)
return False
if not use_json:
self.logger.info("Sending legacy reset command...")
if not target_comm.send_commands(["tgtset /-s\n"]):
self.logger.error("Failed to send legacy reset command.")
return False
if not _wait_for_clear(3.0):
self.logger.error("Legacy reset failed: server did not clear targets.")
return False
return True
# JSON path with fallback
self.logger.info("Sending primary JSON reset command...")
if target_comm.send_commands(['{"CMD":"reset"}\n']):
if _wait_for_clear(1.0):
self.logger.info("Primary JSON reset successful.")
return True
self.logger.warning("Primary JSON reset timed out. Trying fallback...")
else:
self.logger.warning("Primary JSON reset command failed. Trying fallback...")
self.logger.info("Sending fallback per-ID JSON reset payloads...")
try:
json_payloads = command_builder.build_json_reset_ids()
except Exception as e:
self.logger.exception(f"Failed to build fallback payloads: {e}")
return False
for i, payload in enumerate(json_payloads):
if not target_comm.send_commands([payload]):
self.logger.error(f"Failed to send fallback payload part {i+1}.")
return False
if not _wait_for_clear(3.0):
self.logger.error("Fallback per-ID JSON reset failed to clear targets.")
return False
self.logger.info("Fallback per-ID JSON reset successful.")
return True
def start_simulation(self, main_view):
"""Start live simulation asynchronously."""
target_comm = getattr(self.communicator_manager, "target_communicator", None)
if not (target_comm and getattr(target_comm, "is_open", False)):
messagebox.showwarning("Not Connected", "Please connect before starting.")
main_view._start_in_progress_main = False # Release the guard
return
if not main_view.scenario or not main_view.scenario.get_all_targets():
messagebox.showinfo("Empty Scenario", "Cannot start an empty scenario.")
main_view._start_in_progress_main = False # Release the guard
return
def _background_start():
self.logger.info("Background start process initiated.")
reset_ok = self._reset_radar_state_no_ui(main_view)
if not reset_ok:
self.logger.error("Radar reset failed. Aborting start.")
main_view.after(0, lambda: messagebox.showerror(
"Start Failed", "Could not reset radar state. Simulation aborted."
))
main_view.after(0, self._finalize_start_failure, main_view)
return
self.logger.info("Sending initial scenario state...")
if not target_comm.send_scenario(main_view.scenario):
self.logger.error("Failed to send scenario. Aborting start.")
main_view.after(0, lambda: messagebox.showerror(
"Start Failed", "Could not send scenario. Aborted."
))
main_view.after(0, self._finalize_start_failure, main_view)
return
try:
self.logger.info("Initializing SimulationEngine.")
engine = SimulationEngine(communicator=target_comm, simulation_hub=self.simulation_hub)
engine.set_update_interval(main_view.update_time.get())
engine.load_scenario(main_view.scenario)
main_view.simulation_engine = self.simulation_engine = engine
durations = [getattr(t, "_total_duration_s", 0.0) for t in main_view.scenario.get_all_targets()]
total_time = max(durations) if durations else 0.0
archive = SimulationArchive(main_view.scenario)
engine.archive = archive
if hasattr(target_comm, "router"):
router = target_comm.router()
if router:
router.set_archive(archive)
self.logger.info("Starting SimulationEngine thread.")
engine.start()
main_view.after(0, self._finalize_start_success, main_view, archive, total_time)
except Exception as e:
self.logger.exception(f"Failed to start SimulationEngine: {e}")
main_view.after(0, lambda: messagebox.showerror("Start Failed", f"Error: {e}"))
main_view.after(0, self._finalize_start_failure, main_view)
# UI updates on main thread
main_view.show_status_message("Starting simulation...", timeout_ms=None)
main_view._update_button_states()
self.simulation_hub.reset()
if hasattr(main_view, "ppi_widget"):
main_view.ppi_widget.clear_trails()
threading.Thread(target=_background_start, daemon=True).start()
def _finalize_start_success(self, main_view, archive, total_time):
"""Callback on main thread after a successful background start."""
main_view.current_archive = self.current_archive = archive
main_view.total_sim_time = total_time
main_view.sim_elapsed_time = 0.0
main_view.simulation_controls.sim_slider_var.set(0.0)
main_view._update_simulation_progress_display()
if hasattr(main_view, "ppi_widget"):
main_view.ppi_widget.clear_previews()
main_view.is_simulation_running.set(True)
main_view._start_in_progress_main = False
main_view._update_button_states()
main_view.show_status_message("Simulation running", timeout_ms=None)
self.logger.info("Simulation started successfully.")
def _finalize_start_failure(self, main_view):
"""Callback on main thread after a failed background start."""
main_view.is_simulation_running.set(False)
main_view._start_in_progress_main = False
main_view._update_button_states()
main_view.clear_status_message()
self.logger.warning("Simulation start process failed and was finalized.")
def _stop_or_finish_simulation(self, main_view, was_stopped_by_user: bool):
"""Unified logic for handling simulation end, either by user or naturally."""
if self.current_archive:
# --- NUOVA AGGIUNTA INIZIO ---
# Retrieve estimated latency before saving the archive
estimated_latency_s = 0.0
extra_metadata = {}
try:
target_comm = getattr(self.communicator_manager, "target_communicator", None)
if target_comm and hasattr(target_comm, 'router'):
router = target_comm.router()
if router and hasattr(router, 'get_estimated_latency_s'):
estimated_latency_s = router.get_estimated_latency_s()
if estimated_latency_s > 0:
extra_metadata['estimated_latency_ms'] = round(estimated_latency_s * 1000, 2)
# Retrieve prediction offset from config
conn_settings = self.config_manager.get_connection_settings()
target_sfp_cfg = conn_settings.get("target", {}).get("sfp", {})
offset_ms = target_sfp_cfg.get("prediction_offset_ms", 0.0)
if offset_ms > 0:
extra_metadata['prediction_offset_ms'] = offset_ms
except Exception as e:
self.logger.warning(f"Could not retrieve estimated latency for archive: {e}")
# --- NUOVA AGGIUNTA FINE ---
self.current_archive.save(extra_metadata=extra_metadata)
self.current_archive = main_view.current_archive = None
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)
main_view._refresh_analysis_list()
if self.simulation_engine:
self.simulation_engine.stop()
self.simulation_engine = main_view.simulation_engine = None
main_view.is_simulation_running.set(False)
main_view._update_button_states()
if was_stopped_by_user:
main_view.show_status_message("Simulation stopped.", timeout_ms=5000)
else:
main_view.show_status_message("Simulation finished.", timeout_ms=5000)
main_view.simulation_controls.show_notice("Simulation has finished.")
def stop_simulation(self, main_view):
"""Handles user request to stop the simulation."""
if not main_view.is_simulation_running.get():
return
self.logger.info("Stopping live simulation (user request)...")
self._stop_or_finish_simulation(main_view, was_stopped_by_user=True)
def on_simulation_finished(self, main_view):
"""Handles the natural end of a simulation."""
if not main_view.is_simulation_running.get():
return
self.logger.info("Simulation engine finished execution.")
self._stop_or_finish_simulation(main_view, was_stopped_by_user=False)