aggiunta la schermata di debug del canale 1553 nel menu

This commit is contained in:
VALLONGOL 2026-01-12 11:05:01 +01:00
parent 18f3aba5c6
commit ac843839bb
4 changed files with 469 additions and 14 deletions

View File

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

View File

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

View File

@ -51,15 +51,24 @@ except Exception:
class MonitorApp(tk.Frame):
def __init__(self, master=None):
def __init__(self, master=None, bus_monitor=None, use_main_logger=False):
"""MonitorApp GUI.
Args:
master: Tk parent
bus_monitor: Optional existing BusMonitorCore instance to monitor
use_main_logger: If True, do not initialize the local TkinterLogger
and instead use the main application's logging.
"""
super().__init__(master)
self.master = master
self.pack(fill=tk.BOTH, expand=True)
# Use BusMonitorCore (ARTOS API) - same as ARTOS Collector will use
# The application will remain idle until the user presses `Initialize`.
self.bus_monitor = None
# Use an existing BusMonitorCore instance if provided (preferred)
self.bus_monitor = bus_monitor
self.import_error = None
# If True, avoid creating a local TkinterLogger (use main GUI logger)
self._use_main_logger = bool(use_main_logger)
self.create_widgets()
self.update_loop_running = False
# cache tree item ids by message label for incremental updates
@ -170,9 +179,14 @@ class MonitorApp(tk.Frame):
# Status bar will be added below the whole UI (outside all frames)
# Initialize external TkinterLogger if available, attach text widget as handler
# Initialize logging. If we're embedded in the main GUI, use its logger
# (prevents creating a second TkinterLogger and duplicate GUI handlers).
self.logger_system = None
try:
if getattr(self, '_use_main_logger', False):
# Use main application's ARTOS logger so logs go to main GUI
self.logger = logging.getLogger('ARTOS')
else:
if TkinterLogger is not None:
self.logger_system = TkinterLogger(self.master)
self.logger_system.setup(enable_console=True, enable_tkinter=True)

View File

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