aggiunta la schermata di debug del canale 1553 nel menu
This commit is contained in:
parent
18f3aba5c6
commit
ac843839bb
@ -1,5 +1,5 @@
|
||||
{
|
||||
"window_geometry": "1600x1099+310+130",
|
||||
"window_geometry": "1600x1099+52+52",
|
||||
"main_sash_position": [
|
||||
1,
|
||||
793
|
||||
|
||||
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,18 +179,23 @@ 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 TkinterLogger is not None:
|
||||
self.logger_system = TkinterLogger(self.master)
|
||||
self.logger_system.setup(enable_console=True, enable_tkinter=True)
|
||||
self.logger_system.add_tkinter_handler(self.log)
|
||||
self.logger = logging.getLogger(__name__)
|
||||
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:
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
self.logger = logging.getLogger(__name__)
|
||||
self.logger.info("TkinterLogger not available; using basic logging")
|
||||
if TkinterLogger is not None:
|
||||
self.logger_system = TkinterLogger(self.master)
|
||||
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:
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
self.logger = logging.getLogger(__name__)
|
||||
|
||||
@ -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):
|
||||
|
||||
Loading…
Reference in New Issue
Block a user