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": [
|
"main_sash_position": [
|
||||||
1,
|
1,
|
||||||
793
|
793
|
||||||
|
|||||||
@ -7,6 +7,7 @@ em.ENUM_MAP.update({
|
|||||||
"rws_submode_tellback" : RwsSubmode,
|
"rws_submode_tellback" : RwsSubmode,
|
||||||
"spot_function_tellback" : SpotSelection,
|
"spot_function_tellback" : SpotSelection,
|
||||||
"acm_submode_tellback" : AcmSubmode,
|
"acm_submode_tellback" : AcmSubmode,
|
||||||
|
"gm_submode_tellback" : GmSubmode,
|
||||||
"expand_tellback" : Expand,
|
"expand_tellback" : Expand,
|
||||||
"range_scale_tellback" : RangeScale,
|
"range_scale_tellback" : RangeScale,
|
||||||
"number_of_bars_tellback" : BarsNum,
|
"number_of_bars_tellback" : BarsNum,
|
||||||
@ -16,14 +17,14 @@ em.ENUM_MAP.update({
|
|||||||
class _RdrFunAndParam1TellbackStr(ctypes.LittleEndianStructure):
|
class _RdrFunAndParam1TellbackStr(ctypes.LittleEndianStructure):
|
||||||
_pack_ = 1
|
_pack_ = 1
|
||||||
_fields_ = [
|
_fields_ = [
|
||||||
("azimuth_scan_width_tellback" , ctypes.c_uint16,4),
|
("azimuth_scan_width_tellback", ctypes.c_uint16, 4), # bit 0-3
|
||||||
("number_of_bars_tellback" , ctypes.c_uint16,2),
|
("number_of_bars_tellback", ctypes.c_uint16, 2), # bit 4-5
|
||||||
("range_scale_tellback" , ctypes.c_uint16,2),
|
("range_scale_tellback", ctypes.c_uint16, 2), # bit 6-7
|
||||||
("expand_tellback" , ctypes.c_uint16,2),
|
("expand_tellback", ctypes.c_uint16, 2), # bit 8-9
|
||||||
("reserved" , ctypes.c_uint16,1),
|
("gm_submode_tellback", ctypes.c_uint16, 1), # bit 10
|
||||||
("acm_submode_tellback" , ctypes.c_uint16,3),
|
("acm_submode_tellback", ctypes.c_uint16, 3), # bit 11-13
|
||||||
("spot_function_tellback" , ctypes.c_uint16,1),
|
("spot_function_tellback", ctypes.c_uint16, 1), # bit 14
|
||||||
("rws_submode_tellback" , ctypes.c_uint16,1)
|
("rws_submode_tellback", ctypes.c_uint16, 1) # bit 15
|
||||||
]
|
]
|
||||||
|
|
||||||
class RdrFunAndParam1Tellback(ctypes.Union):
|
class RdrFunAndParam1Tellback(ctypes.Union):
|
||||||
@ -88,20 +89,10 @@ class RdrFunAndParam1Tellback(ctypes.Union):
|
|||||||
self.raw &= ~(0x03 << (15-11))
|
self.raw &= ~(0x03 << (15-11))
|
||||||
self.raw |= (value << (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):
|
def get_scan_width(self):
|
||||||
return AzimuthScanWidth((self.raw >> (15-13)) & 0x03)
|
return AzimuthScanWidth(self.raw & 0x0F)
|
||||||
|
|
||||||
def set_scan_width(self, value):
|
def set_scan_width(self, value):
|
||||||
self.raw &= ~(0x03 << (15-13))
|
self.raw &= ~0x0F
|
||||||
self.raw |= (value << (15-13))
|
self.raw |= (value & 0x0F)
|
||||||
|
|
||||||
# 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)
|
|
||||||
@ -12,11 +12,10 @@ em.ENUM_MAP.update({
|
|||||||
class _RdrFunAndParam2Str(ctypes.LittleEndianStructure):
|
class _RdrFunAndParam2Str(ctypes.LittleEndianStructure):
|
||||||
_pack_ = 1
|
_pack_ = 1
|
||||||
_fields_ = [
|
_fields_ = [
|
||||||
("spare", ctypes.c_uint16,8),
|
("spare_8_15", ctypes.c_uint16, 8), # bit 0-7
|
||||||
("sar_map_orientation", ctypes.c_uint16,2),
|
("sar_map_orientation", ctypes.c_uint16, 2), # bit 8-9
|
||||||
("zoom_command", ctypes.c_uint16,2),
|
("zoom_command", ctypes.c_uint16, 2), # bit 10-11
|
||||||
("spare_2", ctypes.c_uint16,4),
|
("spare_0_4", ctypes.c_uint16, 4), # bit 12-15
|
||||||
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -34,21 +33,21 @@ class RdrFunAndParam2(ctypes.Union):
|
|||||||
self.raw &= ~(0x0F << 12)
|
self.raw &= ~(0x0F << 12)
|
||||||
self.raw |= (value << 12)
|
self.raw |= (value << 12)
|
||||||
|
|
||||||
# Bitfield accessors for zoom
|
# Bitfield accessors for zoom (bit 10-11)
|
||||||
def get_zoom(self):
|
def get_zoom(self):
|
||||||
return Zoom((self.raw >> 9) & 0x03)
|
return Zoom((self.raw >> 10) & 0x03)
|
||||||
|
|
||||||
def set_zoom(self, value):
|
def set_zoom(self, value):
|
||||||
self.raw &= ~(0x03 << 9)
|
self.raw &= ~(0x03 << 10)
|
||||||
self.raw |= (value << 9)
|
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):
|
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):
|
def set_sar_map_orientation(self, value):
|
||||||
self.raw &= ~(0x03 << 7)
|
self.raw &= ~(0x03 << 8)
|
||||||
self.raw |= (value << 7)
|
self.raw |= (value << 8)
|
||||||
|
|
||||||
# Bitfield accessors for spare_8_15
|
# Bitfield accessors for spare_8_15
|
||||||
def get_spare_8_15(self):
|
def get_spare_8_15(self):
|
||||||
|
|||||||
@ -1,20 +1,58 @@
|
|||||||
import ctypes
|
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):
|
class _RdrFunAndParam2TellbackStr(ctypes.LittleEndianStructure):
|
||||||
_pack_ = 1
|
_pack_ = 1
|
||||||
_fields_ = [
|
_fields_ = [
|
||||||
("spare2", ctypes.c_uint16,6),
|
("spare6_15", ctypes.c_uint16, 6), # bit 0-5
|
||||||
("reserved9", ctypes.c_uint16,1),
|
("sar_spoi_feasibility", ctypes.c_uint16, 1), # bit 6
|
||||||
("reserved8", ctypes.c_uint16,1),
|
("sar_crs_feasibility", ctypes.c_uint16, 1), # bit 7
|
||||||
("reserved6", ctypes.c_uint16,2),
|
("sar_map_orientation_tellback", ctypes.c_uint16, 2), # bit 8-9
|
||||||
("reserved4", ctypes.c_uint16,2),
|
("zoom_tellback", ctypes.c_uint16, 2), # bit 10-11
|
||||||
("spare", ctypes.c_uint16,4),
|
("spare0_4", ctypes.c_uint16, 4), # bit 12-15
|
||||||
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class RdrFunAndParam2Tellback(ctypes.Union):
|
class RdrFunAndParam2Tellback(ctypes.Union):
|
||||||
_fields_ = [
|
_fields_ = [
|
||||||
("raw", ctypes.c_uint16),
|
("raw", ctypes.c_uint16),
|
||||||
("str", _RdrFunAndParam2TellbackStr)
|
("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),
|
("spare_0_1", ctypes.c_uint16, 2),
|
||||||
("sar_type", ctypes.c_uint16, 1),
|
("sar_type", ctypes.c_uint16, 1),
|
||||||
("silence", 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),
|
("reserved11", ctypes.c_uint16, 1),
|
||||||
("stop_powerup", ctypes.c_uint16, 1),
|
("stop_powerup", ctypes.c_uint16, 1),
|
||||||
("freeze", 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)
|
self.raw = (self.raw & ~(0x01 << 5)) | ((int(value) & 0x01) << 5)
|
||||||
|
|
||||||
def get_reserved11(self):
|
def get_reserved11(self):
|
||||||
|
"""Return the reserved11 bit (EMERGENCY flag in some configs)."""
|
||||||
return (self.raw >> 4) & 0x01
|
return (self.raw >> 4) & 0x01
|
||||||
|
|
||||||
def set_reserved11(self, value):
|
def set_reserved11(self, value):
|
||||||
|
"""Set the reserved11 bit (EMERGENCY flag in some configs)."""
|
||||||
self.raw = (self.raw & ~(0x01 << 4)) | ((int(value) & 0x01) << 4)
|
self.raw = (self.raw & ~(0x01 << 4)) | ((int(value) & 0x01) << 4)
|
||||||
def get_silence(self):
|
def get_silence(self):
|
||||||
return SilenceSelection((self.raw >> 3) & 0x01)
|
return SilenceSelection((self.raw >> 3) & 0x01)
|
||||||
|
|||||||
@ -88,9 +88,15 @@ class RdrStatusTellback(ctypes.Union):
|
|||||||
|
|
||||||
# Bitfield accessors for reserved11
|
# Bitfield accessors for reserved11
|
||||||
def get_reserved11(self):
|
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
|
return (self.raw >> (15-11)) & 0x01
|
||||||
|
|
||||||
def set_reserved11(self, value):
|
def set_reserved11(self, value):
|
||||||
|
"""Set the reserved11 bit (EMERGENCY tellback in some configs)."""
|
||||||
self.raw &= ~(0x01 << (15-11))
|
self.raw &= ~(0x01 << (15-11))
|
||||||
self.raw |= (value << (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):
|
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)
|
super().__init__(master)
|
||||||
self.master = master
|
self.master = master
|
||||||
self.pack(fill=tk.BOTH, expand=True)
|
self.pack(fill=tk.BOTH, expand=True)
|
||||||
|
|
||||||
# Use BusMonitorCore (ARTOS API) - same as ARTOS Collector will use
|
# Use an existing BusMonitorCore instance if provided (preferred)
|
||||||
# The application will remain idle until the user presses `Initialize`.
|
self.bus_monitor = bus_monitor
|
||||||
self.bus_monitor = None
|
|
||||||
self.import_error = None
|
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.create_widgets()
|
||||||
self.update_loop_running = False
|
self.update_loop_running = False
|
||||||
# cache tree item ids by message label for incremental updates
|
# cache tree item ids by message label for incremental updates
|
||||||
@ -170,18 +179,23 @@ class MonitorApp(tk.Frame):
|
|||||||
|
|
||||||
# Status bar will be added below the whole UI (outside all frames)
|
# 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
|
self.logger_system = None
|
||||||
try:
|
try:
|
||||||
if TkinterLogger is not None:
|
if getattr(self, '_use_main_logger', False):
|
||||||
self.logger_system = TkinterLogger(self.master)
|
# Use main application's ARTOS logger so logs go to main GUI
|
||||||
self.logger_system.setup(enable_console=True, enable_tkinter=True)
|
self.logger = logging.getLogger('ARTOS')
|
||||||
self.logger_system.add_tkinter_handler(self.log)
|
|
||||||
self.logger = logging.getLogger(__name__)
|
|
||||||
else:
|
else:
|
||||||
logging.basicConfig(level=logging.INFO)
|
if TkinterLogger is not None:
|
||||||
self.logger = logging.getLogger(__name__)
|
self.logger_system = TkinterLogger(self.master)
|
||||||
self.logger.info("TkinterLogger not available; using basic logging")
|
self.logger_system.setup(enable_console=True, enable_tkinter=True)
|
||||||
|
self.logger_system.add_tkinter_handler(self.log)
|
||||||
|
self.logger = logging.getLogger(__name__)
|
||||||
|
else:
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
self.logger = logging.getLogger(__name__)
|
||||||
|
self.logger.info("TkinterLogger not available; using basic logging")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.basicConfig(level=logging.INFO)
|
logging.basicConfig(level=logging.INFO)
|
||||||
self.logger = logging.getLogger(__name__)
|
self.logger = logging.getLogger(__name__)
|
||||||
@ -467,6 +481,11 @@ class MonitorApp(tk.Frame):
|
|||||||
row_fr.pack(fill=tk.X, padx=2, pady=1)
|
row_fr.pack(fill=tk.X, padx=2, pady=1)
|
||||||
lbl = tk.Label(row_fr, text=name, width=28, anchor=tk.W)
|
lbl = tk.Label(row_fr, text=name, width=28, anchor=tk.W)
|
||||||
lbl.pack(side=tk.LEFT)
|
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
|
# determine per-field editability: make mission date/time readonly
|
||||||
root_field = full.split('.')[0]
|
root_field = full.split('.')[0]
|
||||||
readonly_roots = ('date_of_mission', 'time_of_mission')
|
readonly_roots = ('date_of_mission', 'time_of_mission')
|
||||||
@ -510,6 +529,21 @@ class MonitorApp(tk.Frame):
|
|||||||
if editable:
|
if editable:
|
||||||
cb.bind('<<ComboboxSelected>>', lambda e, path=full: self._on_field_changed(path))
|
cb.bind('<<ComboboxSelected>>', lambda e, path=full: self._on_field_changed(path))
|
||||||
cb.pack(side=tk.RIGHT, fill=tk.X, expand=True)
|
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)
|
widget = ('combobox', cb, enum_items)
|
||||||
else:
|
else:
|
||||||
# numeric or text entry
|
# 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('<Return>', lambda e, path=full: self._on_entry_finished(path))
|
||||||
ent.bind('<FocusOut>', 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)
|
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)
|
widget = ('entry', ent)
|
||||||
|
|
||||||
# register created widget for later updates; values will be filled
|
# register created widget for later updates; values will be filled
|
||||||
@ -545,6 +594,69 @@ class MonitorApp(tk.Frame):
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
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):
|
def _on_entry_focus_in(self, field_path):
|
||||||
"""Mark entry as being edited to prevent refresh from overwriting user input."""
|
"""Mark entry as being edited to prevent refresh from overwriting user input."""
|
||||||
try:
|
try:
|
||||||
|
|||||||
@ -60,10 +60,11 @@ A2_FIELD_MAP = {
|
|||||||
'PWR_UP_STOP_FUNCT_SEL': 'rdr_mode_command.stop_powerup',
|
'PWR_UP_STOP_FUNCT_SEL': 'rdr_mode_command.stop_powerup',
|
||||||
'silence': 'rdr_mode_command.silence',
|
'silence': 'rdr_mode_command.silence',
|
||||||
'SAR_ACQUISITION': 'rdr_mode_command.sar_type',
|
'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)
|
# 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',
|
'rws_submode_command': 'param1.rws_submode',
|
||||||
'SPOT_FUNC_SEL': 'param1.spot',
|
'SPOT_FUNC_SEL': 'param1.spot',
|
||||||
'acm_submode_command': 'param1.acm_submode',
|
'acm_submode_command': 'param1.acm_submode',
|
||||||
@ -186,12 +187,12 @@ A3_FIELD_MAP = {
|
|||||||
# ============================================================================
|
# ============================================================================
|
||||||
A4_FIELD_MAP = {
|
A4_FIELD_MAP = {
|
||||||
# Validity and slew
|
# Validity and slew
|
||||||
# Note: SAR/Ghost/DTT fields may not exist in A4ValidityAndSlew - checking available getters
|
# Note: SAR/Ghost/DTT selectors exist inside the cursor acquisition union
|
||||||
# Available: ant_slew_valid, attitude_invalid, baro_intertial_altitude_invalid, cas_invalid, etc.
|
# (`acq_crs_x` -> `CrsMotionX.str`). Map legacy flat names to the nested
|
||||||
# These fields might be in a different message or not implemented yet
|
# path so GUI controls can access the correct bitfields.
|
||||||
# 'SAR_ENABLED': 'validity_and_slew.spare1', # DISABLED - field not found
|
'SAR_ENABLED': 'acq_crs_x.str.sar_enabled_selector',
|
||||||
# 'NORM_GHOST_SELECTION': 'validity_and_slew.spare2', # DISABLED - field not found
|
'NORM_GHOST_SELECTION': 'acq_crs_x.str.normal_ghost_selector',
|
||||||
# 'DTT_ENABLED': 'validity_and_slew.spare3', # DISABLED - field not found
|
'DTT_ENABLED': 'acq_crs_x.str.mtt_enabled_selector',
|
||||||
'CLEARANCE_PLANE_DIST': 'clearance_plane_distance',
|
'CLEARANCE_PLANE_DIST': 'clearance_plane_distance',
|
||||||
|
|
||||||
# Timetag
|
# Timetag
|
||||||
@ -329,6 +330,10 @@ B6_FIELD_MAP = {
|
|||||||
'CRS_LON_tellback': 'cursor_pos_longitude',
|
'CRS_LON_tellback': 'cursor_pos_longitude',
|
||||||
'crs_x_tellback': 'cursor_x_display_coord_qual.current_x_display_coord',
|
'crs_x_tellback': 'cursor_x_display_coord_qual.current_x_display_coord',
|
||||||
'crs_y_tellback': 'cursor_y_display_coord.current_y_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 tellback
|
||||||
'PARAM_ID_TB': 'param_id_tellback',
|
'PARAM_ID_TB': 'param_id_tellback',
|
||||||
@ -349,25 +354,34 @@ B7_FIELD_MAP = {
|
|||||||
'stby_tellback': 'rdr_mode_tellback.stby',
|
'stby_tellback': 'rdr_mode_tellback.stby',
|
||||||
'freeze_tellback': 'rdr_mode_tellback.freeze',
|
'freeze_tellback': 'rdr_mode_tellback.freeze',
|
||||||
'rf_status': 'rdr_mode_tellback.rf_radiation',
|
'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
|
# Status flags
|
||||||
'TRANSITION': 'rdr_mode_tellback.transition_status',
|
'TRANSITION': 'rdr_mode_tellback.transition_status',
|
||||||
'LAST_ACQ_FAIL': 'rdr_mode_tellback.last_acq_result',
|
'LAST_ACQ_FAIL': 'rdr_mode_tellback.last_acq_result',
|
||||||
'DEGRADED_PERF_STATUS': 'rdr_mode_tellback.spare', # Field not found, using spare
|
'DEGRADED_PERF_STATUS': 'rdr_mode_tellback.spare', # Field not found, using spare
|
||||||
|
|
||||||
# Function parameters tellback
|
# Function parameters tellback - use .str for direct bitfield access
|
||||||
'rws_submode_tellback': 'param1_tellback.rws_submode',
|
'rws_submode_tellback': 'param1_tellback.str.rws_submode_tellback',
|
||||||
'acm_submode_tellback': 'param1_tellback.acm_submode',
|
'spot_function_tellback': 'param1_tellback.str.spot_function_tellback',
|
||||||
'gm_submode_tellback': 'param1_tellback.gm_submode',
|
'acm_submode_tellback': 'param1_tellback.str.acm_submode_tellback',
|
||||||
'range_scale_tellback': 'param1_tellback.range_scale',
|
'gm_submode_tellback': 'param1_tellback.str.gm_submode_tellback',
|
||||||
'bars_tellback': 'param1_tellback.bars_num',
|
'expand_tellback': 'param1_tellback.str.expand_tellback',
|
||||||
'scan_width_tellback': 'param1_tellback.scan_width',
|
'range_scale_tellback': 'param1_tellback.str.range_scale_tellback',
|
||||||
# Note: velocity_scale field does not exist in RdrFunAndParam1Tellback
|
'bars_tellback': 'param1_tellback.str.number_of_bars_tellback',
|
||||||
'velocity_scale_tellback': 'param1_tellback.spare', # Field not found
|
'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
|
||||||
'SAR_ACQUISITION_TB': 'rdr_mode_tellback.sar_type',
|
'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"
|
"label": "IBIT", "description": "toggle IBIT status", "param": "0"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"command": "sar_enabled", "message": None, "field": "SAR_ENABLED", "tooltip": "A4 SAR_ENABLED",
|
"command": "sar_enabled", "message": msg_a4, "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"
|
"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",
|
"command": "sar_crs_feas", "message": None, "field": "", "tooltip": "",
|
||||||
"message_tb": None, "field_tb": "", "label": "Ghost enabled", "description": "toggle ghost enabled [DISABLED - field not in 1553]", "param": "0"
|
"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",
|
"command": "sar_spoi_feas", "message": None, "field": "", "tooltip": "",
|
||||||
"message_tb": None, "field_tb": "", "label": "DTT enabled", "description": "toggle DTT enabled [DISABLED - field not in 1553]", "param": "0"
|
"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",
|
"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"
|
"message_tb": None, "field_tb": "", "label": "ALE blanking", "description": "toggle ALE blanking", "param": "0"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"command": "emergency", "message": None, "field": "emergency", "tooltip": "A2 emergency",
|
"command": "spot", "message": msg_a2, "field": "SPOT_FUNC_SEL", "tooltip": "A2 SPOT",
|
||||||
"message_tb": None, "field_tb": "emergency_tellback", "tooltip_tb": "B7 emergency_tb",
|
"message_tb": msg_b7, "field_tb": "spot_function_tellback", "tooltip_tb": "B7 SPOT_TB",
|
||||||
"label": "EMERGENCY", "description": "toggle EMERGENCY status [DISABLED - field not in 1553]", "param": "0"
|
"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,
|
"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",
|
"command": "expand", "message": msg_a2, "field": "EXPAND", "tooltip": "A2 EXPAND",
|
||||||
"message_tb": None, "field_tb": "", "label": "Expand", "description": "Select Expand",
|
"message_tb": msg_b7, "field_tb": "expand_tellback", "label": "Expand", "description": "Select Expand",
|
||||||
"values": ["NORMAL", "EXPAND", "EXP_NV2", "EXP_NV3"]
|
"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",
|
"command": "zoom", "message": msg_a2, "field": "ZOOM_COMMAND", "tooltip": "A2 ZOOM",
|
||||||
"message_tb": None, "field_tb": "", "label": "Zoom", "description": "Select Zoom",
|
"message_tb": msg_b7, "field_tb": "zoom_tellback", "label": "Zoom", "description": "Select Zoom",
|
||||||
"values": ["ZOOM_NOT_ATIVE", "ZOOM_IN", "ZOOM_OUT", "ZOOM_SPARE"]
|
"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",
|
"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",
|
"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": ["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",
|
"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",
|
"message_tb": None, "field_tb": "", "label": "Power-up stop", "description": "Select Power-up stop",
|
||||||
"values": ["NORMAL", "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",
|
"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",
|
"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"
|
"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"
|
"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
|
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)
|
# Registry for finding widgets by their label (used for automation/scripts)
|
||||||
WIDGET_MAP: Dict[str, Any] = {}
|
WIDGET_MAP: Dict[str, Any] = {}
|
||||||
|
|
||||||
@ -61,6 +99,20 @@ class CommandFrameCheckBox(BaseCommandFrame):
|
|||||||
width=self.column_widths[1]
|
width=self.column_widths[1]
|
||||||
)
|
)
|
||||||
self.command_check.grid(row=0, column=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'])
|
initial_val = self.info['message'].get_value_for_field(self.info['field'])
|
||||||
self.command_var.set(bool(initial_val))
|
self.command_var.set(bool(initial_val))
|
||||||
|
|
||||||
@ -71,6 +123,20 @@ class CommandFrameCheckBox(BaseCommandFrame):
|
|||||||
width=self.column_widths[2]
|
width=self.column_widths[2]
|
||||||
)
|
)
|
||||||
self.tellback_check.grid(row=0, column=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):
|
def on_toggle(self):
|
||||||
# Skip if message is None (disabled field)
|
# Skip if message is None (disabled field)
|
||||||
@ -137,6 +203,16 @@ class CommandFrameComboBox(BaseCommandFrame):
|
|||||||
)
|
)
|
||||||
self.combo.grid(row=0, column=1)
|
self.combo.grid(row=0, column=1)
|
||||||
self.combo.bind("<<ComboboxSelected>>", self.on_select)
|
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'):
|
if self.info.get('message_tb'):
|
||||||
self.tb_var = tk.StringVar(value="---")
|
self.tb_var = tk.StringVar(value="---")
|
||||||
@ -145,6 +221,20 @@ class CommandFrameComboBox(BaseCommandFrame):
|
|||||||
width=self.column_widths[2], relief="sunken"
|
width=self.column_widths[2], relief="sunken"
|
||||||
)
|
)
|
||||||
self.tb_label.grid(row=0, column=2)
|
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):
|
def on_select(self, event=None):
|
||||||
self.start_time = time.time()
|
self.start_time = time.time()
|
||||||
@ -171,7 +261,12 @@ class CommandFrameComboBox(BaseCommandFrame):
|
|||||||
val_tb_text = self.info["values_tb"][idx_tb]
|
val_tb_text = self.info["values_tb"][idx_tb]
|
||||||
except (IndexError, KeyError, TypeError):
|
except (IndexError, KeyError, TypeError):
|
||||||
val_tb_text = "ERR"
|
val_tb_text = "ERR"
|
||||||
self.tb_var.set(val_tb_text)
|
# 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
|
elapsed = (time.time() - self.start_time) * 1000
|
||||||
if idx_tb == self.combo.current():
|
if idx_tb == self.combo.current():
|
||||||
self.label.config(bg="green")
|
self.label.config(bg="green")
|
||||||
@ -242,6 +337,17 @@ class CommandFrameSpinBox(BaseCommandFrame):
|
|||||||
# Also use variable trace to detect arrow changes
|
# Also use variable trace to detect arrow changes
|
||||||
self.cmd_var.trace_add("write", self._on_var_change)
|
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'):
|
if self.info.get('message_tb'):
|
||||||
self.tb_var = tk.StringVar(value="0")
|
self.tb_var = tk.StringVar(value="0")
|
||||||
self.tb_label = tk.Label(
|
self.tb_label = tk.Label(
|
||||||
@ -249,6 +355,20 @@ class CommandFrameSpinBox(BaseCommandFrame):
|
|||||||
width=self.column_widths[2], relief="sunken"
|
width=self.column_widths[2], relief="sunken"
|
||||||
)
|
)
|
||||||
self.tb_label.grid(row=0, column=2)
|
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):
|
def _validate(self, p):
|
||||||
if p == "" or p == "-":
|
if p == "" or p == "-":
|
||||||
@ -335,7 +455,16 @@ class CommandFrameSpinBox(BaseCommandFrame):
|
|||||||
raw_tb = msg_tb.get_value_for_field(self.info['field_tb'])
|
raw_tb = msg_tb.get_value_for_field(self.info['field_tb'])
|
||||||
readable_tb = get_correct_value(self.info, -1, raw_tb)
|
readable_tb = get_correct_value(self.info, -1, raw_tb)
|
||||||
fmt = "%.0f" if self.info.get("number_format") == "integer" else "%.4f"
|
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
|
elapsed = (time.time() - self.start_time) * 1000
|
||||||
target = float(self.cmd_var.get())
|
target = float(self.cmd_var.get())
|
||||||
if abs(readable_tb - target) < 0.0001:
|
if abs(readable_tb - target) < 0.0001:
|
||||||
@ -412,7 +541,16 @@ class CommandFrameLabels(tk.Frame):
|
|||||||
raw = msg.get_value_for_field(field)
|
raw = msg.get_value_for_field(field)
|
||||||
val = get_correct_value(self.info, i, raw)
|
val = get_correct_value(self.info, i, raw)
|
||||||
if val is not None:
|
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:
|
else:
|
||||||
self.vars[field].set("N/A")
|
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)
|
command=lambda f=field, m=msg: self._on_check(f, m)
|
||||||
)
|
)
|
||||||
cb.grid(row=0, column=i)
|
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]}
|
WIDGET_MAP[field] = {"widget": cb, "var": self.vars[field]}
|
||||||
|
|
||||||
elif ctrl_type == "combobox":
|
elif ctrl_type == "combobox":
|
||||||
@ -467,6 +614,18 @@ class CommandFrameControls(tk.Frame):
|
|||||||
)
|
)
|
||||||
cmb.grid(row=0, column=i)
|
cmb.grid(row=0, column=i)
|
||||||
cmb.bind("<<ComboboxSelected>>", lambda e, f=field, m=msg, idx=i: self._on_combo(f, m, idx))
|
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]}
|
WIDGET_MAP[field] = {"widget": cmb, "var": self.vars[field]}
|
||||||
|
|
||||||
elif ctrl_type == "spinbox":
|
elif ctrl_type == "spinbox":
|
||||||
@ -492,6 +651,18 @@ class CommandFrameControls(tk.Frame):
|
|||||||
# Detect arrow button clicks
|
# Detect arrow button clicks
|
||||||
sp.bind("<ButtonRelease-1>", lambda e, f=field, m=msg, idx=i: self._on_spin_arrow(f, m, idx))
|
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]}
|
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":
|
elif ctrl_type == "label":
|
||||||
self.vars[field] = tk.StringVar(value="0")
|
self.vars[field] = tk.StringVar(value="0")
|
||||||
@ -586,7 +757,12 @@ class CommandFrameControls(tk.Frame):
|
|||||||
except IndexError:
|
except IndexError:
|
||||||
self.vars[field].set("ERR")
|
self.vars[field].set("ERR")
|
||||||
elif ctrl_type == "spinbox":
|
elif ctrl_type == "spinbox":
|
||||||
self.vars[field].set(readable)
|
# 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":
|
elif ctrl_type == "label":
|
||||||
self.vars[field].set("%.4f" % readable)
|
self.vars[field].set("%.4f" % readable)
|
||||||
self.is_programmatic_change = False
|
self.is_programmatic_change = False
|
||||||
@ -199,6 +199,8 @@ class MainDockingWindow:
|
|||||||
system_menu.add_command(label="Stop System", command=self._stop_system)
|
system_menu.add_command(label="Stop System", command=self._stop_system)
|
||||||
system_menu.add_separator()
|
system_menu.add_separator()
|
||||||
system_menu.add_command(label="System Info", command=self._show_system_info)
|
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
|
||||||
help_menu = tk.Menu(menubar, tearoff=0)
|
help_menu = tk.Menu(menubar, tearoff=0)
|
||||||
@ -318,6 +320,54 @@ class MainDockingWindow:
|
|||||||
"""Stop the 1553 bus system."""
|
"""Stop the 1553 bus system."""
|
||||||
if hasattr(self.bus_module, 'stop'):
|
if hasattr(self.bus_module, 'stop'):
|
||||||
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.")
|
messagebox.showinfo("System", "Bus system stopped.")
|
||||||
|
|
||||||
def _show_system_info(self):
|
def _show_system_info(self):
|
||||||
|
|||||||
@ -39,7 +39,7 @@ class ControlPage(ScrollableFrame):
|
|||||||
"range_scale", "velocity_scale", "scan_width", "bars",
|
"range_scale", "velocity_scale", "scan_width", "bars",
|
||||||
"expand", "zoom", "sar_map_orientation", "ground_tgt_reject",
|
"expand", "zoom", "sar_map_orientation", "ground_tgt_reject",
|
||||||
"min_det_ground_tgt", "power_up_stop", "spot",
|
"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:
|
for func in functions:
|
||||||
w = create_widget_by_id(self.frame_funcs, func)
|
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 = ttk.LabelFrame(self.scrollable_content, text="Attitude (SNU)")
|
||||||
self.f2.grid(row=1, column=0, padx=10, pady=5, sticky="nsew")
|
self.f2.grid(row=1, column=0, padx=10, pady=5, sticky="nsew")
|
||||||
atts = [
|
atts = [
|
||||||
"attitude", "true_heading", "magnetic_heading",
|
"attitude", "nav_data_invalid", "true_heading", "magnetic_heading",
|
||||||
"platform_azimuth", "yaw_snu", "pitch_snu", "roll_snu"
|
"platform_azimuth", "yaw_snu", "pitch_snu", "roll_snu"
|
||||||
]
|
]
|
||||||
for a in atts:
|
for a in atts:
|
||||||
@ -50,7 +50,7 @@ class MissionPage(ScrollableFrame):
|
|||||||
# 5. Velocities
|
# 5. Velocities
|
||||||
self.f5 = ttk.LabelFrame(self.scrollable_content, text="Velocities")
|
self.f5 = ttk.LabelFrame(self.scrollable_content, text="Velocities")
|
||||||
self.f5.grid(row=1, column=1, padx=10, pady=5, sticky="nsew")
|
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:
|
for v in vels:
|
||||||
create_widget_by_id(self.f5, v).pack(fill=tk.X, padx=5, pady=2)
|
create_widget_by_id(self.f5, v).pack(fill=tk.X, padx=5, pady=2)
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user