S1005403_RisCC/target_simulator/simulation/simulation_controller.py
2025-11-04 08:19:17 +01:00

764 lines
28 KiB
Python

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
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.
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
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 _reset_radar_state_no_ui(self, main_view) -> bool:
"""Same logic as reset_radar_state but avoids any Tk UI calls (messagebox).
This variant is safe to call from a background thread; failures are
logged and a boolean is returned so the caller can decide how to
notify the user on the main thread.
"""
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
return False
try:
use_json = bool(getattr(target_comm, "_use_json_protocol", False))
except Exception:
use_json = False
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
# Legacy textual 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:
self.logger.error("Failed to send reset command to the radar.")
except Exception:
pass
return False
cleared = _wait_for_clear()
return bool(cleared)
# 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
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
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:
self.logger.error("Failed to send one or more JSON reset payloads to the radar.")
except Exception:
pass
return False
cleared = _wait_for_clear()
return bool(cleared)
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
# Do UI updates (disable/notify) and run heavy ops in background
def _background_start():
# Reset radar state in background
ok = self._reset_radar_state_no_ui(main_view)
if not ok:
try:
self.logger.error("Aborting simulation start due to radar reset failure.")
except Exception:
pass
try:
main_view.after(0, lambda: messagebox.showerror("Reset Error", "Failed to reset radar before starting simulation."))
except Exception:
pass
try:
# Clear start-in-progress flag and update UI on main thread
main_view.after(0, lambda: (setattr(main_view, "_start_in_progress_main", False), main_view._update_button_states()))
except Exception:
pass
return
time.sleep(1)
# Send initial scenario
try:
self.logger.info("Sending initial scenario state before starting live updates...")
except Exception:
pass
try:
sent = target_comm.send_scenario(main_view.scenario)
except Exception:
sent = False
if not sent:
try:
self.logger.error("Failed to send initial scenario state. Aborting live simulation start.")
except Exception:
pass
try:
main_view.after(0, lambda: messagebox.showerror("Send Error", "Failed to send the initial scenario configuration. Cannot start live simulation."))
except Exception:
pass
try:
main_view.after(0, lambda: (setattr(main_view, "_start_in_progress_main", False), main_view._update_button_states()))
except Exception:
pass
return
# Initialize engine
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)
self.simulation_engine = engine
except Exception:
try:
self.logger.exception("Failed to initialize SimulationEngine")
except Exception:
pass
try:
main_view.after(0, lambda: messagebox.showerror("Engine Error", "Failed to initialize simulation engine."))
except Exception:
pass
try:
main_view.after(0, lambda: (setattr(main_view, "_start_in_progress_main", False), main_view._update_button_states()))
except Exception:
pass
return
# Compute durations, archive, and start engine thread
try:
durations = [getattr(t, "_total_duration_s", 0.0) for t in main_view.scenario.get_all_targets()]
total_sim_time = max(durations) if durations else 0.0
except Exception:
total_sim_time = 0.0
try:
current_archive = SimulationArchive(main_view.scenario)
engine.archive = current_archive
if target_comm and hasattr(target_comm, "router"):
router = target_comm.router()
if router:
router.set_archive(current_archive)
except Exception:
current_archive = None
try:
engine.start()
except Exception:
try:
self.logger.exception("Failed to start simulation engine thread")
except Exception:
pass
try:
main_view.after(0, lambda: messagebox.showerror("Start Error", "Failed to start simulation engine thread."))
except Exception:
pass
try:
main_view.after(0, lambda: (setattr(main_view, "_start_in_progress_main", False), main_view._update_button_states()))
except Exception:
pass
return
# Finalize on main thread
def _on_started():
try:
main_view.simulation_engine = engine
self.current_archive = current_archive
main_view.current_archive = current_archive
main_view.total_sim_time = total_sim_time
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
try:
if hasattr(main_view, "ppi_widget") and main_view.ppi_widget:
main_view.ppi_widget.clear_previews()
except Exception:
pass
main_view.is_simulation_running.set(True)
# Clear the start-in-progress flag and update UI
try:
main_view._start_in_progress_main = False
except Exception:
pass
try:
main_view._update_button_states()
except Exception:
pass
except Exception:
pass
try:
main_view.after(0, _on_started)
except Exception:
pass
# Mark that a start is in progress so the UI keeps Start disabled
try:
# Set synchronously so immediate callers see the flag
main_view._start_in_progress_main = True
except Exception:
pass
# UI: show status and disable controls before background work
try:
main_view.after(0, lambda: main_view.status_bar.show_status_message("Starting simulation...", timeout_ms=0))
except Exception:
pass
try:
main_view.after(0, lambda: main_view._update_button_states())
except Exception:
pass
t = threading.Thread(target=_background_start, daemon=True)
t.start()
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
try:
main_view.simulation_engine.stop()
except Exception:
pass
main_view.simulation_engine = None
self.simulation_engine = None
except Exception:
pass
main_view.is_simulation_running.set(False)
try:
main_view._update_button_states()
except Exception:
pass
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