Compare commits

...

4 Commits

Author SHA1 Message Date
VALLONGOL
e78691ba54 sistemata anche crs feasability e spoi feasability 2026-01-12 16:03:16 +01:00
VALLONGOL
95a3d6e562 aggiunti i tellback mancanti 2026-01-12 16:00:11 +01:00
VALLONGOL
7d15cf143a sistemati anche i tooltip dei messaggi B 2026-01-12 12:38:47 +01:00
VALLONGOL
ac843839bb aggiunta la schermata di debug del canale 1553 nel menu 2026-01-12 11:05:01 +01:00
15 changed files with 4531 additions and 105 deletions

3631
_cpp/th_b1553_icd.h Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,5 @@
{
"window_geometry": "1600x1099+310+130",
"window_geometry": "1600x1099+26+26",
"main_sash_position": [
1,
793

View File

@ -7,6 +7,7 @@ em.ENUM_MAP.update({
"rws_submode_tellback" : RwsSubmode,
"spot_function_tellback" : SpotSelection,
"acm_submode_tellback" : AcmSubmode,
"gm_submode_tellback" : GmSubmode,
"expand_tellback" : Expand,
"range_scale_tellback" : RangeScale,
"number_of_bars_tellback" : BarsNum,
@ -16,14 +17,14 @@ em.ENUM_MAP.update({
class _RdrFunAndParam1TellbackStr(ctypes.LittleEndianStructure):
_pack_ = 1
_fields_ = [
("azimuth_scan_width_tellback" , ctypes.c_uint16,4),
("number_of_bars_tellback" , ctypes.c_uint16,2),
("range_scale_tellback" , ctypes.c_uint16,2),
("expand_tellback" , ctypes.c_uint16,2),
("reserved" , ctypes.c_uint16,1),
("acm_submode_tellback" , ctypes.c_uint16,3),
("spot_function_tellback" , ctypes.c_uint16,1),
("rws_submode_tellback" , ctypes.c_uint16,1)
("azimuth_scan_width_tellback", ctypes.c_uint16, 4), # bit 0-3
("number_of_bars_tellback", ctypes.c_uint16, 2), # bit 4-5
("range_scale_tellback", ctypes.c_uint16, 2), # bit 6-7
("expand_tellback", ctypes.c_uint16, 2), # bit 8-9
("gm_submode_tellback", ctypes.c_uint16, 1), # bit 10
("acm_submode_tellback", ctypes.c_uint16, 3), # bit 11-13
("spot_function_tellback", ctypes.c_uint16, 1), # bit 14
("rws_submode_tellback", ctypes.c_uint16, 1) # bit 15
]
class RdrFunAndParam1Tellback(ctypes.Union):
@ -88,20 +89,10 @@ class RdrFunAndParam1Tellback(ctypes.Union):
self.raw &= ~(0x03 << (15-11))
self.raw |= (value << (15-11))
# Bitfield accessors for scan_width
# Bitfield accessors for scan_width (bit 0-3)
def get_scan_width(self):
return AzimuthScanWidth((self.raw >> (15-13)) & 0x03)
return AzimuthScanWidth(self.raw & 0x0F)
def set_scan_width(self, value):
self.raw &= ~(0x03 << (15-13))
self.raw |= (value << (15-13))
# Bitfield accessors for velocity_scale
# Bitfield accessors for spare
def get_spare(self):
return (self.raw >> 0) & 0x01
def set_spare(self, value):
self.raw &= ~(0x01 << 0)
self.raw |= (value << 0)
self.raw &= ~0x0F
self.raw |= (value & 0x0F)

View File

@ -12,11 +12,10 @@ em.ENUM_MAP.update({
class _RdrFunAndParam2Str(ctypes.LittleEndianStructure):
_pack_ = 1
_fields_ = [
("spare", ctypes.c_uint16,8),
("sar_map_orientation", ctypes.c_uint16,2),
("zoom_command", ctypes.c_uint16,2),
("spare_2", ctypes.c_uint16,4),
("spare_8_15", ctypes.c_uint16, 8), # bit 0-7
("sar_map_orientation", ctypes.c_uint16, 2), # bit 8-9
("zoom_command", ctypes.c_uint16, 2), # bit 10-11
("spare_0_4", ctypes.c_uint16, 4), # bit 12-15
]
@ -34,21 +33,21 @@ class RdrFunAndParam2(ctypes.Union):
self.raw &= ~(0x0F << 12)
self.raw |= (value << 12)
# Bitfield accessors for zoom
# Bitfield accessors for zoom (bit 10-11)
def get_zoom(self):
return Zoom((self.raw >> 9) & 0x03)
return Zoom((self.raw >> 10) & 0x03)
def set_zoom(self, value):
self.raw &= ~(0x03 << 9)
self.raw |= (value << 9)
self.raw &= ~(0x03 << 10)
self.raw |= (value << 10)
# Bitfield accessors for sar_map_orientation
# Bitfield accessors for sar_map_orientation (bit 8-9)
def get_sar_map_orientation(self):
return SarMapOrientation((self.raw >> 7) & 0x03)
return SarMapOrientation((self.raw >> 8) & 0x03)
def set_sar_map_orientation(self, value):
self.raw &= ~(0x03 << 7)
self.raw |= (value << 7)
self.raw &= ~(0x03 << 8)
self.raw |= (value << 8)
# Bitfield accessors for spare_8_15
def get_spare_8_15(self):

View File

@ -1,15 +1,22 @@
import ctypes
from .enums import Zoom, SarMapOrientation
import Grifo_E_1553lib.data_types.enum_map as em
em.ENUM_MAP.update({
"zoom_tellback": Zoom,
"sar_map_orientation_tellback": SarMapOrientation,
})
class _RdrFunAndParam2TellbackStr(ctypes.LittleEndianStructure):
_pack_ = 1
_fields_ = [
("spare2", ctypes.c_uint16,6),
("reserved9", ctypes.c_uint16,1),
("reserved8", ctypes.c_uint16,1),
("reserved6", ctypes.c_uint16,2),
("reserved4", ctypes.c_uint16,2),
("spare", ctypes.c_uint16,4),
("spare6_15", ctypes.c_uint16, 6), # bit 0-5
("sar_spoi_feasibility", ctypes.c_uint16, 1), # bit 6
("sar_crs_feasibility", ctypes.c_uint16, 1), # bit 7
("sar_map_orientation_tellback", ctypes.c_uint16, 2), # bit 8-9
("zoom_tellback", ctypes.c_uint16, 2), # bit 10-11
("spare0_4", ctypes.c_uint16, 4), # bit 12-15
]
@ -18,3 +25,34 @@ class RdrFunAndParam2Tellback(ctypes.Union):
("raw", ctypes.c_uint16),
("str", _RdrFunAndParam2TellbackStr)
]
# Bitfield accessors for zoom tellback (bit 10-11)
def get_zoom_tellback(self):
return Zoom((self.raw >> 10) & 0x03)
def set_zoom_tellback(self, value):
self.raw &= ~(0x03 << 10)
self.raw |= (value << 10)
# Bitfield accessors for sar_map_orientation tellback (bit 8-9)
def get_sar_map_orientation_tellback(self):
return SarMapOrientation((self.raw >> 8) & 0x03)
def set_sar_map_orientation_tellback(self, value):
self.raw &= ~(0x03 << 8)
self.raw |= (value << 8)
# Bitfield accessors for SAR feasibility tellbacks
def get_sar_crs_feasibility(self):
return bool((self.raw >> 7) & 0x01)
def set_sar_crs_feasibility(self, value):
self.raw &= ~(0x01 << 7)
self.raw |= (int(bool(value)) << 7)
def get_sar_spoi_feasibility(self):
return bool((self.raw >> 6) & 0x01)
def set_sar_spoi_feasibility(self, value):
self.raw &= ~(0x01 << 6)
self.raw |= (int(bool(value)) << 6)

View File

@ -22,6 +22,8 @@ class _RdrModeCommandWordStr(ctypes.LittleEndianStructure):
("spare_0_1", ctypes.c_uint16, 2),
("sar_type", ctypes.c_uint16, 1),
("silence", ctypes.c_uint16, 1),
# reserved11: historically used for EMERGENCY command flag
# GUI mapping: legacy `emergency` -> this reserved11 bit
("reserved11", ctypes.c_uint16, 1),
("stop_powerup", ctypes.c_uint16, 1),
("freeze", ctypes.c_uint16, 1),
@ -77,9 +79,11 @@ class RdrModeCommandWord(ctypes.Union):
self.raw = (self.raw & ~(0x01 << 5)) | ((int(value) & 0x01) << 5)
def get_reserved11(self):
"""Return the reserved11 bit (EMERGENCY flag in some configs)."""
return (self.raw >> 4) & 0x01
def set_reserved11(self, value):
"""Set the reserved11 bit (EMERGENCY flag in some configs)."""
self.raw = (self.raw & ~(0x01 << 4)) | ((int(value) & 0x01) << 4)
def get_silence(self):
return SilenceSelection((self.raw >> 3) & 0x01)

View File

@ -88,9 +88,15 @@ class RdrStatusTellback(ctypes.Union):
# Bitfield accessors for reserved11
def get_reserved11(self):
"""Return the reserved11 bit.
Note: this bit is used as the EMERGENCY tellback flag in some configurations
(legacy GUI expects `emergency_tellback` -> this reserved11 bit).
"""
return (self.raw >> (15-11)) & 0x01
def set_reserved11(self, value):
"""Set the reserved11 bit (EMERGENCY tellback in some configs)."""
self.raw &= ~(0x01 << (15-11))
self.raw |= (value << (15-11))

View File

@ -0,0 +1,391 @@
# -*- coding: utf-8 -*-
"""
Simplified 1553 Debug View for embedding in main ARTOS GUI.
Shows only:
- Bus Monitor table (messages with stats)
- Details pane (selected message fields)
Uses existing BusMonitorCore instance from main application.
Logs to main GUI logger (no internal log widget).
"""
import tkinter as tk
from tkinter import ttk
import logging
import time
class DebugView(tk.Frame):
"""
Lightweight 1553 debug panel showing message table and details.
Designed to be embedded in a Toplevel, uses external BusMonitorCore.
"""
def __init__(self, parent, bus_monitor):
"""
Args:
parent: Tk parent widget
bus_monitor: BusMonitorCore instance (already initialized)
"""
super().__init__(parent)
self.pack(fill=tk.BOTH, expand=True)
self.bus_monitor = bus_monitor
self.logger = logging.getLogger('ARTOS.1553Debug')
# Cache for tree items and message wrappers
self._tree_items = {}
self._current_selected_label = None
self._create_widgets()
self._start_update_loop()
self.logger.info("1553 Debug View initialized")
def _create_widgets(self):
"""Create Bus Monitor table and Details pane."""
# Top controls
controls = tk.Frame(self)
controls.pack(side=tk.TOP, fill=tk.X, padx=6, pady=6)
tk.Label(controls, text="1553 Bus Monitor - Real-time", font=('Arial', 10, 'bold')).pack(side=tk.LEFT, padx=10)
refresh_btn = tk.Button(controls, text="Refresh", command=self._refresh_table)
refresh_btn.pack(side=tk.RIGHT, padx=4)
# Main content: table (left) + details (right)
content = tk.Frame(self)
content.pack(side=tk.TOP, fill=tk.BOTH, expand=True, padx=6, pady=2)
# Left: Bus Monitor table
table_frame = tk.LabelFrame(content, text="Bus Monitor")
table_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(0, 6))
columns = ("label", "cw", "sw", "num", "errs", "period", "mc")
self.tree = ttk.Treeview(table_frame, columns=columns, show="headings", height=20)
self.tree.heading("label", text="Name")
self.tree.heading("cw", text="RT-SA-WC-T/R")
self.tree.heading("sw", text="SW")
self.tree.heading("num", text="Num")
self.tree.heading("errs", text="Errs")
self.tree.heading("period", text="period")
self.tree.heading("mc", text="MC")
self.tree.column("label", width=120)
self.tree.column("cw", width=120)
self.tree.column("sw", width=60)
self.tree.column("num", width=60)
self.tree.column("errs", width=60)
self.tree.column("period", width=80)
self.tree.column("mc", width=60)
scrollbar = ttk.Scrollbar(table_frame, orient=tk.VERTICAL, command=self.tree.yview)
self.tree.configure(yscrollcommand=scrollbar.set)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
self.tree.pack(fill=tk.BOTH, expand=True, padx=6, pady=6)
self.tree.bind('<<TreeviewSelect>>', self._on_tree_select)
# Right: Details pane
details_frame = tk.LabelFrame(content, text="Message Details")
details_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=False, padx=6)
detail_columns = ("param", "value")
self.detail_tree = ttk.Treeview(details_frame, columns=detail_columns, show="headings", height=20)
self.detail_tree.heading("param", text="Parameter")
self.detail_tree.heading("value", text="Value")
self.detail_tree.column("param", width=200)
self.detail_tree.column("value", width=150)
detail_scroll = ttk.Scrollbar(details_frame, orient=tk.VERTICAL, command=self.detail_tree.yview)
self.detail_tree.configure(yscrollcommand=detail_scroll.set)
detail_scroll.pack(side=tk.RIGHT, fill=tk.Y)
self.detail_tree.pack(fill=tk.BOTH, expand=True, padx=6, pady=6)
def _get_messagedb(self):
"""Get MessageDB from BusMonitorCore."""
if self.bus_monitor is None:
return None
return getattr(self.bus_monitor, '_messagedb', None)
def _refresh_table(self):
"""Refresh the message table from MessageDB."""
messagedb = self._get_messagedb()
if not messagedb:
self.logger.warning("MessageDB not available")
return
try:
all_messages = messagedb.getAllMessages()
for label, wrapper in all_messages.items():
try:
# Use the same attributes as MonitorApp for compatibility
# CW from head.cw.raw
try:
cw_raw = getattr(wrapper.head.cw, 'raw', None)
except Exception:
cw_raw = None
# SW from head.sw
try:
sw = getattr(wrapper.head, 'sw', 0)
except Exception:
sw = 0
# Num = sent_count (or size as fallback)
num = getattr(wrapper, 'sent_count', getattr(wrapper, 'size', 0))
# Errs from head.errcode
try:
errs = getattr(wrapper.head, 'errcode', 0)
except Exception:
errs = 0
# Period from _time_ms or calculated from freq
period_ms = getattr(wrapper, '_time_ms', None)
if period_ms is None:
freq = getattr(wrapper, 'freq', None)
if freq and freq > 0:
period_ms = 1000.0 / freq
else:
period_ms = 0
# MC = recv_count
mc = getattr(wrapper, 'recv_count', 0)
# Format CW as RT-SA-WC-T/R
try:
rt = getattr(wrapper.head.cw.str, 'rt', 0)
sa = getattr(wrapper.head.cw.str, 'sa', 0)
wc = getattr(wrapper.head.cw.str, 'wc', 0)
tr = getattr(wrapper.head.cw.str, 'tr', 0)
tr_str = "T" if tr == 0 else "R"
cw_str = f"{rt:02d}-{sa:02d}-{wc:02d}-{tr_str}"
except Exception:
cw_str = "---"
values = (
label,
cw_str,
f"{sw}",
f"{num}",
f"{errs}",
f"{period_ms:.1f}ms" if period_ms > 0 else "---",
f"{mc}"
)
# Update or insert
if label in self._tree_items:
item_id = self._tree_items[label]
self.tree.item(item_id, values=values)
else:
item_id = self.tree.insert('', tk.END, values=values)
self._tree_items[label] = item_id
except Exception as e:
self.logger.debug(f"Error updating row for {label}: {e}")
except Exception as e:
self.logger.error(f"Error refreshing table: {e}")
def _on_tree_select(self, event):
"""Handle tree selection - show message details."""
selection = self.tree.selection()
if not selection:
return
item_id = selection[0]
values = self.tree.item(item_id, 'values')
if not values:
return
label = values[0]
self._current_selected_label = label
self._update_details(label)
def _update_details(self, label):
"""Show details for selected message - display payload fields with raw and enum values."""
# Clear existing details
for item in self.detail_tree.get_children():
self.detail_tree.delete(item)
messagedb = self._get_messagedb()
if not messagedb:
return
try:
all_messages = messagedb.getAllMessages()
if label not in all_messages:
return
wrapper = all_messages[label]
# Show message label header
self._add_detail_row("=== Message ===", label)
# Show message payload fields with raw and enum values
try:
msg = wrapper.message
if msg:
self._extract_message_fields(msg)
else:
self._add_detail_row("No payload data", "---")
except Exception as e:
self._add_detail_row("Error reading payload", str(e))
except Exception as e:
self.logger.error(f"Error updating details for {label}: {e}")
def _get_enum_items(self, class_name):
"""Return list of (name, value) tuples for enum class, or None."""
try:
from .monitor_helpers import get_enum_items
return get_enum_items(class_name)
except Exception:
return None
def _get_enum_for_field(self, field_name):
"""Return enum class for field_name from ENUM_MAP, or None."""
try:
import Grifo_E_1553lib.data_types.enum_map as em
return em.ENUM_MAP.get(field_name)
except Exception:
try:
import pybusmonitor1553.Grifo_E_1553lib.data_types.enum_map as em
return em.ENUM_MAP.get(field_name)
except Exception:
return None
def _is_struct_like(self, v):
"""Check if value is a struct-like object - same logic as monitor.py."""
if v is None:
return False
if isinstance(v, (int, float, str, bytes)):
return False
if callable(v):
return False
# ctypes.Structure expose _fields_
if hasattr(v, '_fields_'):
return True
# objects with many public attributes are likely struct-like
public = [n for n in dir(v) if not n.startswith('_')]
if len(public) > 2:
return True
return False
def _extract_message_fields(self, obj, prefix="", _depth=0, max_depth=5):
"""Recursively extract fields from ctypes message structure - mimics monitor.py formatting."""
if _depth >= max_depth:
return
try:
# Iterate over public attributes like monitor.py does
for name in [n for n in dir(obj) if not n.startswith('_')]:
try:
val = getattr(obj, name)
except Exception:
continue
if callable(val):
continue
full_name = f"{prefix}.{name}" if prefix else name
# Special-case: ctypes Union with a '.str' structure (bitfields)
if hasattr(val, 'str') and self._is_struct_like(getattr(val, 'str')):
# Show structure header
indent = " " * _depth
self._add_detail_row(f"{indent}{name}", "")
# Recurse into the .str sub-structure
try:
self._extract_message_fields(getattr(val, 'str'), prefix=f"{full_name}.str", _depth=_depth+1, max_depth=max_depth)
except Exception:
pass
continue
# Group nested structs (ctypes structs or objects with many public attrs)
if self._is_struct_like(val) and not (hasattr(val, 'raw') or hasattr(val, 'value')):
# Show structure header
indent = " " * _depth
self._add_detail_row(f"{indent}{name}", "")
# Recurse
self._extract_message_fields(val, prefix=full_name, _depth=_depth+1, max_depth=max_depth)
continue
# Scalar value or enum - show with proper formatting
indent = " " * (_depth + 1)
# Get raw value
if hasattr(val, 'value'):
raw_val = val.value
elif hasattr(val, 'raw'):
raw_val = val.raw
else:
raw_val = val
# Try to get enum representation - use ENUM_MAP first (by field name)
enum_items = None
try:
# Try ENUM_MAP first using field name
enum_cls = self._get_enum_for_field(name)
if enum_cls:
enum_items = [(m.name, m.value) for m in enum_cls]
else:
# Fallback to class-based lookup
enum_items = self._get_enum_items(val.__class__.__name__)
except Exception:
pass
# Format the value display like monitor.py: "ENUM_NAME (value)" or just value
if enum_items:
try:
raw = int(raw_val)
# Find matching enum name
enum_name = None
for (n, v) in enum_items:
if int(v) == raw:
enum_name = n
break
if enum_name:
val_str = f"{enum_name} ({raw})"
else:
val_str = str(raw)
except Exception:
val_str = str(raw_val)
else:
# No enum, just show the value
val_str = str(raw_val)
self._add_detail_row(f"{indent}{name}", val_str)
except Exception as e:
self.logger.debug(f"Error extracting fields: {e}")
def _add_detail_row(self, param, value):
"""Add a row to details tree."""
try:
self.detail_tree.insert('', tk.END, values=(param, value))
except Exception:
pass
def _start_update_loop(self):
"""Start periodic update loop."""
self._update_active = True
self._schedule_update()
def _schedule_update(self):
"""Schedule next update."""
if self._update_active:
self._refresh_table()
# If a message is selected, refresh its details
if self._current_selected_label:
self._update_details(self._current_selected_label)
# Schedule next update (500ms)
self.after(500, self._schedule_update)
def stop(self):
"""Stop update loop."""
self._update_active = False

View File

@ -51,15 +51,24 @@ except Exception:
class MonitorApp(tk.Frame):
def __init__(self, master=None):
def __init__(self, master=None, bus_monitor=None, use_main_logger=False):
"""MonitorApp GUI.
Args:
master: Tk parent
bus_monitor: Optional existing BusMonitorCore instance to monitor
use_main_logger: If True, do not initialize the local TkinterLogger
and instead use the main application's logging.
"""
super().__init__(master)
self.master = master
self.pack(fill=tk.BOTH, expand=True)
# Use BusMonitorCore (ARTOS API) - same as ARTOS Collector will use
# The application will remain idle until the user presses `Initialize`.
self.bus_monitor = None
# Use an existing BusMonitorCore instance if provided (preferred)
self.bus_monitor = bus_monitor
self.import_error = None
# If True, avoid creating a local TkinterLogger (use main GUI logger)
self._use_main_logger = bool(use_main_logger)
self.create_widgets()
self.update_loop_running = False
# cache tree item ids by message label for incremental updates
@ -170,9 +179,14 @@ class MonitorApp(tk.Frame):
# Status bar will be added below the whole UI (outside all frames)
# Initialize external TkinterLogger if available, attach text widget as handler
# Initialize logging. If we're embedded in the main GUI, use its logger
# (prevents creating a second TkinterLogger and duplicate GUI handlers).
self.logger_system = None
try:
if getattr(self, '_use_main_logger', False):
# Use main application's ARTOS logger so logs go to main GUI
self.logger = logging.getLogger('ARTOS')
else:
if TkinterLogger is not None:
self.logger_system = TkinterLogger(self.master)
self.logger_system.setup(enable_console=True, enable_tkinter=True)
@ -467,6 +481,11 @@ class MonitorApp(tk.Frame):
row_fr.pack(fill=tk.X, padx=2, pady=1)
lbl = tk.Label(row_fr, text=name, width=28, anchor=tk.W)
lbl.pack(side=tk.LEFT)
# attach tooltip to the label itself (description will be added later for widgets)
try:
from .monitor_helpers import UNSET_TEXT
except Exception:
UNSET_TEXT = '<unset>'
# determine per-field editability: make mission date/time readonly
root_field = full.split('.')[0]
readonly_roots = ('date_of_mission', 'time_of_mission')
@ -510,6 +529,21 @@ class MonitorApp(tk.Frame):
if editable:
cb.bind('<<ComboboxSelected>>', lambda e, path=full: self._on_field_changed(path))
cb.pack(side=tk.RIGHT, fill=tk.X, expand=True)
# attach tooltip showing message-field and unit when available
try:
msg_label = getattr(self, 'current_form_label', '')
unit = None
try:
unit = self._find_unit_for_field(msg_label, name)
except Exception:
unit = None
tt = f"{msg_label} - {name}"
if unit:
tt = f"{tt} ({unit})"
from pymsc.gui.components.base_widgets import ToolTip
ToolTip(cb, tt)
except Exception:
pass
widget = ('combobox', cb, enum_items)
else:
# numeric or text entry
@ -536,6 +570,21 @@ class MonitorApp(tk.Frame):
ent.bind('<Return>', lambda e, path=full: self._on_entry_finished(path))
ent.bind('<FocusOut>', lambda e, path=full: self._on_entry_finished(path))
ent.pack(side=tk.RIGHT, fill=tk.X, expand=True)
# attach tooltip showing message-field and unit when available
try:
msg_label = getattr(self, 'current_form_label', '')
unit = None
try:
unit = self._find_unit_for_field(msg_label, name)
except Exception:
unit = None
tt = f"{msg_label} - {name}"
if unit:
tt = f"{tt} ({unit})"
from pymsc.gui.components.base_widgets import ToolTip
ToolTip(ent, tt)
except Exception:
pass
widget = ('entry', ent)
# register created widget for later updates; values will be filled
@ -545,6 +594,69 @@ class MonitorApp(tk.Frame):
except Exception:
pass
def _find_unit_for_field(self, message_label: str, field_name: str):
"""Find unit string for given message label and field by inspecting command registry."""
try:
import pymsc.gui.command_registry as reg
from pymsc.utils import converters as conv
except Exception:
return None
lists = [
getattr(reg, 'CHECKBOXES', []),
getattr(reg, 'COMBOBOXES', []),
getattr(reg, 'SPINBOXES', []),
getattr(reg, 'LIST_CONTROLS', []),
getattr(reg, 'LABELS', [])
]
for lst in lists:
for item in lst:
# check up to 3 message/field pairs
for i in (1, 2, 3):
mkey = f"message{i}" if f"message{i}" in item else ('message' if 'message' in item and i == 1 else None)
fkey = f"field{i}" if f"field{i}" in item else ('field' if 'field' in item and i == 1 else None)
if not mkey or not fkey:
continue
msgobj = item.get(mkey)
fieldreg = item.get(fkey)
if not msgobj or not fieldreg:
continue
try:
mid = getattr(msgobj, 'message_id', None)
except Exception:
mid = None
if mid != message_label:
continue
# compare field names by suffix
try:
if isinstance(fieldreg, str) and (fieldreg.lower().endswith(field_name.lower()) or fieldreg.lower().endswith('_' + field_name.lower())):
# found matching registry item - determine unit based on lsb/scale
# look for lsb/scale keys
lsb = item.get(f"lsb{i}", item.get('lsb'))
scale = item.get(f"scale{i}", item.get('scale', 1))
# map known scales to units
if scale == getattr(conv, 'RAD_TO_DEG', None):
return 'deg'
if scale == getattr(conv, 'MS_TO_KNOTS', None):
return 'kts'
if scale == getattr(conv, 'METERS_TO_FEET', None):
return 'ft'
# check lsb-based heuristics
if lsb == getattr(conv, 'SEMICIRCLE_RAD_LSB', None) or lsb == getattr(conv, 'SEMICIRCLE_LSB', None):
return 'deg' if scale == getattr(conv, 'RAD_TO_DEG', None) else 'rad'
if lsb == getattr(conv, 'GEOPOS_RAD_LSB', None):
return 'deg'
if lsb == getattr(conv, 'CRS_SLAVE_RNG_METERS_LSB', None) or 'range' in field_name.lower() or 'lat' in field_name.lower() or 'lon' in field_name.lower():
# assume distance - if scale converts to feet, show ft otherwise meters
if scale == getattr(conv, 'METERS_TO_FEET', None):
return 'ft'
return 'm'
return None
except Exception:
continue
return None
def _on_entry_focus_in(self, field_path):
"""Mark entry as being edited to prevent refresh from overwriting user input."""
try:

View File

@ -60,10 +60,11 @@ A2_FIELD_MAP = {
'PWR_UP_STOP_FUNCT_SEL': 'rdr_mode_command.stop_powerup',
'silence': 'rdr_mode_command.silence',
'SAR_ACQUISITION': 'rdr_mode_command.sar_type',
# 'emergency': 'param2.emergency', # DISABLED - field doesn't exist in param2
# emergency command -> reserved11 in rdr_mode_command (bit present)
'emergency': 'rdr_mode_command.reserved11',
# RdrFunAndParam1 fields (param1.field_name)
# Note: These use getter/setter methods, not direct attributes
# Note: These use getter/setter methods automatically via get_{field}()
'rws_submode_command': 'param1.rws_submode',
'SPOT_FUNC_SEL': 'param1.spot',
'acm_submode_command': 'param1.acm_submode',
@ -186,12 +187,12 @@ A3_FIELD_MAP = {
# ============================================================================
A4_FIELD_MAP = {
# Validity and slew
# Note: SAR/Ghost/DTT fields may not exist in A4ValidityAndSlew - checking available getters
# Available: ant_slew_valid, attitude_invalid, baro_intertial_altitude_invalid, cas_invalid, etc.
# These fields might be in a different message or not implemented yet
# 'SAR_ENABLED': 'validity_and_slew.spare1', # DISABLED - field not found
# 'NORM_GHOST_SELECTION': 'validity_and_slew.spare2', # DISABLED - field not found
# 'DTT_ENABLED': 'validity_and_slew.spare3', # DISABLED - field not found
# Note: SAR/Ghost/DTT selectors exist inside the cursor acquisition union
# (`acq_crs_x` -> `CrsMotionX.str`). Map legacy flat names to the nested
# path so GUI controls can access the correct bitfields.
'SAR_ENABLED': 'acq_crs_x.str.sar_enabled_selector',
'NORM_GHOST_SELECTION': 'acq_crs_x.str.normal_ghost_selector',
'DTT_ENABLED': 'acq_crs_x.str.mtt_enabled_selector',
'CLEARANCE_PLANE_DIST': 'clearance_plane_distance',
# Timetag
@ -330,6 +331,10 @@ B6_FIELD_MAP = {
'crs_x_tellback': 'cursor_x_display_coord_qual.current_x_display_coord',
'crs_y_tellback': 'cursor_y_display_coord.current_y_display_coord',
# SAR enabled tellback lives in the cursor X coord qualifier (B6 W16)
# map legacy tellback name to the nested bitfield
'normal_sar_enabled_tellback': 'cursor_x_display_coord_qual.cursor_normal_slave_selector_tellback',
# Param tellback
'PARAM_ID_TB': 'param_id_tellback',
'PARAM_TRANSF_TB': 'param_id_tellback.parameter_transfer',
@ -349,25 +354,34 @@ B7_FIELD_MAP = {
'stby_tellback': 'rdr_mode_tellback.stby',
'freeze_tellback': 'rdr_mode_tellback.freeze',
'rf_status': 'rdr_mode_tellback.rf_radiation',
# Note: emergency field does not exist in RdrFunAndParam2Tellback
# emergency tellback is stored in reserved11 of rdr_mode_tellback
'emergency_tellback': 'rdr_mode_tellback.reserved11',
# Status flags
'TRANSITION': 'rdr_mode_tellback.transition_status',
'LAST_ACQ_FAIL': 'rdr_mode_tellback.last_acq_result',
'DEGRADED_PERF_STATUS': 'rdr_mode_tellback.spare', # Field not found, using spare
# Function parameters tellback
'rws_submode_tellback': 'param1_tellback.rws_submode',
'acm_submode_tellback': 'param1_tellback.acm_submode',
'gm_submode_tellback': 'param1_tellback.gm_submode',
'range_scale_tellback': 'param1_tellback.range_scale',
'bars_tellback': 'param1_tellback.bars_num',
'scan_width_tellback': 'param1_tellback.scan_width',
# Note: velocity_scale field does not exist in RdrFunAndParam1Tellback
'velocity_scale_tellback': 'param1_tellback.spare', # Field not found
# Function parameters tellback - use .str for direct bitfield access
'rws_submode_tellback': 'param1_tellback.str.rws_submode_tellback',
'spot_function_tellback': 'param1_tellback.str.spot_function_tellback',
'acm_submode_tellback': 'param1_tellback.str.acm_submode_tellback',
'gm_submode_tellback': 'param1_tellback.str.gm_submode_tellback',
'expand_tellback': 'param1_tellback.str.expand_tellback',
'range_scale_tellback': 'param1_tellback.str.range_scale_tellback',
'bars_tellback': 'param1_tellback.str.number_of_bars_tellback',
'scan_width_tellback': 'param1_tellback.str.azimuth_scan_width_tellback',
# Note: velocity_scale and range_scale share the same bits in the tellback union
'velocity_scale_tellback': 'param1_tellback.str.range_scale_tellback',
# SAR
'SAR_ACQUISITION_TB': 'rdr_mode_tellback.sar_type',
# Param2 tellbacks (B7 W3) - use .str for direct bitfield access
'zoom_tellback': 'param2_tellback.str.zoom_tellback',
'sar_map_orientation_tellback': 'param2_tellback.str.sar_map_orientation_tellback',
# SAR feasibility flags (B7 W3 bits)
'sar_crs_feasibility': 'param2_tellback.str.sar_crs_feasibility',
'sar_spoi_feasibility': 'param2_tellback.str.sar_spoi_feasibility',
}
# ============================================================================

View File

@ -33,25 +33,41 @@ CHECKBOXES = [
"label": "IBIT", "description": "toggle IBIT status", "param": "0"
},
{
"command": "sar_enabled", "message": None, "field": "SAR_ENABLED", "tooltip": "A4 SAR_ENABLED",
"message_tb": None, "field_tb": "", "label": "SAR enabled", "description": "toggle SAR enabled [DISABLED - field not in 1553]", "param": "0"
"command": "sar_enabled", "message": msg_a4, "field": "SAR_ENABLED", "tooltip": "A4 SAR_ENABLED",
"message_tb": msg_b6, "field_tb": "normal_sar_enabled_tellback", "tooltip_tb": "B6 SAR_TB",
"label": "SAR enabled", "description": "toggle SAR enabled", "param": "0"
},
{
"command": "ghost_enabled", "message": None, "field": "NORM_GHOST_SELECTION", "tooltip": "A4 Ghost",
"message_tb": None, "field_tb": "", "label": "Ghost enabled", "description": "toggle ghost enabled [DISABLED - field not in 1553]", "param": "0"
"command": "sar_crs_feas", "message": None, "field": "", "tooltip": "",
"message_tb": msg_b7, "field_tb": "sar_crs_feasibility", "tooltip_tb": "B7 CRS_FEAS",
"label": "CRS Feas.", "description": "Cursor CRS feasibility tellback", "param": "0"
},
{
"command": "dtt_enabled", "message": None, "field": "DTT_ENABLED", "tooltip": "A4 DTT",
"message_tb": None, "field_tb": "", "label": "DTT enabled", "description": "toggle DTT enabled [DISABLED - field not in 1553]", "param": "0"
"command": "sar_spoi_feas", "message": None, "field": "", "tooltip": "",
"message_tb": msg_b7, "field_tb": "sar_spoi_feasibility", "tooltip_tb": "B7 SPOI_FEAS",
"label": "Spoi Feas.", "description": "Cursor SPOI feasibility tellback", "param": "0"
},
{
"command": "ghost_enabled", "message": msg_a4, "field": "NORM_GHOST_SELECTION", "tooltip": "A4 Ghost",
"message_tb": None, "field_tb": "", "label": "Ghost enabled", "description": "toggle ghost enabled", "param": "0"
},
{
"command": "dtt_enabled", "message": msg_a4, "field": "DTT_ENABLED", "tooltip": "A4 DTT",
"message_tb": None, "field_tb": "", "label": "DTT enabled", "description": "toggle DTT enabled", "param": "0"
},
{
"command": "ale_blanking", "message": msg_a1, "field": "ALE_BLANKING", "tooltip": "A1 ALE",
"message_tb": None, "field_tb": "", "label": "ALE blanking", "description": "toggle ALE blanking", "param": "0"
},
{
"command": "emergency", "message": None, "field": "emergency", "tooltip": "A2 emergency",
"message_tb": None, "field_tb": "emergency_tellback", "tooltip_tb": "B7 emergency_tb",
"label": "EMERGENCY", "description": "toggle EMERGENCY status [DISABLED - field not in 1553]", "param": "0"
"command": "spot", "message": msg_a2, "field": "SPOT_FUNC_SEL", "tooltip": "A2 SPOT",
"message_tb": msg_b7, "field_tb": "spot_function_tellback", "tooltip_tb": "B7 SPOT_TB",
"label": "Spot", "description": "toggle Spot function", "param": "0"
},
{
"command": "emergency", "message": msg_a2, "field": "emergency", "tooltip": "A2 emergency",
"message_tb": msg_b7, "field_tb": "emergency_tellback", "tooltip_tb": "B7 emergency_tb",
"label": "EMERGENCY", "description": "toggle EMERGENCY status", "param": "0"
},
{
"command": "deg_perf", "message": None, "field": "", "message_tb": msg_b7,
@ -135,18 +151,21 @@ COMBOBOXES = [
},
{
"command": "expand", "message": msg_a2, "field": "EXPAND", "tooltip": "A2 EXPAND",
"message_tb": None, "field_tb": "", "label": "Expand", "description": "Select Expand",
"values": ["NORMAL", "EXPAND", "EXP_NV2", "EXP_NV3"]
"message_tb": msg_b7, "field_tb": "expand_tellback", "label": "Expand", "description": "Select Expand",
"values": ["EXP_NORMAL", "EXPAND", "EXP_SPARE2", "EXP_SPARE3"],
"values_tb": ["EXP_NORMAL", "EXPAND", "EXP_SPARE2", "EXP_SPARE3"]
},
{
"command": "zoom", "message": msg_a2, "field": "ZOOM_COMMAND", "tooltip": "A2 ZOOM",
"message_tb": None, "field_tb": "", "label": "Zoom", "description": "Select Zoom",
"values": ["ZOOM_NOT_ATIVE", "ZOOM_IN", "ZOOM_OUT", "ZOOM_SPARE"]
"message_tb": msg_b7, "field_tb": "zoom_tellback", "label": "Zoom", "description": "Select Zoom",
"values": ["ZOOM_NOT_ATIVE", "ZOOM_IN", "ZOOM_OUT", "ZOOM_SPARE"],
"values_tb": ["ZOOM_NOT_ATIVE", "ZOOM_IN", "ZOOM_OUT", "ZOOM_SPARE"]
},
{
"command": "sar_map_orientation", "message": msg_a2, "field": "SAR_MAP_ORIENTATION", "tooltip": "A2 SAR MAP",
"message_tb": None, "field_tb": "", "label": "Sar map orient.", "description": "Select Sar map orientation",
"values": ["SAR_AC_NOSE_REFERENCE", "SAR_SLANT_CROSS_RANGE", "NOT_USED_1", "NOT_USED_2"]
"message_tb": msg_b7, "field_tb": "sar_map_orientation_tellback", "label": "Sar map orient.", "description": "Select Sar map orientation",
"values": ["SAR_AC_NOSE_REFERENCE", "SAR_SLANT_CROSS_RANGE", "NOT_USED_1", "NOT_USED_2"],
"values_tb": ["SAR_AC_NOSE_REFERENCE", "SAR_SLANT_CROSS_RANGE", "NOT_USED_1", "NOT_USED_2"]
},
{
"command": "ground_tgt_reject", "message": msg_a1, "field": "GND_TGT_REJ_RAD_VEL", "tooltip": "A1 GND REJ",
@ -163,11 +182,6 @@ COMBOBOXES = [
"message_tb": None, "field_tb": "", "label": "Power-up stop", "description": "Select Power-up stop",
"values": ["NORMAL", "STOP"]
},
{
"command": "spot", "message": msg_a2, "field": "SPOT_FUNC_SEL", "tooltip": "A2 SPOT",
"message_tb": None, "field_tb": "", "label": "Spot", "description": "Select Spot function",
"values": ["NORMAL", "STOP"]
},
{
"command": "alt_block", "message": msg_a1, "field": "alt_block", "tooltip": "A1 alt_block",
"message_tb": msg_b6, "field_tb": "alt_block_tellback", "tooltip_tb": "B6 alt_block_tb",
@ -432,7 +446,7 @@ LIST_CONTROLS = [
"message2": msg_a4, "field2": "a4_radio_altimeter_invalid", "type2": "checkbox", "label2": "inv", "tooltip2": "inv"
},
{
"command": "attitude", "label": "Invalid", "description": "Set attitude invalid",
"command": "attitude", "label": "Attitude invalid", "description": "Set attitude invalid",
"message1": msg_a4, "field1": "ATTITUDE_INVALID", "type1": "checkbox", "tooltip1": "A4 ATT INV"
},
{

View File

@ -11,6 +11,44 @@ from pymsc.utils.converters import (
FEET_TO_METERS, MS_TO_KNOTS, KNOTS_TO_MS
)
def _infer_unit_from_info(info: Dict, idx: int):
"""Infer a short unit string from a command registry `info` dict.
Looks for `lsb{idx}` / `scale{idx}` or fallback to `lsb`/`scale`.
Returns unit like 'deg', 'rad', 'kts', 'm', 'ft' or None.
"""
try:
suffix = '' if idx == -1 or idx == 1 else str(idx)
lsb = info.get(f'lsb{suffix}', info.get('lsb'))
scale = info.get(f'scale{suffix}', info.get('scale'))
# Angle in degrees if scale==RAD_TO_DEG
if scale == RAD_TO_DEG:
return 'deg'
# Speed in knots
if scale == MS_TO_KNOTS:
return 'kts'
# Feet/meter mapping
if scale == METERS_TO_FEET:
return 'ft'
# heuristics based on lsb
from pymsc.utils import converters as conv
if lsb in (getattr(conv, 'SEMICIRCLE_RAD_LSB', None), getattr(conv, 'SEMICIRCLE_LSB', None)):
# if a scale converts to degrees, label deg, else rad
if scale == RAD_TO_DEG:
return 'deg'
return 'rad'
if lsb == getattr(conv, 'GEOPOS_RAD_LSB', None):
return 'deg'
if lsb in (getattr(conv, 'CRS_SLAVE_RNG_METERS_LSB', None), getattr(conv, 'VELOCITY_METERS_LSB', None)):
# distance or velocity in meters -> show 'm' or 'ft' depending on scale
if scale == METERS_TO_FEET:
return 'ft'
return 'm'
except Exception:
return None
return None
# Registry for finding widgets by their label (used for automation/scripts)
WIDGET_MAP: Dict[str, Any] = {}
@ -61,6 +99,20 @@ class CommandFrameCheckBox(BaseCommandFrame):
width=self.column_widths[1]
)
self.command_check.grid(row=0, column=1)
# attach tooltip on the interactive control: "<message_id> - <field>"
try:
msg_obj = self.info.get('message')
if msg_obj and hasattr(msg_obj, 'message_id'):
tt = f"{msg_obj.message_id} - {self.info.get('field','')}"
# append short unit to tooltip for editable control
unit = _infer_unit_from_info(self.info, 1)
if unit:
tt = f"{tt} ({unit})"
else:
tt = self.info.get('tooltip', self.info.get('description',''))
ToolTip(self.command_check, tt)
except Exception:
pass
initial_val = self.info['message'].get_value_for_field(self.info['field'])
self.command_var.set(bool(initial_val))
@ -71,6 +123,20 @@ class CommandFrameCheckBox(BaseCommandFrame):
width=self.column_widths[2]
)
self.tellback_check.grid(row=0, column=2)
# attach tooltip to tellback showing its source message-field (B-message)
try:
msg_tb = self.info.get('message_tb')
field_tb = self.info.get('field_tb')
if msg_tb and hasattr(msg_tb, 'message_id') and field_tb:
tt_tb = f"{msg_tb.message_id} - {field_tb}"
unit_tb = _infer_unit_from_info(self.info, -1)
if unit_tb:
tt_tb = f"{tt_tb} ({unit_tb})"
else:
tt_tb = self.info.get('tooltip_tb', self.info.get('description',''))
ToolTip(self.tellback_check, tt_tb)
except Exception:
pass
def on_toggle(self):
# Skip if message is None (disabled field)
@ -137,6 +203,16 @@ class CommandFrameComboBox(BaseCommandFrame):
)
self.combo.grid(row=0, column=1)
self.combo.bind("<<ComboboxSelected>>", self.on_select)
# attach tooltip to combobox showing message-field when possible
try:
msg_obj = self.info.get('message')
if msg_obj and hasattr(msg_obj, 'message_id'):
tt = f"{msg_obj.message_id} - {self.info.get('field','')}"
else:
tt = self.info.get('tooltip', self.info.get('description',''))
ToolTip(self.combo, tt)
except Exception:
pass
if self.info.get('message_tb'):
self.tb_var = tk.StringVar(value="---")
@ -145,6 +221,20 @@ class CommandFrameComboBox(BaseCommandFrame):
width=self.column_widths[2], relief="sunken"
)
self.tb_label.grid(row=0, column=2)
# tooltip for tellback cell: show B-message.field mapping
try:
msg_tb = self.info.get('message_tb')
field_tb = self.info.get('field_tb')
if msg_tb and hasattr(msg_tb, 'message_id') and field_tb:
tt_tb = f"{msg_tb.message_id} - {field_tb}"
unit_tb = _infer_unit_from_info(self.info, -1)
if unit_tb:
tt_tb = f"{tt_tb} ({unit_tb})"
else:
tt_tb = self.info.get('tooltip_tb', self.info.get('description',''))
ToolTip(self.tb_label, tt_tb)
except Exception:
pass
def on_select(self, event=None):
self.start_time = time.time()
@ -171,6 +261,11 @@ class CommandFrameComboBox(BaseCommandFrame):
val_tb_text = self.info["values_tb"][idx_tb]
except (IndexError, KeyError, TypeError):
val_tb_text = "ERR"
# append unit if available (combobox tellbacks are typically enums, but keep flexible)
unit = _infer_unit_from_info(self.info, -1)
if unit:
self.tb_var.set(f"{val_tb_text} ({unit})")
else:
self.tb_var.set(val_tb_text)
elapsed = (time.time() - self.start_time) * 1000
if idx_tb == self.combo.current():
@ -242,6 +337,17 @@ class CommandFrameSpinBox(BaseCommandFrame):
# Also use variable trace to detect arrow changes
self.cmd_var.trace_add("write", self._on_var_change)
# attach tooltip to spinbox showing message-field when possible
try:
msg_obj = self.info.get('message')
if msg_obj and hasattr(msg_obj, 'message_id'):
tt = f"{msg_obj.message_id} - {self.info.get('field','')}"
else:
tt = self.info.get('tooltip', self.info.get('description',''))
ToolTip(self.spin, tt)
except Exception:
pass
if self.info.get('message_tb'):
self.tb_var = tk.StringVar(value="0")
self.tb_label = tk.Label(
@ -249,6 +355,20 @@ class CommandFrameSpinBox(BaseCommandFrame):
width=self.column_widths[2], relief="sunken"
)
self.tb_label.grid(row=0, column=2)
# tooltip for spinbox tellback cell
try:
msg_tb = self.info.get('message_tb')
field_tb = self.info.get('field_tb')
if msg_tb and hasattr(msg_tb, 'message_id') and field_tb:
tt_tb = f"{msg_tb.message_id} - {field_tb}"
unit_tb = _infer_unit_from_info(self.info, -1)
if unit_tb:
tt_tb = f"{tt_tb} ({unit_tb})"
else:
tt_tb = self.info.get('tooltip_tb', self.info.get('description',''))
ToolTip(self.tb_label, tt_tb)
except Exception:
pass
def _validate(self, p):
if p == "" or p == "-":
@ -335,7 +455,16 @@ class CommandFrameSpinBox(BaseCommandFrame):
raw_tb = msg_tb.get_value_for_field(self.info['field_tb'])
readable_tb = get_correct_value(self.info, -1, raw_tb)
fmt = "%.0f" if self.info.get("number_format") == "integer" else "%.4f"
self.tb_var.set(fmt % readable_tb)
# append unit indication
unit = _infer_unit_from_info(self.info, -1)
try:
txt = fmt % readable_tb
except Exception:
txt = str(readable_tb)
if unit:
self.tb_var.set(f"{txt} ({unit})")
else:
self.tb_var.set(txt)
elapsed = (time.time() - self.start_time) * 1000
target = float(self.cmd_var.get())
if abs(readable_tb - target) < 0.0001:
@ -412,7 +541,16 @@ class CommandFrameLabels(tk.Frame):
raw = msg.get_value_for_field(field)
val = get_correct_value(self.info, i, raw)
if val is not None:
self.vars[field].set("%.4f" % val)
fmt = "%.0f" if self.info.get(f"number_format{i}") == "integer" else "%.4f"
unit = _infer_unit_from_info(self.info, i)
try:
txt = fmt % val
except Exception:
txt = str(val)
if unit:
self.vars[field].set(f"{txt} ({unit})")
else:
self.vars[field].set(txt)
else:
self.vars[field].set("N/A")
@ -456,6 +594,15 @@ class CommandFrameControls(tk.Frame):
command=lambda f=field, m=msg: self._on_check(f, m)
)
cb.grid(row=0, column=i)
# attach tooltip to checkbox
try:
if msg and hasattr(msg, 'message_id'):
tt = f"{msg.message_id} - {field}"
else:
tt = self.info.get(f"tooltip{i}", self.info.get('description',''))
ToolTip(cb, tt)
except Exception:
pass
WIDGET_MAP[field] = {"widget": cb, "var": self.vars[field]}
elif ctrl_type == "combobox":
@ -467,6 +614,18 @@ class CommandFrameControls(tk.Frame):
)
cmb.grid(row=0, column=i)
cmb.bind("<<ComboboxSelected>>", lambda e, f=field, m=msg, idx=i: self._on_combo(f, m, idx))
# attach tooltip to combobox
try:
if msg and hasattr(msg, 'message_id'):
tt = f"{msg.message_id} - {field}"
unit = _infer_unit_from_info(self.info, i)
if unit:
tt = f"{tt} ({unit})"
else:
tt = self.info.get(f"tooltip{i}", self.info.get('description',''))
ToolTip(cmb, tt)
except Exception:
pass
WIDGET_MAP[field] = {"widget": cmb, "var": self.vars[field]}
elif ctrl_type == "spinbox":
@ -492,6 +651,18 @@ class CommandFrameControls(tk.Frame):
# Detect arrow button clicks
sp.bind("<ButtonRelease-1>", lambda e, f=field, m=msg, idx=i: self._on_spin_arrow(f, m, idx))
WIDGET_MAP[field] = {"widget": sp, "var": self.vars[field]}
# attach tooltip to spinbox (include unit if available)
try:
if msg and hasattr(msg, 'message_id'):
tt = f"{msg.message_id} - {field}"
else:
tt = self.info.get(f"tooltip{i}", self.info.get('description',''))
unit = _infer_unit_from_info(self.info, i)
if unit:
tt = f"{tt} ({unit})"
ToolTip(sp, tt)
except Exception:
pass
elif ctrl_type == "label":
self.vars[field] = tk.StringVar(value="0")
@ -586,6 +757,11 @@ class CommandFrameControls(tk.Frame):
except IndexError:
self.vars[field].set("ERR")
elif ctrl_type == "spinbox":
# Do not place unit inside editable spinbox value; just format number
fmt = '%.0f' if self.info.get(f"number_format{i}") == "integer" else '%.4f'
try:
self.vars[field].set(fmt % readable)
except Exception:
self.vars[field].set(readable)
elif ctrl_type == "label":
self.vars[field].set("%.4f" % readable)

View File

@ -199,6 +199,8 @@ class MainDockingWindow:
system_menu.add_command(label="Stop System", command=self._stop_system)
system_menu.add_separator()
system_menu.add_command(label="System Info", command=self._show_system_info)
system_menu.add_separator()
system_menu.add_command(label="1553 Debug", command=self._open_1553_debug)
# Help menu
help_menu = tk.Menu(menubar, tearoff=0)
@ -318,6 +320,54 @@ class MainDockingWindow:
"""Stop the 1553 bus system."""
if hasattr(self.bus_module, 'stop'):
self.bus_module.stop()
def _open_1553_debug(self):
"""Open a separate window with the PyBusMonitor1553 DebugView (1553 debug)."""
try:
# Import the simplified DebugView (not MonitorApp)
from pymsc.PyBusMonitor1553.gui.debug_view import DebugView
except Exception as e:
messagebox.showerror("1553 Debug", f"Cannot import DebugView: {e}")
return
try:
win = tk.Toplevel(self.root)
win.title("1553 Debug Monitor")
win.geometry("1000x700")
# Pass existing BusMonitorCore instance
bus_monitor_instance = getattr(self.bus_module, 'core', None)
if not bus_monitor_instance:
messagebox.showerror("1553 Debug", "BusMonitorCore not initialized")
win.destroy()
return
# Create DebugView with existing bus_monitor
app = DebugView(parent=win, bus_monitor=bus_monitor_instance)
# Keep references
self._1553_debug_win = win
self._1553_debug_app = app
# Safe close handler
def _close_debug():
try:
app.stop()
except Exception:
pass
try:
win.destroy()
except Exception:
pass
win.protocol("WM_DELETE_WINDOW", _close_debug)
logger = logging.getLogger('ARTOS')
logger.info("1553 Debug window opened")
except Exception as e:
messagebox.showerror("1553 Debug", f"Failed to create debug window: {e}")
return
messagebox.showinfo("System", "Bus system stopped.")
def _show_system_info(self):

View File

@ -39,7 +39,7 @@ class ControlPage(ScrollableFrame):
"range_scale", "velocity_scale", "scan_width", "bars",
"expand", "zoom", "sar_map_orientation", "ground_tgt_reject",
"min_det_ground_tgt", "power_up_stop", "spot",
"sar_enabled", "ghost_enabled", "dtt_enabled"
"sar_enabled", "ghost_enabled", "dtt_enabled","sar_crs_feas", "sar_spoi_feas"
]
for func in functions:
w = create_widget_by_id(self.frame_funcs, func)

View File

@ -26,7 +26,7 @@ class MissionPage(ScrollableFrame):
self.f2 = ttk.LabelFrame(self.scrollable_content, text="Attitude (SNU)")
self.f2.grid(row=1, column=0, padx=10, pady=5, sticky="nsew")
atts = [
"attitude", "true_heading", "magnetic_heading",
"attitude", "nav_data_invalid", "true_heading", "magnetic_heading",
"platform_azimuth", "yaw_snu", "pitch_snu", "roll_snu"
]
for a in atts:
@ -50,7 +50,7 @@ class MissionPage(ScrollableFrame):
# 5. Velocities
self.f5 = ttk.LabelFrame(self.scrollable_content, text="Velocities")
self.f5.grid(row=1, column=1, padx=10, pady=5, sticky="nsew")
vels = ["tas", "cas", "nav_data_invalid", "vx/vy/vz", "accx/accy/accz", "nx/ny/nz", "wind"]
vels = ["tas", "cas", "vx/vy/vz", "accx/accy/accz", "nx/ny/nz", "wind"]
for v in vels:
create_widget_by_id(self.f5, v).pack(fill=tk.X, padx=5, pady=2)