S1005403_RisCC/target_simulator/gui/main_view.py

445 lines
20 KiB
Python

# target_simulator/gui/main_view.py
"""
Main view of the application, containing the primary window and widgets.
"""
import tkinter as tk
from tkinter import ttk, scrolledtext, messagebox
from queue import Queue, Empty
from typing import Optional, Dict, Any, List
# Imports from GUI components
from .ppi_display import PPIDisplay
from .connection_settings_window import ConnectionSettingsWindow
from .radar_config_window import RadarConfigWindow
from .scenario_controls_frame import ScenarioControlsFrame
from .target_list_frame import TargetListFrame
# Imports from Core components
from core.communicator_interface import CommunicatorInterface
from core.serial_communicator import SerialCommunicator
from core.tftp_communicator import TFTPCommunicator
from core.simulation_engine import SimulationEngine
from core.models import Scenario, Target
# Imports from Utils
from utils.logger import get_logger, shutdown_logging_system
from utils.config_manager import ConfigManager
GUI_QUEUE_POLL_INTERVAL_MS = 100
class MainView(tk.Tk):
"""The main application window."""
def __init__(self):
super().__init__()
self.logger = get_logger(__name__)
self.config_manager = ConfigManager()
# --- Load Settings ---
settings = self.config_manager.get_general_settings()
self.scan_limit = settings.get('scan_limit', 60)
self.max_range = settings.get('max_range', 100)
self.connection_config = self.config_manager.get_connection_settings()
# --- Core Logic Handlers ---
self.target_communicator: Optional[CommunicatorInterface] = None
self.lru_communicator: Optional[CommunicatorInterface] = None
self.scenario = Scenario()
self.current_scenario_name: Optional[str] = None
# --- Simulation Engine ---
self.simulation_engine: Optional[SimulationEngine] = None
self.gui_update_queue = Queue()
self.is_simulation_running = tk.BooleanVar(value=False)
# --- Window and UI Setup ---
self.title("Radar Target Simulator")
self.geometry(settings.get('geometry', '1200x1024'))
self.minsize(1024, 768)
self._create_menubar()
self._create_main_layout()
self._create_statusbar()
# --- Post-UI Initialization ---
self._initialize_communicators()
self._load_scenarios_into_ui()
last_scenario = settings.get("last_selected_scenario")
if last_scenario and last_scenario in self.config_manager.get_scenario_names():
self._on_load_scenario(last_scenario)
self._update_window_title()
self.protocol("WM_DELETE_WINDOW", self._on_closing)
self.logger.info("MainView initialized successfully.")
def _create_main_layout(self):
v_pane = ttk.PanedWindow(self, orient=tk.VERTICAL)
v_pane.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
self.h_pane = ttk.PanedWindow(v_pane, orient=tk.HORIZONTAL)
v_pane.add(self.h_pane, weight=4)
# --- Right Pane (PPI) ---
self.ppi_widget = PPIDisplay(self.h_pane, max_range_nm=self.max_range, scan_limit_deg=self.scan_limit)
self.h_pane.add(self.ppi_widget, weight=2)
controls_frame = self.ppi_widget.children.get('!frame')
if controls_frame:
connect_btn = ttk.Button(controls_frame, text="Connect", command=self._on_connect_button)
connect_btn.pack(side=tk.RIGHT, padx=10)
# --- Left Pane ---
left_pane_container = ttk.Frame(self.h_pane)
self.h_pane.add(left_pane_container, weight=1)
left_notebook = ttk.Notebook(left_pane_container)
left_notebook.pack(fill=tk.BOTH, expand=True)
# --- TAB 1: SCENARIO CONFIG ---
scenario_tab = ttk.Frame(left_notebook)
left_notebook.add(scenario_tab, text="Scenario Config")
self.scenario_controls = ScenarioControlsFrame(
scenario_tab,
main_view=self,
load_scenario_command=self._on_load_scenario,
save_as_command=self._on_save_scenario_as,
delete_command=self._on_delete_scenario,
new_scenario_command=self._on_new_scenario
)
self.scenario_controls.pack(fill=tk.X, expand=False, padx=5, pady=(5, 5))
self.target_list = TargetListFrame(scenario_tab, targets_changed_callback=self._on_targets_changed)
self.target_list.pack(fill=tk.BOTH, expand=True, padx=5)
# --- TAB 2: SIMULATION ---
simulation_tab = ttk.Frame(left_notebook)
left_notebook.add(simulation_tab, text="Simulation")
sim_scenario_frame = ttk.LabelFrame(simulation_tab, text="Scenario Control")
sim_scenario_frame.pack(fill=tk.X, padx=5, pady=5, anchor='n')
ttk.Label(sim_scenario_frame, text="Scenario:").pack(side=tk.LEFT, padx=(5, 5), pady=5)
self.sim_scenario_combobox = ttk.Combobox(
sim_scenario_frame,
textvariable=self.scenario_controls.current_scenario, # Share the variable
state="readonly"
)
self.sim_scenario_combobox.pack(side=tk.LEFT, expand=True, fill=tk.X, pady=5)
self.sim_scenario_combobox.bind("<<ComboboxSelected>>", lambda event: self._on_load_scenario(self.sim_scenario_combobox.get()))
engine_frame = ttk.LabelFrame(simulation_tab, text="Live Simulation Engine")
engine_frame.pack(fill=tk.X, padx=5, pady=10, anchor='n')
self.start_button = ttk.Button(engine_frame, text="Start Live", command=self._on_start_simulation)
self.start_button.pack(side=tk.LEFT, padx=5, pady=5)
self.stop_button = ttk.Button(engine_frame, text="Stop Live", command=self._on_stop_simulation, state=tk.DISABLED)
self.stop_button.pack(side=tk.LEFT, padx=5, pady=5)
self.reset_button = ttk.Button(engine_frame, text="Reset State", command=self._on_reset_simulation)
self.reset_button.pack(side=tk.RIGHT, padx=5, pady=5)
# --- TAB 3: LRU SIMULATION ---
lru_tab = ttk.Frame(left_notebook)
left_notebook.add(lru_tab, text="LRU Simulation")
cooling_frame = ttk.LabelFrame(lru_tab, text="Cooling Unit Status")
cooling_frame.pack(fill=tk.X, padx=5, pady=5, anchor='n')
ttk.Label(cooling_frame, text="Status:").pack(side=tk.LEFT, padx=5, pady=5)
self.cooling_status_var = tk.StringVar(value="OK")
cooling_combo = ttk.Combobox(cooling_frame, textvariable=self.cooling_status_var, values=["OK", "OVERHEATING", "FAULT"], state="readonly")
cooling_combo.pack(side=tk.LEFT, expand=True, fill=tk.X, padx=5, pady=5)
power_frame = ttk.LabelFrame(lru_tab, text="Power Supply Unit Status")
power_frame.pack(fill=tk.X, padx=5, pady=5, anchor='n')
ttk.Label(power_frame, text="Status:").pack(side=tk.LEFT, padx=5, pady=5)
self.power_status_var = tk.StringVar(value="OK")
power_combo = ttk.Combobox(power_frame, textvariable=self.power_status_var, values=["OK", "LOW_VOLTAGE", "FAULT"], state="readonly")
power_combo.pack(side=tk.LEFT, expand=True, fill=tk.X, padx=5, pady=5)
lru_action_frame = ttk.Frame(lru_tab)
lru_action_frame.pack(fill=tk.X, padx=5, pady=10, anchor='n')
send_lru_button = ttk.Button(lru_action_frame, text="Send LRU Status", command=self._on_send_lru_status)
send_lru_button.pack(side=tk.RIGHT)
# --- Bottom Pane (Logs) ---
log_frame_container = ttk.LabelFrame(v_pane, text="Logs")
v_pane.add(log_frame_container, weight=1)
self.log_text_widget = scrolledtext.ScrolledText(log_frame_container, state=tk.DISABLED, wrap=tk.WORD, font=("Consolas", 9))
self.log_text_widget.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
def _create_menubar(self):
menubar = tk.Menu(self)
self.config(menu=menubar)
settings_menu = tk.Menu(menubar, tearoff=0)
menubar.add_cascade(label="Settings", menu=settings_menu)
settings_menu.add_command(label="Connection...", command=self._open_settings)
settings_menu.add_command(label="Radar Config...", command=self._open_radar_config)
def _create_statusbar(self):
status_bar = ttk.Frame(self, relief=tk.SUNKEN)
status_bar.pack(side=tk.BOTTOM, fill=tk.X)
ttk.Label(status_bar, text="Target:").pack(side=tk.LEFT, padx=(5, 2))
self.target_status_canvas = tk.Canvas(status_bar, width=16, height=16, highlightthickness=0)
self.target_status_canvas.pack(side=tk.LEFT, padx=(0, 10))
self._draw_status_indicator(self.target_status_canvas, "#e74c3c")
ttk.Label(status_bar, text="LRU:").pack(side=tk.LEFT, padx=(5, 2))
self.lru_status_canvas = tk.Canvas(status_bar, width=16, height=16, highlightthickness=0)
self.lru_status_canvas.pack(side=tk.LEFT, padx=(0, 10))
self._draw_status_indicator(self.lru_status_canvas, "#e74c3c")
self.status_var = tk.StringVar(value="Ready")
ttk.Label(status_bar, textvariable=self.status_var, anchor=tk.W).pack(side=tk.LEFT, fill=tk.X, expand=True, padx=5)
def _draw_status_indicator(self, canvas, color):
canvas.delete("all")
canvas.create_oval(2, 2, 14, 14, fill=color, outline="black")
def _update_window_title(self):
"""Updates the window title based on the current scenario."""
base_title = "Radar Target Simulator"
if self.current_scenario_name:
self.title(f"{base_title} - {self.current_scenario_name}")
else:
self.title(base_title)
def _update_communicator_status(self, comm_name: str, is_connected: bool):
canvas = self.target_status_canvas if comm_name == 'Target' else self.lru_status_canvas
color = "#2ecc40" if is_connected else "#e74c3c"
self._draw_status_indicator(canvas, color)
def _initialize_communicators(self):
# Disconnect any existing connections
if self.target_communicator and self.target_communicator.is_open: self.target_communicator.disconnect()
if self.lru_communicator and self.lru_communicator.is_open: self.lru_communicator.disconnect()
target_cfg = self.connection_config.get("target", {})
lru_cfg = self.connection_config.get("lru", {})
# Initialize Target Communicator
self.target_communicator, target_connected = self._setup_communicator(target_cfg, "Target")
self._update_communicator_status("Target", target_connected)
# Initialize LRU Communicator
self.lru_communicator, lru_connected = self._setup_communicator(lru_cfg, "LRU")
self._update_communicator_status("LRU", lru_connected)
def _setup_communicator(self, config: dict, name: str) -> (Optional[CommunicatorInterface], bool):
comm_type = config.get("type")
self.logger.info(f"Initializing {name} communicator of type: {comm_type}")
communicator = None
config_data = None
if comm_type == "serial":
communicator = SerialCommunicator()
config_data = config.get("serial", {})
elif comm_type == "tftp":
communicator = TFTPCommunicator()
config_data = config.get("tftp", {})
if communicator and config_data:
if communicator.connect(config_data):
return communicator, True
self.logger.warning(f"Failed to initialize or connect {name} communicator.")
return None, False
def update_connection_settings(self, new_config: Dict[str, Any]):
self.logger.info(f"Updating connection settings: {new_config}")
self.connection_config = new_config
self.config_manager.save_connection_settings(new_config)
self._initialize_communicators()
def _open_settings(self):
self.logger.info("Opening connection settings window.")
ConnectionSettingsWindow(self, self.config_manager, self.connection_config)
def _on_connect_button(self):
self.logger.info("Connection requested by user.")
self._initialize_communicators()
def _on_start_simulation(self):
if self.is_simulation_running.get():
self.logger.info("Simulation is already running.")
return
if not self.target_communicator or not self.target_communicator.is_open:
messagebox.showerror("Not Connected", "Target communicator is not connected. Please check settings and connect.")
return
if not self.scenario or not self.scenario.get_all_targets():
messagebox.showinfo("Empty Scenario", "Cannot start simulation with an empty scenario.")
return
self.logger.info("Starting live simulation...")
self.is_simulation_running.set(True)
self._update_button_states()
self.scenario.reset_simulation()
self.simulation_engine = SimulationEngine(self.target_communicator, self.gui_update_queue)
self.simulation_engine.load_scenario(self.scenario)
self.simulation_engine.start()
self.after(GUI_QUEUE_POLL_INTERVAL_MS, self._process_gui_queue)
def _on_stop_simulation(self):
if not self.is_simulation_running.get() or not self.simulation_engine:
return
self.logger.info("Stopping live simulation...")
self.simulation_engine.stop()
self.simulation_engine = None
self.is_simulation_running.set(False)
self._update_button_states()
def _on_reset_simulation(self):
self.logger.info("Resetting scenario to initial state.")
if self.is_simulation_running.get():
self._on_stop_simulation()
self.scenario.reset_simulation()
self._update_all_views()
def _process_gui_queue(self):
try:
while not self.gui_update_queue.empty():
updated_targets: List[Target] = self.gui_update_queue.get_nowait()
self._update_all_views(updated_targets)
finally:
if self.is_simulation_running.get():
self.after(GUI_QUEUE_POLL_INTERVAL_MS, self._process_gui_queue)
def _update_button_states(self):
is_running = self.is_simulation_running.get()
state = tk.DISABLED if is_running else tk.NORMAL
self.start_button.config(state=tk.DISABLED if is_running else tk.NORMAL)
self.stop_button.config(state=tk.NORMAL if is_running else tk.DISABLED)
self.scenario_controls.new_button.config(state=state)
self.scenario_controls.save_button.config(state=state)
self.scenario_controls.save_as_button.config(state=state)
self.scenario_controls.delete_button.config(state=state)
self.scenario_controls.scenario_combobox.config(state="readonly" if not is_running else tk.DISABLED)
self.target_list.add_button.config(state=state)
self.target_list.remove_button.config(state=state)
self.target_list.edit_button.config(state=state)
self.target_list.tree.config(selectmode="browse" if not is_running else "none")
def _on_targets_changed(self, targets: List[Target]):
self.ppi_widget.update_targets(targets)
def _update_all_views(self, targets_to_display: Optional[List[Target]] = None):
self._update_window_title()
if targets_to_display is None:
targets_to_display = self.scenario.get_all_targets()
self.target_list.update_target_list(targets_to_display)
self.ppi_widget.update_targets(targets_to_display)
def _load_scenarios_into_ui(self):
scenario_names = self.config_manager.get_scenario_names()
self.scenario_controls.update_scenario_list(scenario_names, self.current_scenario_name)
self.sim_scenario_combobox['values'] = scenario_names
def _on_load_scenario(self, scenario_name: str):
if self.is_simulation_running.get(): self._on_stop_simulation()
self.logger.info(f"Loading scenario: {scenario_name}")
scenario_data = self.config_manager.get_scenario(scenario_name)
if scenario_data:
try:
self.scenario = Scenario.from_dict(scenario_data)
self.current_scenario_name = scenario_name
self._update_all_views()
self.sim_scenario_combobox.set(scenario_name)
except Exception as e:
self.logger.error(f"Failed to parse scenario data for '{scenario_name}': {e}", exc_info=True)
messagebox.showerror("Load Error", f"Could not load scenario '{scenario_name}'.\n{e}")
else:
self.logger.warning(f"Attempted to load a non-existent scenario: {scenario_name}")
def _on_save_scenario(self, scenario_name: str):
self.logger.info(f"Saving scenario: {scenario_name}")
self.scenario.targets = {t.target_id: t for t in self.target_list.get_targets()}
self.config_manager.save_scenario(scenario_name, self.scenario.to_dict())
self.current_scenario_name = scenario_name
self._load_scenarios_into_ui()
self._update_window_title()
messagebox.showinfo("Success", f"Scenario '{scenario_name}' saved successfully.", parent=self)
def _on_save_scenario_as(self, scenario_name: str):
self.scenario.targets = {t.target_id: t for t in self.target_list.get_targets()}
self._on_save_scenario(scenario_name)
def _on_new_scenario(self, scenario_name: str):
scenario_names = self.config_manager.get_scenario_names()
if scenario_name in scenario_names:
messagebox.showinfo("Duplicate Scenario", f"Scenario '{scenario_name}' already exists. Loading it instead.", parent=self)
self._on_load_scenario(scenario_name)
return
self.logger.info(f"Creating new scenario: {scenario_name}")
self.scenario = Scenario(name=scenario_name)
self.current_scenario_name = scenario_name
# Save the new empty scenario immediately so it's persisted
self.config_manager.save_scenario(scenario_name, self.scenario.to_dict())
# Update all UI elements
self._update_all_views() # Clears targets, updates title
self._load_scenarios_into_ui() # Reloads scenario lists in comboboxes
# After reloading, explicitly set the current selection to the new scenario
self.scenario_controls.current_scenario.set(scenario_name)
self.sim_scenario_combobox.set(scenario_name)
def _on_delete_scenario(self, scenario_name: str):
self.logger.info(f"Deleting scenario: {scenario_name}")
self.config_manager.delete_scenario(scenario_name)
if self.current_scenario_name == scenario_name:
self.current_scenario_name = None
self.scenario = Scenario()
self._update_all_views()
self._load_scenarios_into_ui()
def _on_send_lru_status(self):
# Implementation from your code
pass
def _on_reset_targets(self):
# Implementation from your code
pass
def _open_radar_config(self):
self.logger.info("Opening radar config window.")
dialog = RadarConfigWindow(self, current_scan_limit=self.scan_limit, current_max_range=self.max_range)
if dialog.scan_limit is not None and dialog.max_range is not None:
self.scan_limit = dialog.scan_limit
self.max_range = dialog.max_range
self.ppi_widget.destroy()
self.ppi_widget = PPIDisplay(self.h_pane, max_range_nm=self.max_range, scan_limit_deg=self.scan_limit)
self.h_pane.add(self.ppi_widget, weight=2)
self._update_all_views()
def _on_closing(self):
self.logger.info("Application shutting down.")
if self.is_simulation_running.get():
self._on_stop_simulation()
settings_to_save = {
'scan_limit': self.scan_limit,
'max_range': self.max_range,
'geometry': self.winfo_geometry(),
'last_selected_scenario': self.current_scenario_name
}
self.config_manager.save_general_settings(settings_to_save)
self.config_manager.save_connection_settings(self.connection_config)
if self.target_communicator and self.target_communicator.is_open: self.target_communicator.disconnect()
if self.lru_communicator and self.lru_communicator.is_open: self.lru_communicator.disconnect()
shutdown_logging_system()
self.destroy()