Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e78691ba54 | ||
|
|
95a3d6e562 | ||
|
|
7d15cf143a | ||
|
|
ac843839bb |
3631
_cpp/th_b1553_icd.h
Normal file
3631
_cpp/th_b1553_icd.h
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,5 +1,5 @@
|
||||
{
|
||||
"window_geometry": "1600x1099+310+130",
|
||||
"window_geometry": "1600x1099+26+26",
|
||||
"main_sash_position": [
|
||||
1,
|
||||
793
|
||||
|
||||
@ -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)
|
||||
@ -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):
|
||||
|
||||
@ -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)
|
||||
@ -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)
|
||||
|
||||
@ -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))
|
||||
|
||||
|
||||
391
pymsc/PyBusMonitor1553/gui/debug_view.py
Normal file
391
pymsc/PyBusMonitor1553/gui/debug_view.py
Normal 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
|
||||
@ -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:
|
||||
|
||||
@ -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',
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
|
||||
@ -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"
|
||||
},
|
||||
{
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user