From 19547f8764b0a627e138d31e49b0e5fbf9cd86b3 Mon Sep 17 00:00:00 2001 From: VALLONGOL Date: Thu, 9 Oct 2025 20:37:32 +0200 Subject: [PATCH] add multiplier for simulation time in editor and main window --- settings.json | 19 ++++++++++++------ target_simulator/core/simulation_engine.py | 9 ++++++++- target_simulator/gui/main_view.py | 20 +++++++++++++++++++ .../gui/trajectory_editor_window.py | 19 ++++++++++++++++++ 4 files changed, 60 insertions(+), 7 deletions(-) diff --git a/settings.json b/settings.json index dce1c27..c4d77d7 100644 --- a/settings.json +++ b/settings.json @@ -40,24 +40,31 @@ "trajectory": [ { "maneuver_type": "Fly to Point", - "duration_s": 10.0, + "duration_s": 1.0, "target_altitude_ft": 10000.0, - "target_range_nm": 20.0, + "target_range_nm": 10.0, "target_azimuth_deg": 0.0 }, { "maneuver_type": "Fly for Duration", - "target_velocity_fps": 506.343, + "target_velocity_fps": 1687.81, "target_heading_deg": 10.0, - "duration_s": 10.0, + "duration_s": 30.0, "target_altitude_ft": 10000.0 }, { "maneuver_type": "Fly for Duration", "target_velocity_fps": 506.343, - "target_heading_deg": 90.0, - "duration_s": 10.0, + "target_heading_deg": -90.0, + "duration_s": 30.0, "target_altitude_ft": 10000.0 + }, + { + "maneuver_type": "Fly to Point", + "duration_s": 30.0, + "target_altitude_ft": 10000.0, + "target_range_nm": 40.0, + "target_azimuth_deg": 0.0 } ] } diff --git a/target_simulator/core/simulation_engine.py b/target_simulator/core/simulation_engine.py index 9f17c25..5c87e12 100644 --- a/target_simulator/core/simulation_engine.py +++ b/target_simulator/core/simulation_engine.py @@ -31,6 +31,7 @@ class SimulationEngine(threading.Thread): self.communicator = communicator self.update_queue = update_queue + self.time_multiplier = 1.0 self.scenario: Optional[Scenario] = None self._stop_event = threading.Event() @@ -43,6 +44,11 @@ class SimulationEngine(threading.Thread): self.scenario = scenario self.scenario.reset_simulation() + def set_time_multiplier(self, multiplier: float): + """Sets the simulation time speed multiplier.""" + self.logger.info(f"Setting simulation time multiplier to {multiplier}x") + self.time_multiplier = multiplier + def run(self): """The main loop of the simulation thread.""" self.logger.info("Simulation engine thread started.") @@ -58,9 +64,10 @@ class SimulationEngine(threading.Thread): current_time = time.monotonic() delta_time = current_time - self._last_tick_time self._last_tick_time = current_time + simulated_delta_time = delta_time * self.time_multiplier # --- Simulation Step (always runs) --- - self.scenario.update_state(delta_time) + self.scenario.update_state(simulated_delta_time) updated_targets = self.scenario.get_all_targets() diff --git a/target_simulator/gui/main_view.py b/target_simulator/gui/main_view.py index aea79db..eb002ef 100644 --- a/target_simulator/gui/main_view.py +++ b/target_simulator/gui/main_view.py @@ -51,6 +51,7 @@ class MainView(tk.Tk): self.simulation_engine: Optional[SimulationEngine] = None self.gui_update_queue = Queue() self.is_simulation_running = tk.BooleanVar(value=False) + self.time_multiplier = 1.0 # --- Window and UI Setup --- self.title("Radar Target Simulator") @@ -140,6 +141,12 @@ class MainView(tk.Tk): 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) + ttk.Label(engine_frame, text="Speed:").pack(side=tk.LEFT, padx=(10, 2), pady=5) + self.time_multiplier_var = tk.StringVar(value="1x") + self.multiplier_combo = ttk.Combobox(engine_frame, textvariable=self.time_multiplier_var, values=["1x", "2x", "4x", "10x"], state="readonly", width=4) + self.multiplier_combo.pack(side=tk.LEFT, padx=(0, 5), pady=5) + self.multiplier_combo.bind("<>", self._on_time_multiplier_changed) + # --- TAB 3: LRU SIMULATION --- lru_tab = ttk.Frame(left_notebook) left_notebook.add(lru_tab, text="LRU Simulation") @@ -276,6 +283,7 @@ class MainView(tk.Tk): self.scenario.reset_simulation() self.simulation_engine = SimulationEngine(self.target_communicator, self.gui_update_queue) + self.simulation_engine.set_time_multiplier(self.time_multiplier) self.simulation_engine.load_scenario(self.scenario) self.simulation_engine.start() @@ -314,6 +322,7 @@ class MainView(tk.Tk): 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.multiplier_combo.config(state="readonly" if not is_running else tk.DISABLED) self.scenario_controls.new_button.config(state=state) self.scenario_controls.save_button.config(state=state) @@ -326,6 +335,17 @@ class MainView(tk.Tk): self.target_list.edit_button.config(state=state) self.target_list.tree.config(selectmode="browse" if not is_running else "none") + def _on_time_multiplier_changed(self, event=None): + """Handles changes to the time multiplier selection.""" + try: + multiplier_str = self.time_multiplier_var.get().replace('x', '') + self.time_multiplier = float(multiplier_str) + if self.simulation_engine and self.simulation_engine.is_running(): + self.simulation_engine.set_time_multiplier(self.time_multiplier) + except ValueError: + self.logger.error(f"Invalid time multiplier value: {self.time_multiplier_var.get()}") + self.time_multiplier = 1.0 + def _on_targets_changed(self, targets: List[Target]): """Callback executed when the target list is modified by the user.""" # 1. Update the internal scenario object diff --git a/target_simulator/gui/trajectory_editor_window.py b/target_simulator/gui/trajectory_editor_window.py index e370295..4002f46 100644 --- a/target_simulator/gui/trajectory_editor_window.py +++ b/target_simulator/gui/trajectory_editor_window.py @@ -31,6 +31,7 @@ class TrajectoryEditorWindow(tk.Toplevel): self.existing_ids = existing_ids self.result_target: Optional[Target] = None self.initial_max_range = max_range_nm + self.time_multiplier = 1.0 self.waypoints: List[Waypoint] = [] is_editing = target_to_edit is not None @@ -89,6 +90,12 @@ class TrajectoryEditorWindow(tk.Toplevel): self.reset_button = ttk.Button(preview_controls, text="⟲ Reset", command=self._on_preview_reset) self.reset_button.pack(side=tk.LEFT, padx=5) + ttk.Label(preview_controls, text="Speed:").pack(side=tk.LEFT, padx=(10, 2), pady=5) + self.time_multiplier_var = tk.StringVar(value="1x") + self.multiplier_combo = ttk.Combobox(preview_controls, textvariable=self.time_multiplier_var, values=["1x", "2x", "4x", "10x"], state="readonly", width=4) + self.multiplier_combo.pack(side=tk.LEFT, padx=(0, 5), pady=5) + self.multiplier_combo.bind("<>", self._on_time_multiplier_changed) + button_frame = ttk.Frame(self) button_frame.pack(fill=tk.X, padx=10, pady=(0, 10), side=tk.BOTTOM) ttk.Button(button_frame, text="Cancel", command=self._on_cancel).pack(side=tk.RIGHT) @@ -181,6 +188,16 @@ class TrajectoryEditorWindow(tk.Toplevel): # Passa l'intera lista di waypoint, il PPI ora sa come gestirla self.ppi_preview.draw_trajectory_preview(self.waypoints) + def _on_time_multiplier_changed(self, event=None): + """Handles changes to the time multiplier selection.""" + try: + multiplier_str = self.time_multiplier_var.get().replace('x', '') + self.time_multiplier = float(multiplier_str) + if self.preview_engine and self.preview_engine.is_running(): + self.preview_engine.set_time_multiplier(self.time_multiplier) + except ValueError: + self.time_multiplier = 1.0 + def _on_preview_play(self): if self.is_preview_running.get(): return if not self.waypoints or self.waypoints[0].maneuver_type != ManeuverType.FLY_TO_POINT: @@ -194,6 +211,7 @@ class TrajectoryEditorWindow(tk.Toplevel): preview_scenario = Scenario(); preview_scenario.add_target(preview_target) self.preview_engine = SimulationEngine(communicator=None, update_queue=self.gui_update_queue) + self.preview_engine.set_time_multiplier(self.time_multiplier) self.preview_engine.load_scenario(preview_scenario) self.preview_engine.start() @@ -226,6 +244,7 @@ class TrajectoryEditorWindow(tk.Toplevel): self.play_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.multiplier_combo.config(state="readonly" if not is_running else tk.DISABLED) self.wp_tree.config(selectmode="browse" if not is_running else "none") def _on_ok(self):