From ac843839bb55670ecc9ee676590311f219f40744 Mon Sep 17 00:00:00 2001 From: VALLONGOL Date: Mon, 12 Jan 2026 11:05:01 +0100 Subject: [PATCH] aggiunta la schermata di debug del canale 1553 nel menu --- layouts/user_layout.json | 2 +- pymsc/PyBusMonitor1553/gui/debug_view.py | 391 +++++++++++++++++++++++ pymsc/PyBusMonitor1553/gui/monitor.py | 40 ++- pymsc/gui/main_docking_window.py | 50 +++ 4 files changed, 469 insertions(+), 14 deletions(-) create mode 100644 pymsc/PyBusMonitor1553/gui/debug_view.py diff --git a/layouts/user_layout.json b/layouts/user_layout.json index 08a5e0f..eb98b90 100644 --- a/layouts/user_layout.json +++ b/layouts/user_layout.json @@ -1,5 +1,5 @@ { - "window_geometry": "1600x1099+310+130", + "window_geometry": "1600x1099+52+52", "main_sash_position": [ 1, 793 diff --git a/pymsc/PyBusMonitor1553/gui/debug_view.py b/pymsc/PyBusMonitor1553/gui/debug_view.py new file mode 100644 index 0000000..b633aa6 --- /dev/null +++ b/pymsc/PyBusMonitor1553/gui/debug_view.py @@ -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('<>', 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 diff --git a/pymsc/PyBusMonitor1553/gui/monitor.py b/pymsc/PyBusMonitor1553/gui/monitor.py index f588297..a95ad2f 100644 --- a/pymsc/PyBusMonitor1553/gui/monitor.py +++ b/pymsc/PyBusMonitor1553/gui/monitor.py @@ -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__) diff --git a/pymsc/gui/main_docking_window.py b/pymsc/gui/main_docking_window.py index 825a6c6..8d62821 100644 --- a/pymsc/gui/main_docking_window.py +++ b/pymsc/gui/main_docking_window.py @@ -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):