# 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 typing import Optional, Dict, Any from .ppi_display import PPIDisplay from .settings_window import SettingsWindow from .radar_config_window import RadarConfigWindow from .scenario_controls_frame import ScenarioControlsFrame from .target_list_frame import TargetListFrame from .add_target_window import AddTargetWindow from core.communicator_interface import CommunicatorInterface from core.serial_communicator import SerialCommunicator from core.tftp_communicator import TFTPCommunicator from core.models import Scenario from utils.logger import get_logger, shutdown_logging_system from utils.config_manager import ConfigManager from target_simulator.utils.logger import get_logger, shutdown_logging_system from target_simulator.utils.config_manager import ConfigManager class MainView(tk.Tk): """The main application window.""" def __init__(self): super().__init__() self.logger = get_logger(__name__) self.config_manager = ConfigManager() 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() self.title("Radar Target Simulator") self.geometry(settings.get('geometry', '1200x900')) self.minsize(900, 700) self.communicator = None self.current_scenario_name = None self._create_main_layout() self._create_menubar() self._create_statusbar() self._initialize_communicator() # Attempt to connect with saved settings self._load_scenarios_into_ui() 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) left_frame = ttk.Frame(self.h_pane) self.h_pane.add(left_frame, weight=1) 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) # --- Connessione Frame --- conn_frame = ttk.LabelFrame(left_frame, text="Connection") conn_frame.pack(fill=tk.X, padx=5, pady=(5, 0)) self.conn_type_var = tk.StringVar(value=self.connection_config.get("type", "N/A")) self.conn_params_var = tk.StringVar(value=self._get_connection_params_str()) # Connection status indicator (semaphore) self.conn_status_canvas = tk.Canvas(conn_frame, width=18, height=18, highlightthickness=0, bd=0) self.conn_status_canvas.grid(row=0, column=0, padx=(5,2), pady=2) self._draw_conn_status_indicator(connected=False) # Ripristino i widget e bottoni accanto al semaforo ttk.Label(conn_frame, text="Type:").grid(row=0, column=1, sticky=tk.W, padx=2, pady=2) ttk.Entry(conn_frame, textvariable=self.conn_type_var, state="readonly", width=10).grid(row=0, column=2, sticky=tk.W, padx=2, pady=2) ttk.Entry(conn_frame, textvariable=self.conn_params_var, state="readonly", width=22).grid(row=0, column=3, sticky=tk.W, padx=2, pady=2) self.connect_btn = ttk.Button(conn_frame, text="Connect", command=self._on_connect) self.connect_btn.grid(row=0, column=4, sticky=tk.W, padx=2, pady=2) self.disconnect_btn = ttk.Button(conn_frame, text="Disconnect", command=self._on_disconnect) self.disconnect_btn.grid(row=0, column=5, sticky=tk.W, padx=2, pady=2) self.connect_btn.state(["!disabled"]) self.disconnect_btn.state(["disabled"]) # --- Scenario Controls Frame --- self.scenario_controls = ScenarioControlsFrame(left_frame, load_scenario_command=self._on_load_scenario, save_as_command=self._on_save_scenario_as, update_command=self._on_update_scenario, delete_command=self._on_delete_scenario) self.scenario_controls.pack(fill=tk.X, expand=False, padx=5, pady=(5, 5)) self.target_list = TargetListFrame(left_frame, targets_changed_callback=self._on_targets_changed) self.target_list.pack(fill=tk.BOTH, expand=True, padx=5) # --- Logs Frame --- 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 _on_targets_changed(self, targets): # Called by TargetListFrame when targets are changed self.ppi_widget.update_targets(targets) def _draw_conn_status_indicator(self, connected: bool): self.conn_status_canvas.delete("all") color = "#2ecc40" if connected else "#e74c3c" # green or red self.conn_status_canvas.create_oval(2, 2, 16, 16, fill=color, outline="black") def _draw_conn_status_indicator(self, connected: bool): self.conn_status_canvas.delete("all") color = "#2ecc40" if connected else "#e74c3c" # green or red self.conn_status_canvas.create_oval(2, 2, 16, 16, fill=color, outline="black") def _get_connection_params_str(self): typ = self.connection_config.get("type", "N/A") if typ == "serial": params = self.connection_config.get("serial", {}) return f"Port: {params.get('port', '')}, Baud: {params.get('baudrate', '')}, Parity: {params.get('parity', '')}" elif typ == "tftp": params = self.connection_config.get("tftp", {}) return f"IP: {params.get('ip', '')}, Port: {params.get('port', '')}" return "" def _create_menubar(self): menubar = tk.Menu(self) self.config(menu=menubar) # Solo Settings Menu 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): self.status_var = tk.StringVar(value="Status: Disconnected") status_bar = ttk.Label(self, textvariable=self.status_var, anchor=tk.W, relief=tk.SUNKEN) status_bar.pack(side=tk.BOTTOM, fill=tk.X) def _initialize_communicator(self): """Creates and connects the appropriate communicator based on config.""" if self.communicator and self.communicator.is_open: self.communicator.disconnect() comm_type = self.connection_config.get("type") self.logger.info(f"Initializing communicator of type: {comm_type}") if comm_type == "serial": self.communicator = SerialCommunicator() config_data = self.connection_config.get("serial", {}) elif comm_type == "tftp": self.communicator = TFTPCommunicator() config_data = self.connection_config.get("tftp", {}) else: self.communicator = None self.status_var.set("Status: No communication type configured.") return if self.communicator.connect(config_data): self.status_var.set(f"Status: Connected via {comm_type.upper()}") else: self.status_var.set(f"Status: Disconnected (Failed to auto-connect)") def update_connection_settings(self, new_config: Dict[str, Any]): """Callback from SettingsWindow to apply and save new connection settings.""" self.logger.info(f"Updating connection settings: {new_config}") self.connection_config = new_config self.config_manager.save_connection_settings(new_config) self._initialize_communicator() # Re-initialize with new settings # Aggiorna la visualizzazione dei parametri e tipo connessione self.conn_type_var.set(self.connection_config.get("type", "N/A")) self.conn_params_var.set(self._get_connection_params_str()) def _open_settings(self): self.logger.info("Opening connection settings window.") SettingsWindow(self, self.config_manager, self.connection_config) def _on_connect(self): if not self.communicator: self.logger.warning("Cannot connect: communicator not initialized. Please configure settings.") messagebox.showwarning("Not Configured", "Please configure the connection in the Settings menu first.") return if self.communicator.is_open: self.logger.info("Already connected.") # Always update indicator and buttons if already connected self.connect_btn.state(["disabled"]) self.disconnect_btn.state(["!disabled"]) self._draw_conn_status_indicator(connected=True) return self._initialize_communicator() # Re-attempt connection # Update button states and indicator if self.communicator and self.communicator.is_open: self.connect_btn.state(["disabled"]) self.disconnect_btn.state(["!disabled"]) self._draw_conn_status_indicator(connected=True) def _on_disconnect(self): if self.communicator and self.communicator.is_open: self.communicator.disconnect() self.status_var.set("Status: Disconnected") self.logger.info("User manually disconnected.") self.connect_btn.state(["!disabled"]) self.disconnect_btn.state(["disabled"]) self._draw_conn_status_indicator(connected=False) else: self.logger.info("Already disconnected.") def _on_send_scenario(self): if self.communicator and self.communicator.is_open: if self.scenario and self.scenario.get_all_targets(): self.communicator.send_scenario(self.scenario) else: messagebox.showinfo("Empty Scenario", "Cannot send an empty scenario.", parent=self) else: messagebox.showerror("Not Connected", "Please connect before sending a scenario.", parent=self) # --- Other methods remain largely the same --- def _update_window_title(self): title = "Radar Target Simulator" if self.current_scenario_name: title = f"{self.current_scenario_name} - {title}" self.title(title) def _update_all_views(self): self._update_window_title() all_targets = self.scenario.get_all_targets() self.target_list.update_target_list(all_targets) self.ppi_widget.update_targets(all_targets) self.logger.info(f"Views updated for scenario '{self.current_scenario_name}'. It has {len(all_targets)} target(s).") 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) # Rimosso: la logica di aggiunta target è ora nel TargetListFrame def _on_load_scenario(self, scenario_name: str): 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() except Exception as e: self.logger.error(f"Failed to parse scenario data for '{scenario_name}': {e}") 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_as(self, scenario_name: str): self.logger.info(f"Saving current state as new scenario: {scenario_name}") scenario_data = self.scenario.to_dict() self.config_manager.save_scenario(scenario_name, scenario_data) self.current_scenario_name = scenario_name self._load_scenarios_into_ui() # Refresh list self._update_window_title() messagebox.showinfo("Success", f"Scenario '{scenario_name}' saved successfully.", parent=self) def _on_update_scenario(self, scenario_name: str): self.logger.info(f"Updating scenario: {scenario_name}") scenario_data = self.scenario.to_dict() self.config_manager.save_scenario(scenario_name, scenario_data) self._load_scenarios_into_ui() # Refresh list to be safe messagebox.showinfo("Success", f"Scenario '{scenario_name}' updated successfully.", parent=self) def _on_delete_scenario(self, scenario_name: str): self.logger.info(f"Deleting scenario: {scenario_name}") self.config_manager.delete_scenario(scenario_name) self.logger.info(f"Scenario '{scenario_name}' deleted.") if self.current_scenario_name == scenario_name: self.scenario = Scenario() self.current_scenario_name = None self._update_all_views() self._load_scenarios_into_ui() messagebox.showinfo("Deleted", f"Scenario '{scenario_name}' has been deleted.", parent=self) 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.logger.info(f"Scan limit set to: ±{self.scan_limit} degrees") self.logger.info(f"Max range set to: {self.max_range} NM") 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.") # Save general settings 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.communicator and self.communicator.is_open: self.communicator.disconnect() shutdown_logging_system() self.destroy()