S1005403_RisCC/target_simulator/gui/sfp_debug_window.py

1348 lines
64 KiB
Python

# target_simulator/gui/sfp_debug_window.py
"""
Provides a Toplevel window for debugging the SFP transport layer.
This version uses a thread-safe queue for GUI updates to prevent deadlocks.
"""
import tkinter as tk
from tkinter import ttk, scrolledtext, messagebox
import logging
import datetime
import os
import ctypes
import math
import socket
from queue import Queue, Empty
from typing import Dict, Optional, Any, List
try:
from PIL import Image, ImageTk
import numpy as np
import cv2
_IMAGE_LIBS_AVAILABLE = True
except ImportError:
_IMAGE_LIBS_AVAILABLE = False
from target_simulator.core.sfp_transport import SfpTransport
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, NM_TO_FT
from target_simulator.core import command_builder
# Module-level logger
logger = logging.getLogger(__name__)
DEF_TEST_ID = 1
DEF_TEST_RANGE = 30.0
DEF_TEST_AZIMUTH = 10.0
DEF_TEST_VELOCITY = 300.0
DEF_TEST_HEADING = 0.0
DEF_TEST_ALTITUDE = 10000.0
DEF_TEST_RCS = 1.0
DEF_TEST_AMPLITUDE = 100.0
class SfpDebugWindow(tk.Toplevel):
"""Top-level window for SFP debugging and payload inspection."""
GUI_POLL_INTERVAL_MS = 100
def __init__(self, master=None):
super().__init__(master)
self.master = master
self.geometry("1100x700")
self.protocol("WM_DELETE_WINDOW", self._on_close)
self.logger = logger
self.debug_update_queue = Queue()
self.shared_communicator = getattr(self.master, "target_communicator", None)
self.payload_router: Optional[DebugPayloadRouter] = None
if self.shared_communicator:
router_attr = getattr(self.shared_communicator, "router", None)
if callable(router_attr):
self.payload_router = router_attr()
else:
self.payload_router = router_attr
if self.shared_communicator:
self.shared_communicator.add_connection_state_callback(
self._update_toggle_state
)
if self.payload_router:
self.payload_router.add_ris_target_listener(self._queue_ris_target_update)
self.image_area_size = 150
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="60003")
self.script_var = tk.StringVar(value="print('hello from client')")
self.tgt_id_var = tk.IntVar(value=DEF_TEST_ID)
self.tgt_range_var = tk.DoubleVar(value=DEF_TEST_RANGE)
self.tgt_az_var = tk.DoubleVar(value=DEF_TEST_AZIMUTH)
self.tgt_alt_var = tk.DoubleVar(value=DEF_TEST_ALTITUDE)
self.tgt_vel_var = tk.DoubleVar(value=DEF_TEST_VELOCITY)
self.tgt_hdg_var = tk.DoubleVar(value=DEF_TEST_HEADING)
self.tgt_rcs_var = tk.DoubleVar(value=DEF_TEST_RCS)
self.tgt_amplitude_var = tk.DoubleVar(value=DEF_TEST_AMPLITUDE)
self.tgt_active_var = tk.BooleanVar(value=True)
self.tgt_traceable_var = tk.BooleanVar(value=True)
self.tgt_restart_var = tk.BooleanVar(value=False)
# Send mode: either legacy command-line strings or JSON info payloads
self.send_mode_var = tk.StringVar(value="cmd")
# 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()
# Initialize connection fields from the central connection settings
try:
cfg = None
if (
hasattr(self.master, "connection_config")
and self.master.connection_config
):
cfg = self.master.connection_config.get("target", {})
else:
gm = getattr(self.master, "config_manager", None)
if gm:
cfg = gm.get_connection_settings().get("target", {})
if cfg is not None:
sfp_cfg = cfg.get("sfp", {})
if sfp_cfg:
ip = sfp_cfg.get("ip") or sfp_cfg.get("host")
if ip:
self.ip_var.set(str(ip))
port = sfp_cfg.get("port")
if port is not None:
self.server_port_var.set(str(port))
local = sfp_cfg.get("local_port")
if local is not None:
self.local_port_var.set(str(local))
except Exception:
self.logger.debug(
"Could not initialise SFP debug connection fields from central config",
exc_info=True,
)
if self.shared_communicator:
self._update_toggle_state(self.shared_communicator.is_open)
try:
general = None
if hasattr(self.master, "config_manager") and self.master.config_manager:
general = self.master.config_manager.get_general_settings() or {}
configured = None
if general is not None:
configured = general.get("GUI_POLL_INTERVAL_MS", general.get("gui_poll_interval_ms", None))
self.gui_poll_interval_ms = int(configured) if configured is not None else self.GUI_POLL_INTERVAL_MS
except Exception:
self.gui_poll_interval_ms = self.GUI_POLL_INTERVAL_MS
self.after(self.gui_poll_interval_ms, self._process_latest_payloads)
def _queue_ris_target_update(self, targets: List[Target]):
try:
self.debug_update_queue.put_nowait(targets)
except Exception:
self.logger.warning(
"SFP Debug window update queue is full or unavailable. Skipping an update."
)
def _on_close(self):
self.logger.info("SFP Debug Window closing.")
if self.shared_communicator:
try:
self.shared_communicator.remove_connection_state_callback(
self._update_toggle_state
)
except Exception:
pass
if self.payload_router:
try:
self.payload_router.remove_ris_target_listener(
self._queue_ris_target_update
)
except Exception:
pass
self.destroy()
def _process_gui_updates(self):
try:
while not self.debug_update_queue.empty():
real_targets = self.debug_update_queue.get_nowait()
self.update_ppi_targets(real_targets)
except Empty:
pass
if self.payload_router:
try:
new_payloads = self.payload_router.get_and_clear_latest_payloads()
if new_payloads:
for flow_id, payload in new_payloads.items():
if flow_id == "MFD" and _IMAGE_LIBS_AVAILABLE:
self._display_image_data(payload, self.mfd_tab, "mfd_photo")
elif flow_id == "SAR" and _IMAGE_LIBS_AVAILABLE:
self._display_image_data(payload, self.sar_tab, "sar_photo")
elif flow_id == "BIN":
self._display_hex_data(payload, self.bin_tab)
elif flow_id == "JSON":
self._display_json_data(payload, self.json_tab)
elif flow_id == "RIS_STATUS_JSON":
try:
import json
struct = (
json.loads(payload.decode("utf-8"))
if isinstance(payload, (bytes, bytearray))
else payload
)
view_mode = self.scenario_view_mode.get()
decimals = self.simplified_decimals.get()
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)
minutes_full = (d - degrees) * 60
minutes = int(minutes_full)
seconds = (minutes_full - minutes) * 60
direction = ""
if is_lat:
direction = "N" if deg >= 0 else "S"
else:
direction = "E" if deg >= 0 else "W"
return f"{degrees}° {minutes}' {seconds:.2f}\" {direction}"
self.scenario_tree.delete(
*self.scenario_tree.get_children()
)
scenario = struct.get("scenario", {})
if scenario:
for field, value in scenario.items():
display_value = value
if view_mode == "simplified":
if field in [
"platform_azimuth",
"true_heading",
"ant_nav_az",
"ant_nav_el",
]:
display_value = (
f"{to_deg(value):.{decimals}f}°"
)
elif field in ["vx", "vy", "vz"]:
display_value = f"{m_s_to_ft_s(value):.{decimals}f} ft/s"
elif field == "baro_altitude":
display_value = (
f"{m_to_ft(value):.{decimals}f} ft"
)
elif field in ["latitude", "longitude"]:
display_value = decimal_deg_to_dms(
value, is_lat=(field == "latitude")
)
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)
self.scenario_tree.insert(
"", tk.END, values=(field, display_value)
)
self.ris_tree.delete(*self.ris_tree.get_children())
targets = struct.get("targets", [])
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"
elif view_mode == "polar":
try:
own_x_ft, own_y_ft = (0.0, 0.0)
hub = getattr(self.payload_router, "_hub", None)
if hub:
own = hub.get_ownship_state() or {}
own_x_ft, own_y_ft = own.get("position_xy_ft", (0.0, 0.0))
xm = float(t.get("x", 0.0))
ym = float(t.get("y", 0.0))
x_ft = ym * 3.28084
y_ft = xm * 3.28084
rel_x = x_ft - own_x_ft
rel_y = y_ft - own_y_ft
range_nm_v = math.hypot(rel_x, rel_y) / NM_TO_FT
az_deg_v = math.degrees(math.atan2(rel_x, rel_y))
while az_deg_v > 180:
az_deg_v -= 360
while az_deg_v < -180:
az_deg_v += 360
heading = f"{t.get('heading', 0.0):.6f}"
x_pos = f"{range_nm_v:.{decimals}f} NM"
y_pos = f"{az_deg_v:.{decimals}f}°"
z_pos = f"{m_to_ft(t.get('z', 0.0)):.{decimals}f} ft"
except Exception:
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}"
z_pos = f"{t.get('z', 0.0):.3f}"
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}"
z_pos = f"{t.get('z', 0.0):.3f}"
vals = (
i,
flags_display,
heading,
x_pos,
y_pos,
z_pos,
)
self.ris_tree.insert("", tk.END, values=vals)
except Exception:
self.logger.exception(
"Failed to update RIS tables from JSON payload."
)
except Exception:
self.logger.exception(
"Error while fetching latest payloads from router"
)
if self.payload_router:
try:
raw_pkt = self.payload_router.get_and_clear_raw_packet()
if raw_pkt:
raw_bytes, addr = raw_pkt
self._display_raw_packet(raw_bytes, addr)
self._refresh_history_tree()
except Exception:
self.logger.exception("Error while fetching raw packet from router")
try:
self.after(self.gui_poll_interval_ms, self._process_gui_updates)
except Exception:
self.after(self.GUI_POLL_INTERVAL_MS, self._process_gui_updates)
def _create_widgets(self):
top_controls_frame = ttk.Frame(self)
top_controls_frame.pack(side=tk.TOP, fill=tk.X, padx=5, pady=5)
conn_frame = ttk.Frame(top_controls_frame)
conn_frame.pack(side=tk.TOP, fill=tk.X, pady=(0, 5))
self._create_connection_widgets(conn_frame)
target_sender_frame = ttk.LabelFrame(
top_controls_frame, text="Simple Target Sender"
)
target_sender_frame.pack(side=tk.TOP, fill=tk.X, pady=(5, 5))
self._create_target_sender_widgets(target_sender_frame)
script_frame = ttk.LabelFrame(top_controls_frame, text="Script to send")
script_frame.pack(side=tk.TOP, fill=tk.X, pady=(0, 5))
self._create_script_sender_widgets(script_frame)
self.notebook = ttk.Notebook(self)
self.notebook.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
self._create_notebook_tabs()
def update_ppi_targets(self, targets: List[Target]):
return
def _create_connection_widgets(self, parent):
ttk.Label(parent, text="IP:").pack(side=tk.LEFT, padx=(4, 2))
ttk.Entry(parent, textvariable=self.ip_var, width=18).pack(
side=tk.LEFT, padx=(0, 6)
)
ttk.Label(parent, text="Local Port:").pack(side=tk.LEFT, padx=(0, 2))
ttk.Entry(parent, textvariable=self.local_port_var, width=8).pack(
side=tk.LEFT, padx=(0, 6)
)
ttk.Label(parent, text="Server Port:").pack(side=tk.LEFT, padx=(0, 2))
ttk.Entry(parent, textvariable=self.server_port_var, width=8).pack(
side=tk.LEFT, padx=(0, 6)
)
self.connect_toggle_btn = ttk.Button(
parent, text="Connect", command=self._on_toggle_connect
)
self.connect_toggle_btn.pack(side=tk.LEFT, padx=(0, 6))
self.send_probe_btn = ttk.Button(
parent, text="Send Probe", command=self._on_send_probe
)
self.send_probe_btn.pack(side=tk.LEFT, padx=(6, 4))
def _create_target_sender_widgets(self, parent):
paned = ttk.Panedwindow(parent, orient=tk.HORIZONTAL)
paned.pack(fill=tk.X, expand=False)
left = ttk.Frame(paned, padding=5)
right = ttk.Frame(paned, padding=5)
paned.add(left, weight=3)
paned.add(right, weight=2)
grid = ttk.Frame(left)
grid.pack(fill=tk.X)
grid.columnconfigure(1, pad=15)
grid.columnconfigure(3, pad=15)
grid.columnconfigure(5, pad=15)
ttk.Label(grid, text="ID:").grid(row=0, column=0, sticky=tk.W)
ttk.Spinbox(grid, from_=0, to=15, textvariable=self.tgt_id_var, width=8).grid(
row=1, column=0, sticky=tk.W
)
ttk.Label(grid, text="Range (NM):").grid(row=0, column=1, sticky=tk.W)
ttk.Spinbox(
grid, from_=0, to=500, textvariable=self.tgt_range_var, width=10
).grid(row=1, column=1, sticky=tk.W)
ttk.Label(grid, text="Azimuth (°):").grid(row=0, column=2, sticky=tk.W)
ttk.Spinbox(
grid, from_=-180, to=180, textvariable=self.tgt_az_var, width=10
).grid(row=1, column=2, sticky=tk.W)
ttk.Label(grid, text="Velocity (kn):").grid(row=0, column=3, sticky=tk.W)
ttk.Spinbox(
grid, from_=0, to=2000, textvariable=self.tgt_vel_var, width=10
).grid(row=1, column=3, sticky=tk.W)
ttk.Label(grid, text="Heading (°):").grid(row=0, column=4, sticky=tk.W)
ttk.Spinbox(
grid, from_=0, to=360, textvariable=self.tgt_hdg_var, width=10
).grid(row=1, column=4, sticky=tk.W)
ttk.Label(grid, text="Altitude (ft):").grid(row=0, column=5, sticky=tk.W)
ttk.Spinbox(
grid, from_=0, to=80000, textvariable=self.tgt_alt_var, width=12
).grid(row=1, column=5, sticky=tk.W)
flags_frame = ttk.Frame(grid)
flags_frame.grid(row=2, column=0, columnspan=7, sticky="w", pady=(8, 0))
ttk.Label(flags_frame, text="RCS:").pack(side=tk.LEFT, anchor="w", padx=(0,2))
ttk.Spinbox(flags_frame, from_=0, to=1000, format="%.2f", increment=0.1, textvariable=self.tgt_rcs_var, width=8).pack(side=tk.LEFT, anchor="w")
ttk.Label(flags_frame, text="Amp:").pack(side=tk.LEFT, anchor="w", padx=(8,2))
ttk.Spinbox(flags_frame, from_=0, to=10000, format="%.1f", increment=1.0, textvariable=self.tgt_amplitude_var, width=8).pack(side=tk.LEFT, anchor="w")
ttk.Checkbutton(flags_frame, text="Active", variable=self.tgt_active_var).pack(
side=tk.LEFT, anchor="w", padx=(15, 5)
)
ttk.Checkbutton(
flags_frame, text="Traceable", variable=self.tgt_traceable_var
).pack(side=tk.LEFT, anchor="w", padx=5)
ttk.Checkbutton(
flags_frame, text="Restart", variable=self.tgt_restart_var
).pack(side=tk.LEFT, anchor="w", padx=5)
notebook = ttk.Notebook(right)
notebook.pack(fill=tk.BOTH, expand=True)
cmd_tab = ttk.Frame(notebook)
json_tab = ttk.Frame(notebook)
notebook.add(cmd_tab, text="CMD")
notebook.add(json_tab, text="JSON")
try:
notebook.select(cmd_tab)
except Exception:
pass
cmd_btn_frame = ttk.Frame(cmd_tab)
cmd_btn_frame.pack(side=tk.TOP, anchor="w", padx=4, pady=4)
btn_specs = [
("tgtreset", lambda: self._on_send_simple_command(command_builder.build_tgtreset())),
("pause", lambda: self._on_send_simple_command(command_builder.build_pause())),
("continue", lambda: self._on_send_simple_command(command_builder.build_continue())),
("Send Target", lambda: (self.send_mode_var.set("cmd"), self._on_send_target())),
("reset (legacy)", lambda: self._on_send_reset_button()),
]
for idx, (text, cmd) in enumerate(btn_specs):
r = idx // 3
c = idx % 3
ttk.Button(cmd_btn_frame, text=text, command=cmd, width=12).grid(
row=r, column=c, padx=6, pady=2, sticky="w"
)
ttk.Button(
json_tab,
text="Send Target (JSON)",
command=lambda: (self.send_mode_var.set("json"), self._on_send_target()),
).pack(side=tk.TOP, anchor="w", padx=4, pady=4)
ttk.Button(
json_tab,
text="Reset (JSON)",
command=lambda: (self.send_mode_var.set("json"), self._on_send_reset_button()),
).pack(side=tk.TOP, anchor="w", padx=4, pady=4)
ttk.Button(
json_tab,
text="Reset IDs",
command=lambda: (self.send_mode_var.set("json"), self._on_send_reset_ids()),
).pack(side=tk.TOP, anchor="w", padx=4, pady=4)
def _create_notebook_tabs(self):
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_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_frame = ttk.Frame(self.notebook)
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))
try:
self.scenario_view_mode_opt = ttk.OptionMenu(
controls,
self.scenario_view_mode,
self.scenario_view_mode.get(),
"simplified",
"raw",
"polar",
)
self.scenario_view_mode_opt.pack(side=tk.LEFT)
except Exception:
self.scenario_view_mode.set("simplified")
self.scenario_view_mode_opt = tk.OptionMenu(
controls, self.scenario_view_mode, "simplified", "raw", "polar"
)
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)
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)
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=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)
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")
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_cmd = getattr(
self.ris_tree,
"xview",
getattr(self.ris_tree, "yview", lambda *a, **k: None),
)
ris_h = ttk.Scrollbar(right, orient=tk.HORIZONTAL, command=ris_h_cmd)
xscroll_cmd = ris_h.set if hasattr(ris_h, "set") else (lambda *a, **k: None)
self.ris_tree.configure(yscrollcommand=ris_v.set, xscrollcommand=xscroll_cmd)
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_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.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")
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")
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):
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:
self._log_to_widget("ERROR: Cannot send target, not connected.", "ERROR")
messagebox.showerror(
"Connection Error", "Communicator is not connected.", parent=self
)
return
try:
target_id = self.tgt_id_var.get()
range_nm = self.tgt_range_var.get()
az_deg = self.tgt_az_var.get()
alt_ft = self.tgt_alt_var.get()
vel_kn = self.tgt_vel_var.get()
hdg_deg = self.tgt_hdg_var.get()
rcs = self.tgt_rcs_var.get()
amplitude = self.tgt_amplitude_var.get()
is_active = self.tgt_active_var.get()
is_traceable = self.tgt_traceable_var.get()
is_restart = self.tgt_restart_var.get()
vel_fps = vel_kn * KNOTS_TO_FPS
initial_waypoint = Waypoint(
maneuver_type=ManeuverType.FLY_TO_POINT,
target_range_nm=range_nm,
target_azimuth_deg=az_deg,
target_altitude_ft=alt_ft,
target_velocity_fps=vel_fps,
target_heading_deg=hdg_deg,
)
temp_target = Target(target_id=target_id, trajectory=[initial_waypoint])
temp_target.active = is_active
temp_target.traceable = is_traceable
temp_target.restart = is_restart
temp_target.rcs = rcs
temp_target.amplitude = amplitude
temp_target.current_range_nm = range_nm
temp_target.current_azimuth_deg = az_deg
temp_target.current_velocity_fps = vel_fps
temp_target.current_heading_deg = hdg_deg
temp_target.current_altitude_ft = alt_ft
if self.send_mode_var.get() == "json":
try:
json_payload = command_builder.build_json_update([temp_target])
if hasattr(self.shared_communicator, "_save_json_payload_to_temp"):
try:
self.shared_communicator._save_json_payload_to_temp(
json_payload, "sfp_debug_send"
)
except Exception:
pass
if not json_payload.endswith("\n"):
json_payload = json_payload + "\n"
self._log_to_widget(
f"Built JSON payload for target {target_id}.", "INFO"
)
success = self.shared_communicator._send_single_command(
json_payload
)
if success:
self._log_to_widget(
f"Successfully sent JSON payload for target {target_id}.",
"INFO",
)
else:
self._log_to_widget(
f"Failed to send JSON payload for target {target_id}.",
"ERROR",
)
except Exception:
self.logger.exception(
"Failed to build/send JSON payload in debug window"
)
self._log_to_widget(
"ERROR: Failed to build/send JSON payload.", "ERROR"
)
else:
command_str = command_builder.build_tgtset_from_target_state(
temp_target, include_flags=True
)
command_str = self._ensure_legacy_prefixed(command_str)
self._log_to_widget(f"Built command: {command_str!r}", "INFO")
success = self.shared_communicator._send_single_command(command_str)
if success:
self._log_to_widget(
f"Successfully sent command for target {target_id}.", "INFO"
)
else:
self._log_to_widget(
f"Failed to send command for target {target_id}.", "ERROR"
)
except (ValueError, tk.TclError) as e:
error_msg = f"Invalid input value: {e}"
self._log_to_widget(f"ERROR: {error_msg}", "ERROR")
messagebox.showerror("Input Error", error_msg, parent=self)
except Exception as e:
self.logger.exception("An unexpected error occurred in _on_send_target.")
self._log_to_widget(f"ERROR: {e}", "CRITICAL")
messagebox.showerror("Unexpected Error", str(e), parent=self)
def _create_image_tab(self, title: str) -> Dict:
frame = ttk.Frame(self.notebook)
image_container = ttk.Frame(
frame,
width=self.image_area_size,
height=self.image_area_size,
relief=tk.SUNKEN,
)
image_container.pack(pady=5, padx=5)
image_container.pack_propagate(False)
image_label = ttk.Label(
image_container, text=f"Waiting for {title}...", anchor=tk.CENTER
)
image_label.pack(fill=tk.BOTH, expand=True)
hex_view = scrolledtext.ScrolledText(
frame, height=8, state=tk.DISABLED, wrap=tk.NONE, font=("Consolas", 9)
)
hex_view.pack(fill=tk.BOTH, expand=True, pady=5, padx=5)
return {
"frame": frame,
"image_label": image_label,
"hex_view": hex_view,
"image_container": image_container,
}
def _open_image_size_dialog(self):
dlg = tk.Toplevel(self)
dlg.title("Image Size")
dlg.transient(self)
dlg.grab_set()
ttk.Label(dlg, text="Image area size (px):").pack(padx=10, pady=(10, 2))
size_var = tk.StringVar(value=str(self.image_area_size))
entry = ttk.Entry(dlg, textvariable=size_var, width=8)
entry.pack(padx=10, pady=(0, 10))
btn_frame = ttk.Frame(dlg)
btn_frame.pack(padx=10, pady=(0, 10))
def on_save():
try:
v = int(size_var.get())
if v <= 0:
raise ValueError()
except Exception:
messagebox.showerror(
"Invalid value",
"Please enter a positive integer for image size.",
parent=dlg,
)
return
self.image_area_size = v
for tab in (getattr(self, "mfd_tab", None), getattr(self, "sar_tab", None)):
if tab and "image_container" in tab:
tab["image_container"].config(width=v, height=v)
gm = getattr(self.master, "config_manager", None)
if gm:
general = gm.get_general_settings() or {}
image_display = general.get("image_display", {})
image_display["size"] = v
general["image_display"] = image_display
gm.save_general_settings(general)
dlg.destroy()
def on_cancel():
dlg.destroy()
ttk.Button(btn_frame, text="Cancel", command=on_cancel).pack(
side=tk.RIGHT, padx=(0, 5)
)
ttk.Button(btn_frame, text="Save", command=on_save).pack(side=tk.RIGHT)
def _on_toggle_connect(self):
if not self.shared_communicator:
self._log_to_widget("ERROR: No shared communicator available.", "ERROR")
messagebox.showerror(
"Error", "No shared communicator available.", parent=self
)
return
if self.shared_communicator.is_open:
self.shared_communicator.disconnect()
else:
try:
ip = self.ip_var.get()
server_port = int(self.server_port_var.get())
local_port = int(self.local_port_var.get())
config = {"ip": ip, "port": server_port, "local_port": local_port}
self.shared_communicator.connect(config)
except (ValueError, tk.TclError) as e:
self._log_to_widget(f"ERROR: Invalid connection settings: {e}", "ERROR")
messagebox.showerror(
"Input Error", f"Invalid connection settings: {e}", parent=self
)
def _on_send_probe(self):
ip = self.ip_var.get()
try:
port = int(self.server_port_var.get())
except Exception:
self._log_to_widget("ERROR: Invalid port number for probe.", "ERROR")
return
probe_payload = b"SFP_PROBE\n"
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.settimeout(1.0)
sock.sendto(probe_payload, (ip, port))
sock.close()
self._log_to_widget(f"Sent probe to {ip}:{port}", "INFO")
except Exception as e:
self._log_to_widget(f"Failed to send probe to {ip}:{port}: {e}", "ERROR")
def _on_send_script(self):
if not self.shared_communicator or not self.shared_communicator.is_open:
self._log_to_widget("ERROR: Cannot send script, not connected.", "ERROR")
return
try:
command_str = self.script_var.get()
command_str = self._ensure_legacy_prefixed(command_str)
self.shared_communicator._send_single_command(command_str)
except (ValueError, tk.TclError) as e:
self._log_to_widget(
f"ERROR: Invalid input for script sending: {e}", "ERROR"
)
def _on_send_reset_button(self):
if not self.shared_communicator or not self.shared_communicator.is_open:
self._log_to_widget("ERROR: Cannot send reset, not connected.", "ERROR")
messagebox.showerror(
"Connection Error", "Communicator is not connected.", parent=self
)
return False
try:
if self.send_mode_var.get() == "json":
json_payload = '{"CMD":"reset"}'
if not json_payload.endswith("\n"):
json_payload = json_payload + "\n"
self._log_to_widget("Sending JSON reset payload...", "INFO")
return self._on_send_simple_command(json_payload)
else:
cmd1 = "mex.t_rows=80\n"
cmd2 = "tgtset /-s\n"
self._log_to_widget("Sending legacy reset sequence...", "INFO")
ok1 = self._on_send_simple_command(cmd1)
ok2 = self._on_send_simple_command(cmd2)
return bool(ok1 and ok2)
except Exception:
self.logger.exception("Failed to send reset command from debug window")
self._log_to_widget("ERROR: Failed to send reset.", "ERROR")
return False
def _on_send_reset_ids(self):
if not self.shared_communicator or not self.shared_communicator.is_open:
self._log_to_widget("ERROR: Cannot send reset IDs, not connected.", "ERROR")
messagebox.showerror(
"Connection Error", "Communicator is not connected.", parent=self
)
return False
try:
import json as _json
MAX_BYTES = 1020
payloads = command_builder.build_json_reset_ids(max_bytes=MAX_BYTES)
if not payloads:
self._log_to_widget("ERROR: Cannot build reset ID payloads.", "ERROR")
return False
overall_ok = True
for i, payload in enumerate(payloads):
if hasattr(self.shared_communicator, "_save_json_payload_to_temp") and i == 0:
try:
self.shared_communicator._save_json_payload_to_temp(
payload, f"sfp_debug_reset_ids_part_{i}"
)
except Exception:
pass
self._log_to_widget(
f"Sending Reset IDs JSON payload part {i+1}/{len(payloads)}...", "INFO"
)
ok = self.shared_communicator._send_single_command(payload)
overall_ok = overall_ok and bool(ok)
if not ok:
self._log_to_widget(
f"Failed to send Reset IDs payload part {i+1}.", "ERROR"
)
if overall_ok:
self._log_to_widget(
"Successfully sent all Reset IDs JSON payloads.", "INFO"
)
else:
self._log_to_widget(
"One or more Reset IDs JSON payloads failed.", "ERROR"
)
return overall_ok
except Exception:
self.logger.exception("Failed to build/send Reset IDs payload")
self._log_to_widget("ERROR: Failed to send Reset IDs.", "ERROR")
return False
def _on_send_simple_command(self, command_str: str):
if not self.shared_communicator or not self.shared_communicator.is_open:
self._log_to_widget("ERROR: Cannot send command, not connected.", "ERROR")
messagebox.showerror(
"Connection Error", "Communicator is not connected.", parent=self
)
return False
try:
command_str = self._ensure_legacy_prefixed(command_str)
success = self.shared_communicator._send_single_command(command_str)
if success:
self._log_to_widget(f"Successfully sent command: {command_str}", "INFO")
else:
self._log_to_widget(f"Failed to send command: {command_str}", "ERROR")
return success
except Exception as e:
self.logger.exception("Unexpected error in _on_send_simple_command")
self._log_to_widget(f"ERROR: {e}", "ERROR")
return False
def _on_send_tgtset(self):
try:
self._on_send_target()
return True
except Exception:
self.logger.exception("Failed while sending tgtset")
return False
def _update_toggle_state(self, connected: bool):
try:
if connected:
self.connect_toggle_btn.config(text="Disconnect")
else:
self.connect_toggle_btn.config(text="Connect")
except Exception:
pass
def update_toggle_state(self, connected: bool):
self._update_toggle_state(connected)
def _process_latest_payloads(self):
if not self.payload_router:
self.after(self.GUI_POLL_INTERVAL_MS, self._process_latest_payloads)
return
try:
new_payloads = self.payload_router.get_and_clear_latest_payloads()
except Exception:
self.logger.exception("Error while fetching latest payloads from router")
new_payloads = {}
if new_payloads:
for flow_id, payload in new_payloads.items():
if flow_id == "MFD" and _IMAGE_LIBS_AVAILABLE:
self._display_image_data(payload, self.mfd_tab, "mfd_photo")
elif flow_id == "SAR" and _IMAGE_LIBS_AVAILABLE:
self._display_image_data(payload, self.sar_tab, "sar_photo")
elif flow_id == "BIN":
self._display_hex_data(payload, self.bin_tab)
elif flow_id == "JSON":
self._display_json_data(payload, self.json_tab)
elif flow_id == "RIS_STATUS_JSON":
try:
import json
struct = json.loads(payload.decode("utf-8")) if isinstance(payload, (bytes, bytearray)) else payload
view_mode = self.scenario_view_mode.get()
decimals = self.simplified_decimals.get()
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)
minutes_full = (d - degrees) * 60
minutes = int(minutes_full)
seconds = (minutes_full - minutes) * 60
direction = "N" if deg >= 0 else "S" if is_lat else "E" if deg >= 0 else "W"
return f"{degrees}° {minutes}' {seconds:.2f}\" {direction}"
for iid in self.scenario_tree.get_children(): self.scenario_tree.delete(iid)
scenario = struct.get("scenario", {})
if scenario:
for field_name, value in scenario.items():
display_value = value
if view_mode == "simplified":
if field_name in ["platform_azimuth", "true_heading", "ant_nav_az", "ant_nav_el"]:
try: display_value = f"{to_deg(value):.{decimals}f}°"
except Exception: display_value = str(value)
elif field_name in ["vx", "vy", "vz"]:
try: display_value = f"{m_s_to_ft_s(value):.{decimals}f} ft/s"
except Exception: display_value = str(value)
elif field_name == "baro_altitude":
try: display_value = f"{m_to_ft(value):.{decimals}f} ft"
except Exception: display_value = str(value)
elif field_name in ["latitude", "longitude"]:
try: display_value = decimal_deg_to_dms(value, is_lat=(field_name == "latitude"))
except Exception: display_value = str(value)
elif field_name == "mode" and isinstance(value, int) 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)
self.scenario_tree.insert("", tk.END, values=(field_name, display_value))
for iid in self.ris_tree.get_children(): self.ris_tree.delete(iid)
targets = struct.get("targets", [])
for i, t in enumerate(targets):
flags_val = t.get("flags", 0)
flags_display = f"0x{flags_val:X}"
if view_mode == "simplified":
try: heading = f"{to_deg(t.get('heading', 0.0)):.{decimals}f}°"
except Exception: heading = f"{t.get('heading', 0.0):.6f}"
try:
x_pos, y_pos, z_pos = f"{m_to_ft(t.get('x', 0.0)):.{decimals}f} ft", f"{m_to_ft(t.get('y', 0.0)):.{decimals}f} ft", f"{m_to_ft(t.get('z', 0.0)):.{decimals}f} ft"
except Exception:
x_pos, y_pos, z_pos = f"{t.get('x', 0.0):.3f}", f"{t.get('y', 0.0):.3f}", f"{t.get('z', 0.0):.3f}"
elif view_mode == "polar":
try:
hub = getattr(self.payload_router, "_hub", None)
own_x_ft, own_y_ft = hub.get_ownship_state().get("position_xy_ft", (0.0, 0.0)) if hub and hub.get_ownship_state() else (0.0, 0.0)
xm, ym = float(t.get('x', 0.0)), float(t.get('y', 0.0))
x_ft, y_ft = ym * 3.28084, xm * 3.28084
rel_x, rel_y = x_ft - own_x_ft, y_ft - own_y_ft
range_nm_v, az_deg_v = math.hypot(rel_x, rel_y) / NM_TO_FT, math.degrees(math.atan2(rel_x, rel_y))
az_deg_v = (az_deg_v + 180) % 360 - 180
heading, x_pos, y_pos, z_pos = f"{t.get('heading', 0.0):.6f}", f"{range_nm_v:.{decimals}f} NM", f"{az_deg_v:.{decimals}f}°", f"{m_to_ft(t.get('z', 0.0)):.{decimals}f} ft"
except Exception:
heading, x_pos, y_pos, z_pos = f"{t.get('heading', 0.0):.6f}", f"{t.get('x', 0.0):.3f}", f"{t.get('y', 0.0):.3f}", f"{t.get('z', 0.0):.3f}"
else:
heading, x_pos, y_pos, z_pos = f"{t.get('heading', 0.0):.6f}", f"{t.get('x', 0.0):.3f}", f"{t.get('y', 0.0):.3f}", f"{t.get('z', 0.0):.3f}"
vals = (i, flags_display, heading, x_pos, y_pos, z_pos)
self.ris_tree.insert("", tk.END, values=vals)
except Exception:
self.logger.exception("Failed to update RIS tables from JSON payload.")
try:
raw_pkt = self.payload_router.get_and_clear_raw_packet() if self.payload_router else None
if raw_pkt:
raw_bytes, addr = raw_pkt
self._display_raw_packet(raw_bytes, addr)
self._refresh_history_tree()
except Exception:
self.logger.exception("Error while fetching raw packet from router")
self.after(self.GUI_POLL_INTERVAL_MS, self._process_latest_payloads)
def _refresh_history_tree(self):
try:
if self.payload_router:
hist = self.payload_router.get_history()
self.history_tree.delete(*self.history_tree.get_children())
for i, entry in enumerate(reversed(hist)):
ts = entry["ts"].strftime("%H:%M:%S.%f")[:-3]
flow_name = entry.get("flow_name", "")
tid = entry.get("tid", "")
size = len(entry.get("raw", b""))
self.history_tree.insert("", tk.END, values=(ts, flow_name, tid, f"{size}B"))
except Exception:
pass
def _on_history_select(self, event=None):
try:
sel = self.history_tree.selection()
if not sel: return
iid = sel[0]
children = list(self.history_tree.get_children())
idx = children.index(iid)
hist = self.payload_router.get_history()
rev_idx = len(hist) - 1 - idx
if not (0 <= rev_idx < len(hist)): return
entry = hist[rev_idx]
self._display_raw_packet(entry["raw"], entry["addr"])
except Exception:
self.logger.exception("Error during history selection handling")
def _on_clear_history(self):
if self.payload_router: self.payload_router.clear_history()
self._refresh_history_tree()
def _open_history_settings_dialog(self):
dlg = tk.Toplevel(self)
dlg.title("History Settings")
dlg.transient(self)
dlg.grab_set()
hist_size = self.payload_router._history_size if self.payload_router else 20
persist = self.payload_router._persist if self.payload_router else False
ttk.Label(dlg, text="History size (entries):").pack(padx=10, pady=(10, 2))
size_var = tk.StringVar(value=str(hist_size))
entry = ttk.Entry(dlg, textvariable=size_var, width=8)
entry.pack(padx=10, pady=(0, 10))
persist_var = tk.BooleanVar(value=bool(persist))
ttk.Checkbutton(dlg, text="Persist raw packets to Temp/", variable=persist_var).pack(padx=10, pady=(0, 10))
try:
import logging as _logging
LEVEL_NAMES = ["NOTSET", "DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
pr_logger = _logging.getLogger("target_simulator.gui.payload_router")
current_level = _logging.getLevelName(pr_logger.getEffectiveLevel())
except Exception:
LEVEL_NAMES = ["NOTSET", "DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
current_level = "INFO"
ttk.Label(dlg, text="Payload Router logger level:").pack(padx=10, pady=(4, 2))
logger_level_var = tk.StringVar(value=current_level)
level_combo = ttk.Combobox(dlg, values=LEVEL_NAMES, textvariable=logger_level_var, state="readonly", width=12)
level_combo.pack(padx=10, pady=(0, 8))
btn_frame = ttk.Frame(dlg)
btn_frame.pack(padx=10, pady=(0, 10), fill=tk.X)
def on_save():
try: v = int(size_var.get())
except Exception: messagebox.showerror("Invalid value", "Please enter a positive integer for history size.", parent=dlg); return
if v <= 0: messagebox.showerror("Invalid value", "History size must be positive.", parent=dlg); return
if self.payload_router:
self.payload_router.set_history_size(v)
self.payload_router.set_persist(bool(persist_var.get()))
gm = getattr(self.master, "config_manager", None)
if gm:
general = gm.get_general_settings() or {}
sfp_debug = general.get("sfp_debug", {})
sfp_debug["history_size"], sfp_debug["persist_raw"] = v, bool(persist_var.get())
general["sfp_debug"] = sfp_debug
try:
lvl_name = logger_level_var.get()
if lvl_name:
lp = general.get("logger_panel", {})
saved = lp.get("saved_levels", {})
saved["target_simulator.gui.payload_router"] = lvl_name
lp["saved_levels"] = saved
general["logger_panel"] = lp
try:
import logging as _logging
lvl_val = _logging.getLevelName(lvl_name)
if isinstance(lvl_val, int): _logging.getLogger("target_simulator.gui.payload_router").setLevel(lvl_val)
except Exception: pass
except Exception: pass
gm.save_general_settings(general)
dlg.destroy()
def on_cancel(): dlg.destroy()
ttk.Button(btn_frame, text="Cancel", command=on_cancel).pack(side=tk.RIGHT, padx=(0, 5))
ttk.Button(btn_frame, text="Save", command=on_save).pack(side=tk.RIGHT)
def _display_raw_packet(self, raw_bytes: bytes, addr: tuple):
try:
header_size = SFPHeader.size()
if len(raw_bytes) < header_size: raise ValueError("Packet smaller than SFP header")
header = SFPHeader.from_buffer_copy(raw_bytes)
body = raw_bytes[header_size:]
field_list = ["SFP_MARKER", "SFP_DIRECTION", "SFP_PROT_VER", "SFP_PT_SPARE", "SFP_TAG", "SFP_SRC", "SFP_FLOW", "SFP_TID", "SFP_FLAGS", "SFP_WIN", "SFP_ERR", "SFP_ERR_INFO", "SFP_TOTFRGAS", "SFP_FRAG", "SFP_RECTYPE", "SFP_RECSPARE", "SFP_PLDAP", "SFP_PLEXT", "SFP_RECCOUNTER", "SFP_PLSIZE", "SFP_TOTSIZE", "SFP_PLOFFSET"]
pairs, flag_val = [], 0
for f in field_list:
val = getattr(header, f)
if f == "SFP_FLAGS": flag_val = val
pairs.append((f, f"{val} (0x{val:X})" if isinstance(val, int) else str(val)))
out_lines = [f"From {addr}\n\nSFP Header:\n\n"]
col_width = 36
for i in range(0, len(pairs), 2):
left_text = f"{pairs[i][0]:12s}: {pairs[i][1]}"
right_text = f"{pairs[i+1][0]:12s}: {pairs[i+1][1]}" if (i + 1) < len(pairs) else ""
out_lines.append(f"{left_text:<{col_width}} {right_text}\n")
flag_defs = [(0, "ACQ_REQ"), (1, "RESENT"), (2, "TRAILER_ACK"), (3, "RESV3"), (4, "RESV4"), (5, "RESV5"), (6, "RESV6"), (7, "ERROR")]
out_lines.append(f"SFP_FLAGS : {flag_val} (0x{flag_val:X}) " + "".join([f" [{(name if bool((flag_val >> bit) & 1) else ' ')}]" for bit, name in flag_defs]) + "\n")
out_lines.append("\nBODY (hex):\n" + self._format_hex_dump(body))
self.raw_tab_text.config(state=tk.NORMAL)
self.raw_tab_text.delete("1.0", tk.END)
self.raw_tab_text.insert("1.0", "".join(out_lines))
self.raw_tab_text.config(state=tk.DISABLED)
except Exception as e:
text = f"Failed to format raw packet: {e}\n\nRaw dump:\n{self._format_hex_dump(raw_bytes)}"
self.raw_tab_text.config(state=tk.NORMAL)
self.raw_tab_text.delete("1.0", tk.END)
self.raw_tab_text.insert("1.0", text)
self.raw_tab_text.config(state=tk.DISABLED)
def _display_image_data(self, payload: bytearray, tab_widgets: Dict[str, Any], photo_attr: str):
try:
if len(payload) < ctypes.sizeof(ImageLeaderData): raise ValueError("Payload smaller than ImageLeaderData header.")
leader = ImageLeaderData.from_buffer(payload)
h, w, bpp, stride = leader.HEADER_DATA.DY, leader.HEADER_DATA.DX, leader.HEADER_DATA.BPP, leader.HEADER_DATA.STRIDE
offset = ctypes.sizeof(ImageLeaderData)
if not (h > 0 and w > 0 and bpp in [1, 2] and stride >= w): raise ValueError("Invalid image dimensions")
expected_size = stride * h * bpp
if (offset + expected_size) > len(payload): raise ValueError("Incomplete image data")
pixel_data_view = np.ndarray(shape=(h, stride), dtype=np.uint8 if bpp == 1 else np.uint16, buffer=payload, offset=offset)
image_data = pixel_data_view[:, :w]
display_img_8bit = cv2.normalize(image_data, None, 0, 255, cv2.NORM_MINMAX, cv2.CV_8U)
img_pil = Image.fromarray(cv2.cvtColor(display_img_8bit, cv2.COLOR_GRAY2RGB))
resized = self._resize_pil_to_label(img_pil, tab_widgets["image_label"])
photo = ImageTk.PhotoImage(image=resized)
tab_widgets["image_label"].config(image=photo, text="")
setattr(self, photo_attr, photo)
except Exception as e:
tab_widgets["image_label"].config(image=None, text=f"Error parsing image:\n{e}")
setattr(self, photo_attr, None)
self._display_hex_data(payload, tab_widgets["hex_view"])
def _resize_pil_to_label(self, img: "Image.Image", label_widget: ttk.Label) -> "Image.Image":
try:
width, height = label_widget.winfo_width(), label_widget.winfo_height()
if width <= 1 or height <= 1: return img
scale = min(width / img.width, height / img.height)
if scale >= 1.0: return img
new_w, new_h = int(img.width * scale), int(img.height * scale)
return img.resize((new_w, new_h), Image.LANCZOS)
except Exception:
return img
def _display_hex_data(self, payload: bytearray, widget: scrolledtext.ScrolledText):
hex_dump = self._format_hex_dump(payload)
widget.config(state=tk.NORMAL)
widget.delete("1.0", tk.END)
widget.insert("1.0", hex_dump)
widget.config(state=tk.DISABLED)
def _display_json_data(self, payload: bytearray, widget: scrolledtext.ScrolledText):
try:
import json
text = json.dumps(json.loads(payload.decode("utf-8")), indent=2)
except Exception as e:
text = f"--- FAILED TO PARSE JSON ---\n{e}\n\n--- RAW HEX DUMP ---\n{self._format_hex_dump(payload)}"
widget.config(state=tk.NORMAL)
widget.delete("1.0", tk.END)
widget.insert("1.0", text)
widget.config(state=tk.DISABLED)
def _log_to_widget(self, message: str, level: str = "DEBUG"):
self.logger.info(message)
self.log_tab.config(state=tk.NORMAL)
self.log_tab.insert(tk.END, f"[{level}] {message}\n")
self.log_tab.config(state=tk.DISABLED)
self.log_tab.see(tk.END)
def _ensure_legacy_prefixed(self, command: str) -> str:
try:
s = command.decode("utf-8") if isinstance(command, (bytes, bytearray)) else str(command)
except Exception:
s = str(command)
s = s.strip()
if not s: return s
if not s.endswith("\n"): s = s + "\n"
return s
def _on_save_ris_csv(self):
try:
import csv
scenario_rows = [self.scenario_tree.item(iid, "values") for iid in self.scenario_tree.get_children()]
rows = [self.ris_tree.item(iid, "values") for iid in self.ris_tree.get_children()]
if not (scenario_rows or rows): self._log_to_widget("No RIS data to save.", "INFO"); return
temp_dir = os.path.join(os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")), "Temp")
os.makedirs(temp_dir, exist_ok=True)
ts = datetime.datetime.utcnow().strftime("%Y%m%dT%H%M%S")
path = os.path.join(temp_dir, f"ris_data_{ts}.csv")
with open(path, "w", newline="", encoding="utf-8") as f:
writer = csv.writer(f)
if scenario_rows:
writer.writerow(["Scenario Field", "Value"])
writer.writerows(scenario_rows)
writer.writerow([])
writer.writerow(["#", "flags", "heading", "x", "y", "z"])
writer.writerows(rows)
self._log_to_widget(f"Saved RIS data to CSV: {path}", "INFO")
except Exception as e:
self._log_to_widget(f"Failed to save RIS CSV: {e}", "ERROR")
def _format_hex_dump(self, data: bytes, length=16) -> str:
lines = []
for i in range(0, len(data), length):
chunk = data[i : i + length]
hex_part = " ".join(f"{b:02X}" for b in chunk)
ascii_part = "".join(chr(b) if 32 <= b < 127 else "." for b in chunk)
lines.append(f"{i:08X} {hex_part:<{length*3}} |{ascii_part}|")
return "\n".join(lines)