From 3b447d7fdb316b742772aee7bfa95f5190d35937 Mon Sep 17 00:00:00 2001 From: VALLONGOL Date: Tue, 7 Oct 2025 14:40:52 +0200 Subject: [PATCH] fix save/new/load scenario minor fix in gui --- script.txt | 4 + scripts.txt | 4 + settings.json | 65 ++- target_simulator/core/command_builder.py | 2 +- target_simulator/core/tftp_communicator.py | 23 +- .../gui/connection_settings_window.py | 207 +++++++++ target_simulator/gui/main_view.py | 406 ++++++++++++------ .../gui/scenario_controls_frame.py | 48 ++- target_simulator/gui/target_list_frame.py | 6 +- target_simulator/utils/config_manager.py | 13 +- 10 files changed, 617 insertions(+), 161 deletions(-) create mode 100644 script.txt create mode 100644 scripts.txt create mode 100644 target_simulator/gui/connection_settings_window.py diff --git a/script.txt b/script.txt new file mode 100644 index 0000000..d16c125 --- /dev/null +++ b/script.txt @@ -0,0 +1,4 @@ +$tgtinit 0 20.00 45.00 500.00 270.00 10000.00 /s /t +$tgtinit 1 30.00 45.00 500.00 270.00 10000.00 /s /t +$tgtinit 2 60.00 20.00 500.00 270.00 10000.00 /s /t +$tgtinit 3 25.00 -10.00 130.00 20.00 20000.00 /s /t \ No newline at end of file diff --git a/scripts.txt b/scripts.txt new file mode 100644 index 0000000..d16c125 --- /dev/null +++ b/scripts.txt @@ -0,0 +1,4 @@ +$tgtinit 0 20.00 45.00 500.00 270.00 10000.00 /s /t +$tgtinit 1 30.00 45.00 500.00 270.00 10000.00 /s /t +$tgtinit 2 60.00 20.00 500.00 270.00 10000.00 /s /t +$tgtinit 3 25.00 -10.00 130.00 20.00 20000.00 /s /t \ No newline at end of file diff --git a/settings.json b/settings.json index 047c76c..b4afb50 100644 --- a/settings.json +++ b/settings.json @@ -5,10 +5,27 @@ "geometry": "1200x900+291+141", "last_selected_scenario": "scenario1", "connection": { - "type": "tftp", - "tftp": { - "ip": "127.0.0.1", - "port": 50071 + "target": { + "type": "tftp", + "tftp": { + "ip": "127.0.0.1", + "port": 50069 + }, + "serial": { + "port": "COM1", + "baudrate": 9600 + } + }, + "lru": { + "type": "serial", + "tftp": { + "ip": "127.0.0.1", + "port": 69 + }, + "serial": { + "port": "COM2", + "baudrate": 38400 + } } } }, @@ -57,6 +74,46 @@ "traceable": true } ] + }, + "scenario2": { + "name": "scenario2", + "targets": [ + { + "target_id": 0, + "range_nm": 20.0, + "azimuth_deg": 45.0, + "velocity_fps": 843.905, + "heading_deg": 270.0, + "altitude_ft": 10000.0, + "active": true, + "traceable": true + } + ] + }, + "scenario3": { + "name": "scenario3", + "targets": [ + { + "target_id": 0, + "range_nm": 20.0, + "azimuth_deg": 45.0, + "velocity_fps": 843.905, + "heading_deg": 270.0, + "altitude_ft": 10000.0, + "active": true, + "traceable": true + }, + { + "target_id": 1, + "range_nm": 20.0, + "azimuth_deg": 45.0, + "velocity_fps": 843.905, + "heading_deg": 270.0, + "altitude_ft": 10000.0, + "active": true, + "traceable": true + } + ] } } } \ No newline at end of file diff --git a/target_simulator/core/command_builder.py b/target_simulator/core/command_builder.py index 5631d94..ff9ed12 100644 --- a/target_simulator/core/command_builder.py +++ b/target_simulator/core/command_builder.py @@ -126,4 +126,4 @@ def build_aclatch() -> str: def build_acunlatch() -> str: """Builds the command to release the A/C data latch.""" - return "aclatch /r" \ No newline at end of file + return "aclatch /r" diff --git a/target_simulator/core/tftp_communicator.py b/target_simulator/core/tftp_communicator.py index bcaea52..b2cb9b0 100644 --- a/target_simulator/core/tftp_communicator.py +++ b/target_simulator/core/tftp_communicator.py @@ -20,7 +20,8 @@ from target_simulator.utils.logger import get_logger class TFTPCommunicator(CommunicatorInterface): """A class to manage and abstract TFTP communication.""" - REMOTE_FILENAME = "ris_scenario.txt" # The filename to upload on the TFTP server + REMOTE_FILENAME = "MON:script.txt" # The filename to upload on the TFTP server + LOCAL_FILENAME = "scripts.txt" def __init__(self): self.logger = get_logger(__name__) @@ -76,20 +77,28 @@ class TFTPCommunicator(CommunicatorInterface): # 1. Build the script content in memory script_lines = [] - # Per il manuale, gli script devono iniziare con una linea specifica - script_lines.append(";++++") for target in scenario.get_all_targets(): - script_lines.append(command_builder.build_tgtinit(target)) + command = command_builder.build_tgtinit(target) + script_lines.append(f"${command}") script_content = "\n".join(script_lines) - # 2. Use io.StringIO to treat the string as a file + # 2. Save the script to a local file for verification + try: + with open(self.LOCAL_FILENAME, "w") as f: + f.write(script_content) + self.logger.info(f"Scenario script saved locally to '{self.LOCAL_FILENAME}'") + except IOError as e: + self.logger.error(f"Failed to save script locally: {e}") + # We might not want to abort the whole process for this + + # 3. Use io.StringIO to treat the string as a file for upload in_memory_script = io.StringIO(script_content) - # 3. Upload the in-memory file + # 4. Upload the in-memory file try: client = TFTPClient(self.config['ip'], self.config['port']) - success = client.upload(self.REMOTE_FILENAME, in_memory_script) + success = client.upload(self.REMOTE_FILENAME, in_memory_script, mode="netascii") if success: self.logger.info(f"Successfully uploaded scenario as '{self.REMOTE_FILENAME}'.") return True diff --git a/target_simulator/gui/connection_settings_window.py b/target_simulator/gui/connection_settings_window.py new file mode 100644 index 0000000..bb4fa3d --- /dev/null +++ b/target_simulator/gui/connection_settings_window.py @@ -0,0 +1,207 @@ +# target_simulator/gui/connection_settings_window.py +""" +Toplevel window for configuring Target and LRU connections. +""" +import tkinter as tk +from tkinter import ttk, messagebox + +class ConnectionSettingsWindow(tk.Toplevel): + """A dialog for configuring connection settings.""" + def __init__(self, master, config_manager, connection_config): + super().__init__(master) + self.master_view = master + self.config_manager = config_manager + self.connection_config = connection_config + + self.title("Connection Settings") + self.transient(master) + self.grab_set() + self.resizable(False, False) + + self._create_widgets() + self._load_settings() + self.protocol("WM_DELETE_WINDOW", self._on_cancel) + + # Center the window on the main view + self.update_idletasks() + main_x = master.winfo_rootx() + main_y = master.winfo_rooty() + main_w = master.winfo_width() + main_h = master.winfo_height() + win_w = self.winfo_width() + win_h = self.winfo_height() + x = main_x + (main_w // 2) - (win_w // 2) + y = main_y + (main_h // 2) - (win_h // 2) + self.geometry(f"{win_w}x{win_h}+{x}+{y}") + + def _load_settings(self): + # --- Load Target Settings --- + target_cfg = self.connection_config.get('target', {}) + self.target_vars['conn_type'].set("Serial" if target_cfg.get('type', 'TFTP').lower() == "serial" else "TFTP") + self.target_vars['tftp_ip'].set(target_cfg.get('tftp', {}).get('ip', '127.0.0.1')) + self.target_vars['tftp_port'].set(target_cfg.get('tftp', {}).get('port', 69)) + self.target_vars['serial_port'].set(target_cfg.get('serial', {}).get('port', 'COM1')) + self.target_vars['serial_baud'].set(target_cfg.get('serial', {}).get('baudrate', 9600)) + self._switch_view(self.target_vars) + + # --- Load LRU Settings --- + lru_cfg = self.connection_config.get('lru', {}) + self.lru_vars['conn_type'].set("Serial" if lru_cfg.get('type', 'TFTP').lower() == "serial" else "TFTP") + self.lru_vars['tftp_ip'].set(lru_cfg.get('tftp', {}).get('ip', '127.0.0.1')) + self.lru_vars['tftp_port'].set(lru_cfg.get('tftp', {}).get('port', 69)) + self.lru_vars['serial_port'].set(lru_cfg.get('serial', {}).get('port', 'COM1')) + self.lru_vars['serial_baud'].set(lru_cfg.get('serial', {}).get('baudrate', 9600)) + self._switch_view(self.lru_vars) + + def _create_widgets(self): + main_frame = ttk.Frame(self, padding="10") + main_frame.pack(fill=tk.BOTH, expand=True) + + # --- Target Connection Frame --- + target_frame = ttk.LabelFrame(main_frame, text="Target Connection") + target_frame.pack(fill=tk.X, expand=True, pady=5, padx=5) + self.target_vars = self._create_connection_panel(target_frame, "Target") + + # --- LRU Connection Frame --- + lru_frame = ttk.LabelFrame(main_frame, text="LRU Connection") + lru_frame.pack(fill=tk.X, expand=True, pady=5, padx=5) + self.lru_vars = self._create_connection_panel(lru_frame, "LRU") + + # --- Buttons --- + button_frame = ttk.Frame(main_frame) + button_frame.pack(pady=10, fill=tk.X, side=tk.BOTTOM) + + ttk.Button(button_frame, text="Save", command=self._on_save).pack(side=tk.RIGHT, padx=5) + ttk.Button(button_frame, text="Cancel", command=self._on_cancel).pack(side=tk.RIGHT, padx=5) + + def _create_connection_panel(self, parent_frame, name): + vars = {} + + # --- Connection Type Selection --- + conn_type_frame = ttk.Frame(parent_frame) + conn_type_frame.pack(fill=tk.X, padx=5, pady=(5,10)) + + vars['conn_type'] = tk.StringVar() + + ttk.Label(conn_type_frame, text="Type:").pack(side=tk.LEFT) + ttk.Radiobutton(conn_type_frame, text="TFTP", variable=vars['conn_type'], value="TFTP", command=lambda: self._switch_view(vars)).pack(side=tk.LEFT, padx=5) + ttk.Radiobutton(conn_type_frame, text="Serial", variable=vars['conn_type'], value="Serial", command=lambda: self._switch_view(vars)).pack(side=tk.LEFT, padx=5) + + # --- Configuration Frames (TFTP and Serial) --- + vars['tftp_frame'] = ttk.Frame(parent_frame, padding=5) + vars['serial_frame'] = ttk.Frame(parent_frame, padding=5) + + # --- TFTP Widgets --- + tftp_grid = vars['tftp_frame'] + ttk.Label(tftp_grid, text="IP Address:").grid(row=0, column=0, sticky=tk.W, pady=2) + vars['tftp_ip'] = tk.StringVar() + ttk.Entry(tftp_grid, textvariable=vars['tftp_ip']).grid(row=0, column=1, sticky=tk.EW, padx=5) + + ttk.Label(tftp_grid, text="Port:").grid(row=1, column=0, sticky=tk.W, pady=2) + vars['tftp_port'] = tk.IntVar() + ttk.Spinbox(tftp_grid, from_=1, to=65535, textvariable=vars['tftp_port'], width=7).grid(row=1, column=1, sticky=tk.W, padx=5) + tftp_grid.columnconfigure(1, weight=1) + + # --- Serial Widgets --- + serial_grid = vars['serial_frame'] + ttk.Label(serial_grid, text="COM Port:").grid(row=0, column=0, sticky=tk.W, pady=2) + vars['serial_port'] = tk.StringVar() + # In a real app, you might want to auto-detect available ports + ttk.Combobox(serial_grid, textvariable=vars['serial_port'], values=[f"COM{i}" for i in range(1, 17)]).grid(row=0, column=1, sticky=tk.EW, padx=5) + + ttk.Label(serial_grid, text="Baud Rate:").grid(row=1, column=0, sticky=tk.W, pady=2) + vars['serial_baud'] = tk.IntVar() + baud_rates = [9600, 19200, 38400, 57600, 115200] + ttk.Combobox(serial_grid, textvariable=vars['serial_baud'], values=baud_rates, width=7).grid(row=1, column=1, sticky=tk.W, padx=5) + serial_grid.columnconfigure(1, weight=1) + + # --- Test Button --- + test_button = ttk.Button(parent_frame, text="Test Connection", command=lambda: self._test_connection(name, vars)) + test_button.pack(pady=5, padx=5, anchor=tk.E) + + # Initial view setup is now done in _load_settings + # self._switch_view(vars) + + return vars + + def _switch_view(self, vars): + conn_type = vars['conn_type'].get() + if conn_type == "TFTP": + vars['serial_frame'].pack_forget() + vars['tftp_frame'].pack(fill=tk.X, expand=True, padx=5, pady=5) + else: # Serial + vars['tftp_frame'].pack_forget() + vars['serial_frame'].pack(fill=tk.X, expand=True, padx=5, pady=5) + + def _test_connection(self, name, vars): + conn_type = vars['conn_type'].get() + result = False + error = None + if conn_type == "TFTP": + ip = vars['tftp_ip'].get() + port = vars['tftp_port'].get() + try: + port = int(port) + from core.tftp_communicator import TFTPCommunicator + comm = TFTPCommunicator() + result = comm.connect({'ip': ip, 'port': port}) + except Exception as e: + error = str(e) + else: # Serial + serial_port = vars['serial_port'].get() + baudrate = vars['serial_baud'].get() + try: + baudrate = int(baudrate) + from core.serial_communicator import SerialCommunicator + comm = SerialCommunicator() + result = comm.connect({'port': serial_port, 'baudrate': baudrate}) + except Exception as e: + error = str(e) + if result: + messagebox.showinfo("Connection Test", f"{name} connection successful!", parent=self) + else: + msg = f"{name} connection failed." + if error: + msg += f"\nError: {error}" + messagebox.showerror("Connection Test", msg, parent=self) + + def _on_save(self): + # Save Target and LRU connection configs in a consistent structure + new_config = { + 'target': { + 'type': self.target_vars['conn_type'].get().lower(), + 'tftp': { + 'ip': self.target_vars['tftp_ip'].get(), + 'port': self.target_vars['tftp_port'].get() + }, + 'serial': { + 'port': self.target_vars['serial_port'].get(), + 'baudrate': self.target_vars['serial_baud'].get() + } + }, + 'lru': { + 'type': self.lru_vars['conn_type'].get().lower(), + 'tftp': { + 'ip': self.lru_vars['tftp_ip'].get(), + 'port': self.lru_vars['tftp_port'].get() + }, + 'serial': { + 'port': self.lru_vars['serial_port'].get(), + 'baudrate': self.lru_vars['serial_baud'].get() + } + } + } + self.master_view.update_connection_settings(new_config) + print("Settings saved and applied.") + self.destroy() + + def _on_cancel(self): + self.destroy() + +if __name__ == '__main__': + # Example usage for testing + root = tk.Tk() + root.withdraw() # Hide the root window + app = ConnectionSettingsWindow(root) + root.wait_window(app) + root.destroy() diff --git a/target_simulator/gui/main_view.py b/target_simulator/gui/main_view.py index fb297ab..e0ca489 100644 --- a/target_simulator/gui/main_view.py +++ b/target_simulator/gui/main_view.py @@ -9,6 +9,7 @@ from typing import Optional, Dict, Any from .ppi_display import PPIDisplay from .settings_window import SettingsWindow +from .connection_settings_window import ConnectionSettingsWindow from .radar_config_window import RadarConfigWindow from .scenario_controls_frame import ScenarioControlsFrame from .target_list_frame import TargetListFrame @@ -23,6 +24,44 @@ from target_simulator.utils.logger import get_logger, shutdown_logging_system from target_simulator.utils.config_manager import ConfigManager class MainView(tk.Tk): + def _on_new_scenario(self, scenario_name): + """ + Handles creation of a new scenario: + - If scenario name exists, notify user and load it. + - If not, create new scenario, update combobox, select, and clear targets. + """ + 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 saved data.", + parent=self + ) + self._on_load_scenario(scenario_name) + # Select in comboboxes + self.scenario_controls.update_scenario_list(scenario_names, scenario_name) + if hasattr(self, 'sim_scenario_combobox'): + self.sim_scenario_combobox['values'] = scenario_names + self.sim_scenario_combobox.set(scenario_name) + return + # Create new scenario + self.scenario = Scenario(name=scenario_name) + self.current_scenario_name = scenario_name + # Clear targets in UI + self.target_list.update_target_list([]) + # Update comboboxes + scenario_names.append(scenario_name) + self.scenario_controls.update_scenario_list(scenario_names, scenario_name) + if hasattr(self, 'sim_scenario_combobox'): + self.sim_scenario_combobox['values'] = scenario_names + self.sim_scenario_combobox.set(scenario_name) + self._update_window_title() + self.logger.info(f"Created new scenario '{scenario_name}'. Ready for target input.") + messagebox.showinfo( + "New Scenario", + f"Scenario '{scenario_name}' created. Add targets to begin.", + parent=self + ) """The main application window.""" def __init__(self): super().__init__() @@ -33,82 +72,154 @@ class MainView(tk.Tk): 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.geometry(settings.get('geometry', '1200x1024')) + self.minsize(1024, 1024) + self.target_communicator = None + self.lru_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) + + # --- 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) - # --- 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"]) + # Add Connect button next to PPI controls + # Find the controls_frame inside PPIDisplay and add the button + 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) - # --- 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 --- + # --- 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) + # --- Left Pane --- + left_pane_container = ttk.Frame(self.h_pane) + self.h_pane.add(left_pane_container, weight=1) + + # --- Notebook for left pane --- + left_notebook = ttk.Notebook(left_pane_container) + left_notebook.pack(fill=tk.BOTH, expand=True) + + # --- TAB 2: SCENARIO CONFIG --- + if not hasattr(self, 'scenario_tab'): + self.scenario_tab = ttk.Frame(left_notebook) + left_notebook.add(self.scenario_tab, text="Scenario Config") + self.scenario_controls = ScenarioControlsFrame(self.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(self.scenario_tab, targets_changed_callback=self._on_targets_changed) + self.target_list.pack(fill=tk.BOTH, expand=True, padx=5) + + # --- TAB 3: SIMULATION --- + if not hasattr(self, 'simulation_tab'): + self.simulation_tab = ttk.Frame(left_notebook) + left_notebook.add(self.simulation_tab, text="Simulation") + sim_controls_frame = ttk.LabelFrame(self.simulation_tab, text="Simulation Controls") + sim_controls_frame.pack(fill=tk.X, padx=5, pady=5, anchor='n') + ttk.Label(sim_controls_frame, text="Scenario:").pack(side=tk.LEFT, padx=(5, 5), pady=5) + self.sim_scenario_combobox = ttk.Combobox( + sim_controls_frame, + textvariable=self.scenario_controls.current_scenario, + state="readonly" + ) + self.sim_scenario_combobox.pack(side=tk.LEFT, expand=True, fill=tk.X, pady=5) + self.sim_scenario_combobox.bind("<>", lambda event: self._on_load_scenario(self.sim_scenario_combobox.get())) + send_button = ttk.Button(sim_controls_frame, text="Send to Radar", command=self._on_send_scenario) + send_button.pack(side=tk.LEFT, padx=5, pady=5) + + # --- TAB 4: LRU SIMULATION --- + if not hasattr(self, 'lru_tab'): + self.lru_tab = ttk.Frame(left_notebook) + left_notebook.add(self.lru_tab, text="LRU Simulation") + cooling_frame = ttk.LabelFrame(self.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(self.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(self.lru_tab) + lru_action_frame.pack(fill=tk.X, padx=5, pady=10, anchor='n') + self.lru_message_var = tk.StringVar() + ttk.Entry(lru_action_frame, textvariable=self.lru_message_var, state="readonly").pack(side=tk.LEFT, expand=True, fill=tk.X, padx=(0, 5)) + send_lru_button = ttk.Button(lru_action_frame, text="Send LRU Status", command=self._on_send_lru_status) + send_lru_button.pack(side=tk.LEFT) + + # Carica gli scenari solo dopo aver creato scenario_controls + self._load_scenarios_into_ui() + + # Rimuovi la voce di menu per la configurazione + 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 _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") # Default to red + + 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") # Default to red + + 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 _on_connect_button(self): + try: + self.logger.info("Connection requested by user.") + self._initialize_communicator() + except Exception as e: + self.logger.error(f"Error during connection: {e}") + self._log_message(f"Error during connection: {e}") + + def _log_message(self, message): + self.log_text_widget.configure(state=tk.NORMAL) + self.log_text_widget.insert(tk.END, message + "\n") + self.log_text_widget.configure(state=tk.DISABLED) + self.log_text_widget.see(tk.END) + def _draw_status_indicator(self, canvas, color): + canvas.delete("all") + canvas.create_oval(2, 2, 14, 14, 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) @@ -118,35 +229,89 @@ class MainView(tk.Tk): 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 + """Creates and connects the appropriate communicators based on config.""" + # Disconnect if already connected + 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() + + + # --- Initialize Target Communicator --- + # --- Initialize Target Communicator --- + target_cfg = self.connection_config.get("target", {}) + target_comm_type = target_cfg.get("type") + self.logger.info(f"Initializing Target communicator of type: {target_comm_type}") + self.target_communicator = None + config_data = None + if target_comm_type == "serial": + config_data = target_cfg.get("serial", {}) + port = config_data.get("port") + baudrate = config_data.get("baudrate") + if isinstance(port, str) and port and isinstance(baudrate, int): + self.target_communicator = SerialCommunicator() + if self.target_communicator.connect(config_data): + self._draw_status_indicator(self.target_status_canvas, "#2ecc40") # green + self.status_var.set("Target: Connected") + else: + self._draw_status_indicator(self.target_status_canvas, "#e74c3c") # red + self.status_var.set("Target: Disconnected") + else: + self.logger.error(f"Skipped Target serial connection: invalid config {config_data}") + self._draw_status_indicator(self.target_status_canvas, "#e74c3c") + self.status_var.set("Target: Invalid config") + elif target_comm_type == "tftp": + config_data = target_cfg.get("tftp", {}) + ip = config_data.get("ip") + port = config_data.get("port") + if isinstance(ip, str) and ip and isinstance(port, int): + self.target_communicator = TFTPCommunicator() + if self.target_communicator.connect(config_data): + self._draw_status_indicator(self.target_status_canvas, "#2ecc40") # green + self.status_var.set("Target: Connected") + else: + self._draw_status_indicator(self.target_status_canvas, "#e74c3c") # red + self.status_var.set("Target: Disconnected") + else: + self.logger.error(f"Skipped Target TFTP connection: invalid config {config_data}") + self._draw_status_indicator(self.target_status_canvas, "#e74c3c") + self.status_var.set("Target: Invalid config") + + # --- Initialize LRU Communicator --- + lru_cfg = self.connection_config.get("lru", {}) + lru_comm_type = lru_cfg.get("type") + self.logger.info(f"Initializing LRU communicator of type: {lru_comm_type}") + self.lru_communicator = None + lru_config_data = None + if lru_comm_type == "serial": + lru_config_data = lru_cfg.get("serial", {}) + port = lru_config_data.get("port") + baudrate = lru_config_data.get("baudrate") + if isinstance(port, str) and port and isinstance(baudrate, int): + self.lru_communicator = SerialCommunicator() + if self.lru_communicator.connect(lru_config_data): + self._draw_status_indicator(self.lru_status_canvas, "#2ecc40") # green + else: + self._draw_status_indicator(self.lru_status_canvas, "#e74c3c") # red + else: + self.logger.error(f"Skipped LRU serial connection: invalid config {lru_config_data}") + self._draw_status_indicator(self.lru_status_canvas, "#e74c3c") + elif lru_comm_type == "tftp": + lru_config_data = lru_cfg.get("tftp", {}) + ip = lru_config_data.get("ip") + port = lru_config_data.get("port") + if isinstance(ip, str) and ip and isinstance(port, int): + self.lru_communicator = TFTPCommunicator() + if self.lru_communicator.connect(lru_config_data): + self._draw_status_indicator(self.lru_status_canvas, "#2ecc40") # green + else: + self._draw_status_indicator(self.lru_status_canvas, "#e74c3c") # red + else: + self.logger.error(f"Skipped LRU TFTP connection: invalid config {lru_config_data}") + self._draw_status_indicator(self.lru_status_canvas, "#e74c3c") - 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.""" @@ -154,52 +319,40 @@ class MainView(tk.Tk): 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.") + ConnectionSettingsWindow(self, self.config_manager, self.connection_config) def _on_send_scenario(self): - if self.communicator and self.communicator.is_open: + if self.target_communicator and self.target_communicator.is_open: if self.scenario and self.scenario.get_all_targets(): - self.communicator.send_scenario(self.scenario) + self.target_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) + messagebox.showerror("Not Connected", "Target communicator is not connected. Please check settings.", parent=self) + + def _on_send_lru_status(self): + cooling_status = self.cooling_status_var.get() + power_status = self.power_status_var.get() + + # Assume a simple CSV-like format for the message + message = f"LRU_STATUS,COOLING={cooling_status},POWER={power_status}" + self.lru_message_var.set(message) + self.logger.info(f"Formatted LRU status message: {message}") + + if self.lru_communicator and self.lru_communicator.is_open: + if hasattr(self.lru_communicator, 'send_raw_message'): + self.lru_communicator.send_raw_message(message) + self.logger.info("Sent LRU status message to radar.") + messagebox.showinfo("LRU Status Sent", f"Message sent:\n{message}", parent=self) + else: + self.logger.warning("Communicator does not have a 'send_raw_message' method.") + messagebox.showwarning("Not Supported", "The current communicator does not support sending raw messages.", parent=self) + else: + self.logger.warning("Attempted to send LRU status while disconnected.") + messagebox.showerror("Not Connected", "LRU communicator is not connected. Please check settings.", parent=self) # --- Other methods remain largely the same --- @@ -219,6 +372,9 @@ class MainView(tk.Tk): 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) + # Also update the values for the simulation tab combobox + if hasattr(self, 'sim_scenario_combobox'): + self.sim_scenario_combobox['values'] = scenario_names # Rimosso: la logica di aggiunta target รจ ora nel TargetListFrame @@ -245,12 +401,19 @@ class MainView(tk.Tk): 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}") + def _on_save_scenario(self, scenario_name: str): + """ + Save the current scenario and its targets (from the target list) under the selected name. + """ + self.logger.info(f"Saving scenario: {scenario_name}") + # Update scenario's targets from the target list + self.scenario.targets = {t.target_id: t for t in self.target_list.get_targets()} 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) + 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_delete_scenario(self, scenario_name: str): self.logger.info(f"Deleting scenario: {scenario_name}") @@ -288,7 +451,10 @@ class MainView(tk.Tk): 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() + 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() \ No newline at end of file + self.destroy() diff --git a/target_simulator/gui/scenario_controls_frame.py b/target_simulator/gui/scenario_controls_frame.py index 573edd3..8dd0192 100644 --- a/target_simulator/gui/scenario_controls_frame.py +++ b/target_simulator/gui/scenario_controls_frame.py @@ -13,17 +13,19 @@ class ScenarioControlsFrame(ttk.LabelFrame): def __init__(self, master, *, + main_view, load_scenario_command: Callable[[str], None], save_as_command: Callable[[str], None], - update_command: Callable[[str], None], delete_command: Callable[[str], None], + new_scenario_command: Callable[[str], None], send_scenario_command: Callable[[], None] = None): super().__init__(master, text="Scenario Controls") + self.main_view = main_view self._load_scenario_command = load_scenario_command self._save_as_command = save_as_command - self._update_command = update_command self._delete_command = delete_command + self._new_scenario_command = new_scenario_command self._send_scenario_command = send_scenario_command self.current_scenario = tk.StringVar() @@ -48,27 +50,52 @@ class ScenarioControlsFrame(ttk.LabelFrame): send_btn = ttk.Button(top_frame, text="Send Scenario", command=self._send_scenario_command) send_btn.pack(side=tk.LEFT, padx=5) + self.new_button = ttk.Button( + top_frame, text="New...", command=self._on_new + ) + self.new_button.pack(side=tk.LEFT, padx=(10, 5)) + + self.save_button = ttk.Button( + top_frame, text="Save", command=self._on_save + ) + self.save_button.pack(side=tk.LEFT, padx=5) + self.save_as_button = ttk.Button( top_frame, text="Save As...", command=self._on_save_as ) - self.save_as_button.pack(side=tk.LEFT, padx=(10, 5)) + self.save_as_button.pack(side=tk.LEFT, padx=5) - self.update_button = ttk.Button( - top_frame, text="Update", command=self._on_update - ) - self.update_button.pack(side=tk.LEFT, padx=5) self.delete_button = ttk.Button( top_frame, text="Delete", command=self._on_delete ) self.delete_button.pack(side=tk.LEFT, padx=5) + def _on_save(self): + """Handle the 'Save' button click: overwrite the selected scenario.""" + scenario_name = self.current_scenario.get() + if not scenario_name: + messagebox.showwarning("No Scenario Selected", "Please select a scenario to save.", parent=self) + return + # Call the new save method in MainView + self.main_view._on_save_scenario(scenario_name) + def _on_scenario_select(self, event): """Callback for when a scenario is selected from the combobox.""" scenario_name = self.current_scenario.get() if scenario_name: self._load_scenario_command(scenario_name) + def _on_new(self): + """Handle the 'New' button click.""" + scenario_name = simpledialog.askstring( + "New Scenario", + "Enter a name for the new scenario:", + parent=self + ) + if scenario_name: + self._new_scenario_command(scenario_name) + def _on_save_as(self): """Handle the 'Save As' button click.""" scenario_name = simpledialog.askstring( @@ -79,13 +106,6 @@ class ScenarioControlsFrame(ttk.LabelFrame): if scenario_name: self._save_as_command(scenario_name) - def _on_update(self): - """Handle the 'Update' button click.""" - scenario_name = self.current_scenario.get() - if not scenario_name: - messagebox.showwarning("No Scenario Selected", "Please select a scenario to update.", parent=self) - return - self._update_command(scenario_name) def _on_delete(self): """Handle the 'Delete' button click.""" diff --git a/target_simulator/gui/target_list_frame.py b/target_simulator/gui/target_list_frame.py index e5d762f..1d73226 100644 --- a/target_simulator/gui/target_list_frame.py +++ b/target_simulator/gui/target_list_frame.py @@ -51,10 +51,8 @@ class TargetListFrame(ttk.LabelFrame): # --- Buttons --- - button_frame = ttk.Frame(self, relief=tk.RAISED, borderwidth=1) - button_frame.grid(row=2, column=0, columnspan=2, pady=10, padx=10, sticky="ew") - self.rowconfigure(2, weight=0) - self.columnconfigure(0, weight=1) + button_frame = ttk.Frame(self) + button_frame.grid(row=1, column=0, columnspan=2, pady=5) self.add_button = ttk.Button(button_frame, text="Add", command=self._on_add_click) self.add_button.pack(side=tk.LEFT, padx=5) diff --git a/target_simulator/utils/config_manager.py b/target_simulator/utils/config_manager.py index 7ae92c0..ce7a549 100644 --- a/target_simulator/utils/config_manager.py +++ b/target_simulator/utils/config_manager.py @@ -12,20 +12,11 @@ from typing import Dict, Any, Optional, List class ConfigManager: def save_connection_settings(self, config: Dict[str, Any]): """ - Updates only the selected connection type settings, preserving the other type. + Save both target and lru connection configs in the settings file. """ if "general" not in self._settings: self._settings["general"] = {} - conn = self._settings["general"].get("connection", {}) - # Aggiorna solo la sezione del tipo selezionato - typ = config.get("type") - if typ == "serial": - conn["type"] = "serial" - conn["serial"] = config.get("serial", {}) - elif typ == "tftp": - conn["type"] = "tftp" - conn["tftp"] = config.get("tftp", {}) - self._settings["general"]["connection"] = conn + self._settings["general"]["connection"] = config self._save_settings() def get_connection_settings(self) -> Dict[str, Any]: """