diff --git a/target_simulator/analysis/simulation_state_hub.py b/target_simulator/analysis/simulation_state_hub.py index 94306bd..53c930f 100644 --- a/target_simulator/analysis/simulation_state_hub.py +++ b/target_simulator/analysis/simulation_state_hub.py @@ -169,6 +169,9 @@ class SimulationStateHub: """Clears all stored data for all targets.""" with self._lock: self._target_data.clear() + # also clear heading caches + self._latest_real_heading.clear() + self._latest_raw_heading.clear() def _initialize_target(self, target_id: int): """Internal helper to create the data structure for a new target.""" @@ -176,4 +179,18 @@ class SimulationStateHub: self._target_data[target_id] = { "simulated": collections.deque(maxlen=self._history_size), "real": collections.deque(maxlen=self._history_size), - } \ No newline at end of file + } + + def remove_target(self, target_id: int): + """Remove all stored data for a specific target id.""" + with self._lock: + try: + tid = int(target_id) + if tid in self._target_data: + del self._target_data[tid] + if tid in self._latest_real_heading: + del self._latest_real_heading[tid] + if tid in self._latest_raw_heading: + del self._latest_raw_heading[tid] + except Exception: + pass \ No newline at end of file diff --git a/target_simulator/gui/payload_router.py b/target_simulator/gui/payload_router.py index 9c9be0f..6af2341 100644 --- a/target_simulator/gui/payload_router.py +++ b/target_simulator/gui/payload_router.py @@ -137,6 +137,19 @@ class DebugPayloadRouter: try: parsed_for_hub = SfpRisStatusPayload.from_buffer_copy(payload) ts_s = parsed_for_hub.scenario.timetag / 1000.0 + + # First: remove any targets that the server marked as inactive (flags == 0) + try: + for i, ris_t in enumerate(parsed_for_hub.tgt.tgt): + try: + if ris_t.flags == 0 and self._hub and hasattr(self._hub, 'remove_target'): + self._hub.remove_target(i) + except Exception: + pass + except Exception: + pass + + # Add real states for active targets for target in real_targets: state_tuple = ( getattr(target, '_pos_x_ft', 0.0), @@ -146,17 +159,19 @@ class DebugPayloadRouter: self._hub.add_real_state( target_id=target.target_id, timestamp=ts_s, state=state_tuple ) + # Propagate heading information (if available) into the hub so # GUI builders that reconstruct lightweight Target objects # from the hub can also pick up the last known heading. - try: - for target in real_targets: - if hasattr(self._hub, 'set_real_heading'): - raw_val = getattr(target, '_raw_heading', None) - self._hub.set_real_heading(target.target_id, getattr(target, 'current_heading_deg', 0.0), raw_value=raw_val) - except Exception: - # Never allow heading propagation to break payload handling - self._logger.debug("Failed to propagate heading to hub", exc_info=True) + try: + for target in real_targets: + if hasattr(self._hub, 'set_real_heading'): + raw_val = getattr(target, '_raw_heading', None) + self._hub.set_real_heading(target.target_id, getattr(target, 'current_heading_deg', 0.0), raw_value=raw_val) + except Exception: + # Never allow heading propagation to break payload handling + self._logger.debug("Failed to propagate heading to hub", exc_info=True) + if self._update_queue: try: self._update_queue.put_nowait([]) diff --git a/target_simulator/gui/sfp_debug_window.py b/target_simulator/gui/sfp_debug_window.py index c4dbece..a282221 100644 --- a/target_simulator/gui/sfp_debug_window.py +++ b/target_simulator/gui/sfp_debug_window.py @@ -11,6 +11,7 @@ import datetime import os import ctypes import math +import socket from queue import Queue, Empty from typing import Dict, Optional, Any, List @@ -27,7 +28,7 @@ from target_simulator.core.sfp_structures import ImageLeaderData, SFPHeader from target_simulator.gui.payload_router import DebugPayloadRouter from target_simulator.core.models import Target, Waypoint, ManeuverType, KNOTS_TO_FPS from target_simulator.core import command_builder -from target_simulator.gui.ppi_display import PPIDisplay + DEF_TEST_ID = 1 DEF_TEST_RANGE = 30.0 @@ -48,7 +49,7 @@ class SfpDebugWindow(tk.Toplevel): self.geometry("1100x700") self.protocol("WM_DELETE_WINDOW", self._on_close) self.logger = logging.getLogger(__name__) - + self.debug_update_queue = Queue() self.shared_communicator = getattr(self.master, 'target_communicator', None) @@ -67,7 +68,6 @@ class SfpDebugWindow(tk.Toplevel): self.payload_router.add_ris_target_listener(self._queue_ris_target_update) self.image_area_size = 150 - self._ppi_visible = False self.ip_var = tk.StringVar(value="127.0.0.1") self.local_port_var = tk.StringVar(value="60002") self.server_port_var = tk.StringVar(value="60001") @@ -81,10 +81,21 @@ class SfpDebugWindow(tk.Toplevel): self.tgt_active_var = tk.BooleanVar(value=True) self.tgt_traceable_var = tk.BooleanVar(value=True) self.tgt_restart_var = tk.BooleanVar(value=False) - self._master_mode_names = [ "idle_master_mode", "int_bit_master_mode", "gm_master_mode", "dbs_master_mode", "rws_master_mode", "vs_master_mode", "acm_master_mode", "tws_master_mode", "sea_low_master_mode", "sea_high_master_mode", "gmti_master_mode", "bcn_master_mode", "sam_master_mode", "ta_master_mode", "wa_master_mode", "stt_master_mode", "dtt_master_mode", "sstt_master_mode", "acq_master_mode", "ftt_master_mode", "agr_master_mode", "sar_master_mode", "invalid_master_mode_", "xtst_dummy_mode", "xtst_hw_validation_mode", "boot_master_mode", "master_mode_id_cardinality_", ] - + # View settings for RIS/status presentation + self.scenario_view_mode = tk.StringVar(value='simplified') + self.simplified_decimals = tk.IntVar(value=2) + self._master_mode_names = [ + "idle_master_mode", "int_bit_master_mode", "gm_master_mode", "dbs_master_mode", + "rws_master_mode", "vs_master_mode", "acm_master_mode", "tws_master_mode", + "sea_low_master_mode", "sea_high_master_mode", "gmti_master_mode", "bcn_master_mode", + "sam_master_mode", "ta_master_mode", "wa_master_mode", "stt_master_mode", + "dtt_master_mode", "sstt_master_mode", "acq_master_mode", "ftt_master_mode", + "agr_master_mode", "sar_master_mode", "invalid_master_mode_", "xtst_dummy_mode", + "xtst_hw_validation_mode", "boot_master_mode", "master_mode_id_cardinality_", + ] + self._create_widgets() - + if self.shared_communicator: self._update_toggle_state(self.shared_communicator.is_open) self.after(self.GUI_POLL_INTERVAL_MS, self._process_gui_updates) @@ -99,20 +110,21 @@ class SfpDebugWindow(tk.Toplevel): self.logger.info("SFP Debug Window closing.") if self.shared_communicator: self.shared_communicator.remove_connection_state_callback(self._update_toggle_state) - + if self.payload_router: self.payload_router.remove_ris_target_listener(self._queue_ris_target_update) - + self.destroy() def _process_gui_updates(self): """ This method runs on the GUI thread and processes all queued updates. """ - # 1. Process target updates for the PPI + # 1. Process target updates (no PPI here; still support queued updates) try: while not self.debug_update_queue.empty(): real_targets = self.debug_update_queue.get_nowait() + # kept for compatibility; this window no longer shows a PPI self.update_ppi_targets(real_targets) except Empty: pass @@ -135,16 +147,19 @@ class SfpDebugWindow(tk.Toplevel): try: import json struct = json.loads(payload.decode("utf-8")) if isinstance(payload, (bytes, bytearray)) else payload - - # --- START OF NEW LOGIC --- view_mode = self.scenario_view_mode.get() decimals = self.simplified_decimals.get() - # Helper functions for conversion - def to_deg(rad): return rad * 180.0 / math.pi - def m_s_to_ft_s(ms): return ms * 3.28084 - def m_to_ft(m): return m * 3.28084 + def to_deg(rad): + return rad * 180.0 / math.pi + + def m_s_to_ft_s(ms): + return ms * 3.28084 + + def m_to_ft(m): + return m * 3.28084 + def decimal_deg_to_dms(deg, is_lat): d = abs(deg) degrees = int(d) @@ -176,8 +191,8 @@ class SfpDebugWindow(tk.Toplevel): elif field == 'mode' and value < len(self._master_mode_names): display_value = f"{value} ({self._master_mode_names[value].replace('_master_mode', '')})" elif isinstance(value, list): - display_value = str(value) # Keep arrays as string - + display_value = str(value) + self.scenario_tree.insert("", tk.END, values=(field, display_value)) # 2. Update ris_tree (all targets) @@ -186,13 +201,13 @@ class SfpDebugWindow(tk.Toplevel): for i, t in enumerate(targets): flags_val = t.get("flags", 0) flags_display = f"0x{flags_val:X}" - + if view_mode == 'simplified': heading = f"{to_deg(t.get('heading', 0.0)):.{decimals}f}°" x_pos = f"{m_to_ft(t.get('x', 0.0)):.{decimals}f} ft" y_pos = f"{m_to_ft(t.get('y', 0.0)):.{decimals}f} ft" z_pos = f"{m_to_ft(t.get('z', 0.0)):.{decimals}f} ft" - else: # Raw mode + else: heading = f"{t.get('heading', 0.0):.6f}" x_pos = f"{t.get('x', 0.0):.3f}" y_pos = f"{t.get('y', 0.0):.3f}" @@ -200,9 +215,7 @@ class SfpDebugWindow(tk.Toplevel): vals = (i, flags_display, heading, x_pos, y_pos, z_pos) self.ris_tree.insert("", tk.END, values=vals) - - # --- END OF NEW LOGIC --- - + except Exception: self.logger.exception("Failed to update RIS tables from JSON payload.") @@ -238,27 +251,9 @@ class SfpDebugWindow(tk.Toplevel): self.notebook.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) self._create_notebook_tabs() - def _toggle_ppi(self): - try: - if self._ppi_visible: - self.ris_ppi_container.pack_forget() - self.ris_table_container.pack(fill=tk.BOTH, expand=True) - self._ppi_visible = False - self.ppi_toggle_btn.config(text="Show PPI Map") - else: - self.ris_table_container.pack_forget() - self.ris_ppi_container.pack(fill=tk.BOTH, expand=True) - self._ppi_visible = True - self.ppi_toggle_btn.config(text="Hide PPI Map") - except Exception: - self.logger.exception("Toggle PPI failed") - def update_ppi_targets(self, targets: List[Target]): - try: - if self.ris_ppi_widget: - self.ris_ppi_widget.update_targets({"real": targets}) - except Exception: - self.logger.exception("Failed to update RIS PPI targets") + # PPI removed from SFP Debug Window; keep for compatibility but no-op + return def _create_connection_widgets(self, parent): ttk.Label(parent, text="IP:").pack(side=tk.LEFT, padx=(4, 2)) @@ -304,99 +299,125 @@ class SfpDebugWindow(tk.Toplevel): ttk.Button(quick_cmd_frame, text="pause", command=lambda: self._on_send_simple_command(command_builder.build_pause())).pack(side=tk.LEFT, padx=4) ttk.Button(quick_cmd_frame, text="continue", command=lambda: self._on_send_simple_command(command_builder.build_continue())).pack(side=tk.LEFT, padx=4) ttk.Button(quick_cmd_frame, text="tgtset (cur)", command=lambda: self._on_send_tgtset()).pack(side=tk.LEFT, padx=8) - self.ppi_toggle_btn = ttk.Button(quick_cmd_frame, text="Show PPI Map", command=self._toggle_ppi) - self.ppi_toggle_btn.pack(side=tk.RIGHT, padx=4) - - def _create_script_sender_widgets(self, parent): - ttk.Label(parent, text="Script to send:").pack(side=tk.LEFT, padx=(5, 2)) - ttk.Entry(parent, textvariable=self.script_var, width=60).pack(side=tk.LEFT, expand=True, fill=tk.X, padx=(0, 5)) - self.send_script_btn = ttk.Button(parent, text="Send script", command=self._on_send_script) - self.send_script_btn.pack(side=tk.LEFT, padx=5) + # PPI widget and toggle removed from this debug window def _create_notebook_tabs(self): - self.log_tab = scrolledtext.ScrolledText(self.notebook, state=tk.DISABLED, wrap=tk.WORD, font=("Consolas", 9)) - self.notebook.add(self.log_tab, text="Raw Log") - if _IMAGE_LIBS_AVAILABLE: - self.mfd_tab = self._create_image_tab("MFD Image") - self.notebook.add(self.mfd_tab["frame"], text="MFD Image") - self.sar_tab = self._create_image_tab("SAR Image") - self.notebook.add(self.sar_tab["frame"], text="SAR Image") + """Create the notebook tabs used by the SFP Debug Window. + + This method ensures the widgets referenced elsewhere (log_tab, raw_tab_text, + ris_tree, scenario_tree, history_tree, image tabs, bin/json views) exist. + """ + # Raw packet tab + raw_frame = ttk.Frame(self.notebook) + self.raw_tab_text = scrolledtext.ScrolledText(raw_frame, height=12, state=tk.DISABLED, wrap=tk.NONE, font=("Consolas", 9)) + self.raw_tab_text.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) + self.notebook.add(raw_frame, text="Raw") + + # Log tab + log_frame = ttk.Frame(self.notebook) + self.log_tab = scrolledtext.ScrolledText(log_frame, height=12, state=tk.DISABLED, wrap=tk.WORD, font=("Consolas", 9)) + self.log_tab.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) + self.notebook.add(log_frame, text="Log") + + # RIS tab - scenario and targets (side-by-side) ris_frame = ttk.Frame(self.notebook) + + # Controls above the paned area: view mode (raw/simplified) and decimals + controls = ttk.Frame(ris_frame) + controls.pack(side=tk.TOP, fill=tk.X, padx=5, pady=(4, 2)) + ttk.Label(controls, text="View:").pack(side=tk.LEFT, padx=(2, 4)) + self.scenario_view_mode_opt = ttk.OptionMenu(controls, self.scenario_view_mode, self.scenario_view_mode.get(), 'simplified', 'raw') + self.scenario_view_mode_opt.pack(side=tk.LEFT) + ttk.Label(controls, text="Decimals:").pack(side=tk.LEFT, padx=(12, 4)) + self.dec_spin = ttk.Spinbox(controls, from_=0, to=6, textvariable=self.simplified_decimals, width=4) + self.dec_spin.pack(side=tk.LEFT) + + # Use a paned window to place scenario (left) and targets (right) side-by-side paned = ttk.Panedwindow(ris_frame, orient=tk.HORIZONTAL) paned.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) + left = ttk.Frame(paned) + right = ttk.Frame(paned) + paned.add(left, weight=1) + paned.add(right, weight=3) + + # Scenario tree on the left with scrollbar self.scenario_tree = ttk.Treeview(left, columns=("field", "value"), show="headings", height=12) self.scenario_tree.heading("field", text="Field") self.scenario_tree.heading("value", text="Value") - self.scenario_tree.column("field", width=140, anchor="w") - self.scenario_tree.column("value", width=160, anchor="w") - self.scenario_tree.pack(fill=tk.BOTH, expand=True) - paned.add(left, weight=1) - right = ttk.Frame(paned) - self.ris_table_container = ttk.Frame(right) - self.ris_ppi_container = ttk.Frame(right) - cols = ("idx", "flags", "heading", "x", "y", "z") - self.ris_tree = ttk.Treeview(self.ris_table_container, columns=cols, show="headings", height=12) - for c, txt in zip(cols, ("#", "flags", "heading", "x", "y", "z")): - self.ris_tree.heading(c, text=txt) - self.ris_tree.column(c, width=70, anchor="center") - self.ris_tree.pack(fill=tk.BOTH, expand=True) - self.ris_table_container.pack(fill=tk.BOTH, expand=True) - paned.add(right, weight=2) - gm = getattr(self.master, "config_manager", None) - trail_len = None - if gm: - general = gm.get_general_settings() or {} - trail_len = general.get("ppi_trail_length") - self.ris_ppi_widget = PPIDisplay(self.ris_ppi_container, max_range_nm=100, trail_length=trail_len) - self.ris_ppi_widget.pack(fill=tk.BOTH, expand=True) - btn_frame = ttk.Frame(ris_frame) - btn_frame.pack(fill=tk.X, padx=5, pady=(0, 5)) - self.scenario_view_mode = tk.StringVar(value="simplified") - mode_frame = ttk.Frame(btn_frame) - mode_frame.pack(side=tk.LEFT, padx=(4, 0)) - ttk.Label(mode_frame, text="View:").pack(side=tk.LEFT, padx=(0, 6)) - ttk.Radiobutton(mode_frame, text="Raw", value="raw", variable=self.scenario_view_mode).pack(side=tk.LEFT) - ttk.Radiobutton(mode_frame, text="Simplified", value="simplified", variable=self.scenario_view_mode).pack(side=tk.LEFT) - self.simplified_decimals = tk.IntVar(value=4) - ttk.Label(mode_frame, text=" Decimals:").pack(side=tk.LEFT, padx=(8, 2)) - ttk.Spinbox(mode_frame, from_=0, to=8, width=3, textvariable=self.simplified_decimals).pack(side=tk.LEFT) - self.ris_save_csv_btn = ttk.Button(btn_frame, text="Save CSV", command=self._on_save_ris_csv) - self.ris_save_csv_btn.pack(side=tk.RIGHT) - self.notebook.add(ris_frame, text="RIS Status") - raw_frame = ttk.Frame(self.notebook) - history_frame = ttk.Frame(raw_frame, width=380) - history_frame.pack(side=tk.LEFT, fill=tk.Y, padx=(5, 2), pady=5) - ttk.Label(history_frame, text="History (latest)").pack(anchor=tk.W, padx=4) - list_container = ttk.Frame(history_frame) - list_container.pack(fill=tk.BOTH, expand=True, padx=4, pady=(2, 4)) - columns = ("ts", "flow", "tid", "size") - self.history_tree = ttk.Treeview(list_container, columns=columns, show="headings", height=20) - self.history_tree.heading("ts", text="Timestamp") + # Set sensible column widths and stretching + self.scenario_tree.column("field", width=160, anchor=tk.W, stretch=False) + self.scenario_tree.column("value", width=220, anchor=tk.W, stretch=True) + scen_scroll = ttk.Scrollbar(left, orient=tk.VERTICAL, command=self.scenario_tree.yview) + self.scenario_tree.configure(yscrollcommand=scen_scroll.set) + scen_scroll.pack(side=tk.RIGHT, fill=tk.Y) + self.scenario_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=5, pady=5) + + # RIS targets table on the right with vertical and horizontal scrollbars + cols = ("index", "flags", "heading", "x", "y", "z") + self.ris_tree = ttk.Treeview(right, columns=cols, show="headings", height=16) + self.ris_tree.heading("index", text="#") + self.ris_tree.heading("flags", text="flags") + self.ris_tree.heading("heading", text="heading") + self.ris_tree.heading("x", text="x") + self.ris_tree.heading("y", text="y") + self.ris_tree.heading("z", text="z") + # Column sizing + self.ris_tree.column("index", width=40, anchor=tk.CENTER, stretch=False) + self.ris_tree.column("flags", width=60, anchor=tk.CENTER, stretch=False) + self.ris_tree.column("heading", width=80, anchor=tk.CENTER, stretch=False) + self.ris_tree.column("x", width=120, anchor=tk.E, stretch=True) + self.ris_tree.column("y", width=120, anchor=tk.E, stretch=True) + self.ris_tree.column("z", width=100, anchor=tk.E, stretch=False) + ris_v = ttk.Scrollbar(right, orient=tk.VERTICAL, command=self.ris_tree.yview) + ris_h = ttk.Scrollbar(right, orient=tk.HORIZONTAL, command=self.ris_tree.xview) + self.ris_tree.configure(yscrollcommand=ris_v.set, xscrollcommand=ris_h.set) + ris_v.pack(side=tk.RIGHT, fill=tk.Y) + ris_h.pack(side=tk.BOTTOM, fill=tk.X) + self.ris_tree.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) + + self.notebook.add(ris_frame, text="RIS") + + # History tab + history_frame = ttk.Frame(self.notebook) + self.history_tree = ttk.Treeview(history_frame, columns=("time", "flow", "tid", "size"), show="headings", height=8) + self.history_tree.heading("time", text="Time") self.history_tree.heading("flow", text="Flow") self.history_tree.heading("tid", text="TID") self.history_tree.heading("size", text="Size") - self.history_tree.column("ts", width=100, anchor="w") - self.history_tree.column("flow", width=50, anchor="w") - self.history_tree.column("tid", width=40, anchor="center") - self.history_tree.column("size", width=50, anchor="e") - self.history_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) - self.history_vscroll = ttk.Scrollbar(list_container, orient=tk.VERTICAL, command=self.history_tree.yview) - self.history_vscroll.pack(side=tk.RIGHT, fill=tk.Y) - self.history_tree.config(yscrollcommand=self.history_vscroll.set) - hb_frame = ttk.Frame(history_frame) - hb_frame.pack(fill=tk.X, padx=4, pady=(4, 4)) - self.history_settings_btn = ttk.Button(hb_frame, text="Settings", command=self._open_history_settings_dialog) - self.history_settings_btn.pack(side=tk.LEFT) - self.history_clear_btn = ttk.Button(hb_frame, text="Clear", command=self._on_clear_history) - self.history_clear_btn.pack(side=tk.RIGHT) - self.raw_tab_text = scrolledtext.ScrolledText(raw_frame, state=tk.DISABLED, wrap=tk.NONE, font=("Consolas", 9)) - self.raw_tab_text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(2, 5), pady=5) - self.notebook.insert(1, raw_frame, text="SFP Raw") - self.bin_tab = scrolledtext.ScrolledText(self.notebook, state=tk.DISABLED, wrap=tk.NONE, font=("Consolas", 10)) - self.notebook.add(self.bin_tab, text="Binary (Hex)") - self.json_tab = scrolledtext.ScrolledText(self.notebook, state=tk.DISABLED, wrap=tk.WORD, font=("Consolas", 10)) - self.notebook.add(self.json_tab, text="JSON") + self.history_tree.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) + btn_frame = ttk.Frame(history_frame) + btn_frame.pack(fill=tk.X, padx=5, pady=5) + ttk.Button(btn_frame, text="Clear", command=self._on_clear_history).pack(side=tk.LEFT) + ttk.Button(btn_frame, text="Settings", command=self._open_history_settings_dialog).pack(side=tk.LEFT, padx=4) + self.notebook.add(history_frame, text="History") + + # Image tabs (MFD, SAR) + self.mfd_tab = self._create_image_tab("MFD") + self.notebook.add(self.mfd_tab["frame"], text="MFD") + self.sar_tab = self._create_image_tab("SAR") + self.notebook.add(self.sar_tab["frame"], text="SAR") + + # Binary and JSON viewers + bin_frame = ttk.Frame(self.notebook) + self.bin_tab = scrolledtext.ScrolledText(bin_frame, height=12, state=tk.DISABLED, wrap=tk.NONE, font=("Consolas", 9)) + self.bin_tab.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) + self.notebook.add(bin_frame, text="BIN") + + json_frame = ttk.Frame(self.notebook) + self.json_tab = scrolledtext.ScrolledText(json_frame, height=12, state=tk.DISABLED, wrap=tk.NONE, font=("Consolas", 9)) + self.json_tab.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) + self.notebook.add(json_frame, text="JSON") + + def _create_script_sender_widgets(self, parent): + """Create a small UI to enter and send ad-hoc script/commands.""" + frame = ttk.Frame(parent, padding=4) + frame.pack(fill=tk.X) + ttk.Label(frame, text="Script:").pack(side=tk.LEFT, padx=(0, 4)) + entry = ttk.Entry(frame, textvariable=self.script_var, width=72) + entry.pack(side=tk.LEFT, fill=tk.X, expand=True) + send_btn = ttk.Button(frame, text="Send", command=self._on_send_script) + send_btn.pack(side=tk.LEFT, padx=(6, 0)) def _on_send_target(self): if not self.shared_communicator or not self.shared_communicator.is_open: diff --git a/tools/test_ris_remove.py b/tools/test_ris_remove.py new file mode 100644 index 0000000..bf09c52 --- /dev/null +++ b/tools/test_ris_remove.py @@ -0,0 +1,56 @@ +# Test removal of targets with flags == 0 +import logging +logging.basicConfig(level=logging.DEBUG, format='%(asctime)s [%(levelname)s] %(name)s: %(message)s') + +from target_simulator.gui.payload_router import DebugPayloadRouter +from target_simulator.analysis.simulation_state_hub import SimulationStateHub +import target_simulator.gui.payload_router as pr_mod + +class FakeRisTarget: + def __init__(self, flags, x, y, z, heading): + self.flags = flags + self.x = x + self.y = y + self.z = z + self.heading = heading + +class FakeScenario: + def __init__(self, timetag=123456789): + self.timetag = timetag + +class FakeParsed: + def __init__(self, tgt_list): + self.tgt = type('T', (), {'tgt': tgt_list})() + self.scenario = FakeScenario() + + @staticmethod + def from_buffer_copy(payload): + raise RuntimeError('Should be monkeypatched') + +# first, create initial payload with 2 active targets +rads = [0.0, 1.5707964897155762] +fake_targets = [] +for i, h in enumerate(rads): + fake_targets.append(FakeRisTarget(flags=1, x=1000+i, y=2000+i, z=100.0, heading=h)) + +original_parser = pr_mod.SfpRisStatusPayload + +def make_parser(targets): + return type('P', (), {'from_buffer_copy': staticmethod(lambda payload: FakeParsed(targets))}) + +# inject initial +pr_mod.SfpRisStatusPayload = make_parser(fake_targets) +hub = SimulationStateHub() +router = DebugPayloadRouter(simulation_hub=hub, update_queue=None) +router._handle_ris_status(b'INJ') +print('After first injection, hub ids:', hub.get_all_target_ids()) + +# now create a payload where target 1 is inactive (flags=0) +fake_targets2 = [FakeRisTarget(flags=1, x=1000, y=2000, z=100.0, heading=0.0), FakeRisTarget(flags=0, x=0, y=0, z=0, heading=0.0)] +pr_mod.SfpRisStatusPayload = make_parser(fake_targets2) +router._handle_ris_status(b'INJ2') +print('After second injection (one inactive), hub ids:', hub.get_all_target_ids()) + +# restore +pr_mod.SfpRisStatusPayload = original_parser +print('Done')