From 944c2694fd30b74a746bfe91b22f1817690baf9e Mon Sep 17 00:00:00 2001 From: VALLONGOL Date: Wed, 17 Dec 2025 10:48:40 +0100 Subject: [PATCH] sistemato visualizzazione details messaggi --- pybusmonitor1553/gui/monitor.py | 401 +++++++++++++++++++++++++++----- 1 file changed, 348 insertions(+), 53 deletions(-) diff --git a/pybusmonitor1553/gui/monitor.py b/pybusmonitor1553/gui/monitor.py index daab2b7..a075464 100644 --- a/pybusmonitor1553/gui/monitor.py +++ b/pybusmonitor1553/gui/monitor.py @@ -63,6 +63,8 @@ class MonitorApp(tk.Frame): self.import_error = None self.create_widgets() self.update_loop_running = False + # cache tree item ids by message label for incremental updates + self._tree_items = {} # No background bind or retry at startup; user must press Initialize. def create_widgets(self): @@ -132,8 +134,19 @@ class MonitorApp(tk.Frame): details_frame = tk.LabelFrame(middle, text="Details") details_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=False) tk.Label(details_frame, text="Message details:").pack(anchor=tk.W) - self.detail_text = scrolledtext.ScrolledText(details_frame, width=40, height=18) - self.detail_text.pack(fill=tk.BOTH, expand=True, padx=6, pady=6) + # Use a Treeview for stable tabular details: parameter | value + self.detail_tree = ttk.Treeview(details_frame, columns=("param", "value"), show="headings", height=18) + self.detail_tree.heading("param", text="Parameter") + self.detail_tree.heading("value", text="Value") + self.detail_tree.column("param", width=180) + self.detail_tree.column("value", width=320) + vsb = ttk.Scrollbar(details_frame, orient=tk.VERTICAL, command=self.detail_tree.yview) + self.detail_tree.configure(yscrollcommand=vsb.set) + self.detail_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(6,0), pady=6) + vsb.pack(side=tk.RIGHT, fill=tk.Y, padx=(0,6), pady=6) + # map param name -> item id for quick updates + self.detail_rows = {} + self.detail_selected_label = None # Bottom: Log area log_frame = tk.LabelFrame(self, text="Log") @@ -232,6 +245,167 @@ class MonitorApp(tk.Frame): self.import_error = str(e) return None + def _fmt_enum(self, val, enum_names): + """Format a numeric `val` with possible enum name(s). + + `enum_names` is a list of candidate enum class names to try in the + `Grifo_E_1553lib.data_types.enums` module. Returns a string like + "3 (RWS)" or the original value if no mapping found. + """ + try: + import importlib + enums_mod = None + try: + enums_mod = importlib.import_module('Grifo_E_1553lib.data_types.enums') + except Exception: + try: + enums_mod = importlib.import_module('pybusmonitor1553.Grifo_E_1553lib.data_types.enums') + except Exception: + enums_mod = None + if enums_mod is None: + return str(val) + for name in enum_names: + enum_cls = getattr(enums_mod, name, None) + if enum_cls is not None: + try: + # if val is already enum instance, get value + v = int(val) + try: + enum_name = enum_cls(v).name + except Exception: + enum_name = str(getattr(enum_cls, v, '')) + return f"{v} ({enum_name})" + except Exception: + continue + except Exception: + pass + return str(val) + + def _format_ctypes_obj(self, obj, indent=0, max_depth=3): + """Recursively format ctypes-backed message fields into readable text. + + - Prints simple ctypes numbers as integers + - For nested objects, recurses up to `max_depth` + - Uses `_fmt_enum` to annotate enum-like values where possible + """ + pad = ' ' * indent + out_lines = [] + if obj is None: + return f"{pad}\n" + # primitive types + try: + # ctypes scalar like c_uint16 etc. -> has value attribute or can be int() + if hasattr(obj, 'value') and not hasattr(obj, '__dict__'): + try: + return f"{pad}{int(obj.value)}\n" + except Exception: + return f"{pad}{obj}\n" + # plain int/float/str + if isinstance(obj, (int, float, str)): + return f"{pad}{obj}\n" + except Exception: + pass + + # If object has a 'raw' attribute, prefer that as a primitive value + if hasattr(obj, 'raw') and not hasattr(obj, '__dict__'): + try: + raw = int(obj.raw) + # Try mapping by class name + cls_name = obj.__class__.__name__ + enum_str = self._fmt_enum(raw, [cls_name]) + return f"{pad}{raw} ({enum_str if enum_str!=str(raw) else cls_name})\n" + except Exception: + return f"{pad}{repr(obj)}\n" + + # If object is a ctypes-like struct or has attributes, iterate fields + if max_depth <= 0: + return f"{pad}{obj}\n" + + # Try to iterate public attributes + names = [n for n in dir(obj) if not n.startswith('_')] + simple_fields = [] + for n in names: + try: + v = getattr(obj, n) + except Exception: + continue + # skip methods + if callable(v): + continue + # skip module/class descriptors + if isinstance(v, (type,)): + continue + # format recursively + try: + if hasattr(v, '__dict__') or hasattr(v, 'raw') or isinstance(v, (int, float, str)): + nested = self._format_ctypes_obj(v, indent=indent+1, max_depth=max_depth-1) + simple_fields.append(f"{pad}{n}:\n{nested}") + else: + # fallback repr + simple_fields.append(f"{pad}{n}: {v}\n") + except Exception: + simple_fields.append(f"{pad}{n}: \n") + + if simple_fields: + return '\n'.join(simple_fields) + '\n' + return f"{pad}{repr(obj)}\n" + + def _format_value_for_table(self, val): + """Return a short string suitable for the details table's value column.""" + try: + # simple primitives + if val is None: + return "" + if isinstance(val, (int, float, str)): + return str(val) + # prefer common ctypes attrs + if hasattr(val, 'raw'): + try: + return self._fmt_enum(int(getattr(val, 'raw')), [val.__class__.__name__]) + except Exception: + try: + return str(getattr(val, 'raw')) + except Exception: + return repr(val) + if hasattr(val, 'value'): + try: + return str(int(val.value)) + except Exception: + try: + return str(val.value) + except Exception: + return repr(val) + # fallback: attempt a shallow ctypes-style formatting + try: + s = self._format_ctypes_obj(val, indent=0, max_depth=1) + return s.strip().splitlines()[0] + except Exception: + return repr(val) + except Exception: + return '' + + def _add_detail_row(self, name: str, value: str): + """Insert or update a row in the details table (param -> value).""" + try: + if name in self.detail_rows: + iid = self.detail_rows[name] + try: + # keep the param column stable + self.detail_tree.item(iid, values=(name, value)) + return iid + except Exception: + pass + # insert new row + iid = self.detail_tree.insert('', tk.END, values=(name, value)) + self.detail_rows[name] = iid + return iid + except Exception: + try: + # best-effort: insert without tracking + return self.detail_tree.insert('', tk.END, values=(name, str(value))) + except Exception: + return None + def on_init(self): # On-demand import/creation of the connection manager when the user requests initialization. @@ -424,16 +598,14 @@ class MonitorApp(tk.Frame): pass def refresh_messages(self): - self.tree.delete(*self.tree.get_children()) + # Incremental tree update: preserve selection and avoid full redraw try: mdb = self._get_message_db() if mdb is None: - # no message DB available (import error) -> nothing to show return msgs = mdb.getAllMessages() lbl_filter = self.filter_entry.get().strip().lower() - pmin = None - pmax = None + pmin = pmax = None try: if self.period_min.get().strip() != "": pmin = float(self.period_min.get().strip()) @@ -442,55 +614,91 @@ class MonitorApp(tk.Frame): except Exception: pmin = pmax = None + # preserve current selection label (if any) + cur_sel = None + sel = self.tree.selection() + if sel: + try: + cur_item = sel[0] + cur_vals = self.tree.item(cur_item, 'values') + if cur_vals: + cur_sel = cur_vals[0] + except Exception: + cur_sel = None + + # mark seen labels to remove stale rows afterwards + seen = set() + for k, v in msgs.items(): if lbl_filter and lbl_filter not in k.lower(): continue - # cw raw - cw_raw = None - wc = '' + + # compute display fields try: cw_raw = getattr(v.head.cw, 'raw', None) wc = getattr(v.head.cw.str, 'wc', '') except Exception: cw_raw = None - - # sw - sw_raw = '' + wc = '' try: sw_raw = getattr(v.head, 'sw', '') except Exception: sw_raw = '' - - # num -> sent_count (how many times this message was sent) num = getattr(v, 'sent_count', getattr(v, 'size', '')) - # errs - errs = '' try: errs = getattr(v.head, 'errcode', '') except Exception: errs = '' - - # period (ms) period = getattr(v, '_time_ms', None) if period is None: - # try compute from freq freq = getattr(v, 'freq', None) if freq: period = 1000.0 / freq - if period is not None and pmin is not None and period < pmin: continue if period is not None and pmax is not None and period > pmax: continue - - # mc -> recv_count (how many times we received this message) - mc = '' try: mc = getattr(v, 'recv_count', '') except Exception: mc = '' - self.tree.insert('', tk.END, values=(k, hex(cw_raw) if cw_raw is not None else '', sw_raw, num, errs, f"{period:.3f}" if period else '', wc, mc)) + values = (k, hex(cw_raw) if cw_raw is not None else '', sw_raw, num, errs, f"{period:.3f}" if period else '', wc, mc) + + # update existing row or insert new one + if k in self._tree_items and self._tree_items[k] in self.tree.get_children(''): + try: + self.tree.item(self._tree_items[k], values=values) + except Exception: + # fallback: recreate item + try: + self.tree.delete(self._tree_items[k]) + except Exception: + pass + iid = self.tree.insert('', tk.END, values=values) + self._tree_items[k] = iid + else: + iid = self.tree.insert('', tk.END, values=values) + self._tree_items[k] = iid + + seen.add(k) + + # remove stale items not present anymore + stale = [lbl for lbl in list(self._tree_items.keys()) if lbl not in seen] + for lbl in stale: + try: + self.tree.delete(self._tree_items[lbl]) + except Exception: + pass + del self._tree_items[lbl] + + # restore selection if possible + if cur_sel and cur_sel in self._tree_items: + try: + self.tree.selection_set(self._tree_items[cur_sel]) + except Exception: + pass + except Exception: pass @@ -506,25 +714,82 @@ class MonitorApp(tk.Frame): self.show_message_detail(label) def show_message_detail(self, label: str): - self.detail_text.delete('1.0', tk.END) + # build table for this label; detail_tree will be populated and updated + try: + self.detail_tree.delete(*self.detail_tree.get_children()) + except Exception: + pass + self.detail_rows = {} + self.detail_selected_label = label try: mdb = self._get_message_db() if mdb is None: - self.detail_text.insert(tk.END, f"Message DB not available (import error). Open Diagnostics for details.\n") + try: + self.detail_tree.insert('', tk.END, values=("Message DB not available", "Open Diagnostics for details.")) + except Exception: + pass + try: + self.log.insert(tk.END, "Message DB not available (import error). Open Diagnostics for details.\n") + except Exception: + pass return - msg_wrapper = mdb.getMessage(label) - except Exception: - self.detail_text.insert(tk.END, f"Message {label} not found in MessageDB\n") + return + # Try direct lookup first + try: + msg_wrapper = mdb.getMessage(label) + except Exception: + # Try alternative access patterns: getAllMessages() dict lookup + try: + allm = mdb.getAllMessages() + # exact key + if label in allm: + msg_wrapper = allm[label] + else: + # case-insensitive match or startswith + found = None + for k in allm.keys(): + if k.lower() == label.lower() or k.startswith(label): + found = allm[k] + break + msg_wrapper = found + except Exception: + msg_wrapper = None + except Exception as e: + # log diagnostic to the UI log and detail pane + try: + self.log.insert(tk.END, f"show_message_detail lookup error: {e}\n") + except Exception: + pass + self.detail_text.insert(tk.END, f"Message {label} not found in MessageDB (error).\n") return if not msg_wrapper: - self.detail_text.insert(tk.END, f"No data for {label}\n") + try: + self.detail_tree.insert('', tk.END, values=("No data", f"{label}")) + except Exception: + pass + try: + self.log.insert(tk.END, f"No wrapper found for {label} in MessageDB\n") + except Exception: + pass + return + try: + self.log.insert(tk.END, f"No wrapper found for {label} in MessageDB\n") + except Exception: + pass return # The actual ctypes message instance msg = getattr(msg_wrapper, 'message', None) if msg is None: - self.detail_text.insert(tk.END, "No message field\n") + try: + self.detail_tree.insert('', tk.END, values=("No message", "")) + except Exception: + pass + try: + self.log.insert(tk.END, f"Wrapper for {label} has no 'message' attribute\n") + except Exception: + pass return # Handle known tellbacks @@ -538,16 +803,21 @@ class MonitorApp(tk.Frame): mm = rb.get_master_mode() except Exception: mm = getattr(rb, 'raw', rb) - self.detail_text.insert(tk.END, f"B7 - RdrStatusTellback:\n") - self.detail_text.insert(tk.END, f" master_mode: {mm}\n") + try: + mm_str = self._fmt_enum(mm, ['RdrModes', 'RdrModes']) + except Exception: + mm_str = str(mm) + self._add_detail_row('master_mode', mm_str) try: des = rb.get_des_ctrl() - self.detail_text.insert(tk.END, f" designation_ctrl: {des}\n") + des_str = self._fmt_enum(des, ['DesControl', 'DesignationStatus']) + self._add_detail_row('designation_ctrl', des_str) except Exception: pass try: ib = rb.get_ibit() - self.detail_text.insert(tk.END, f" ibit: {ib}\n") + ib_str = self._fmt_enum(ib, ['IbitRequest', 'BITReportAvailable']) + self._add_detail_row('ibit', ib_str) except Exception: pass @@ -562,48 +832,73 @@ class MonitorApp(tk.Frame): sym = st.get_sym_intensity() except Exception: sym = '' - self.detail_text.insert(tk.END, f"B6 - RdrSettingsTellback:\n") - self.detail_text.insert(tk.END, f" history_level: {hist}\n") - self.detail_text.insert(tk.END, f" symbology_intensity: {sym}\n") + self._add_detail_row('history_level', self._fmt_enum(hist, ['TargetHistory'])) + self._add_detail_row('symbology_intensity', str(sym)) # param1/param2 fields (if present) try: if hasattr(msg, 'param1_tellback'): p1 = msg.param1_tellback - # try common getters - out = [] - for fn in ('get_rws_submode', 'get_spot', 'get_acm_submode', 'get_gm_submode', 'get_expand', 'get_range_scale', 'get_bars_num', 'get_scan_width'): + for fn, enum_names in ( + ('get_rws_submode', ['RwsSubmode']), + ('get_spot', ['SpotSelection','SpotSelection']), + ('get_acm_submode', ['AcmSubmode']), + ('get_gm_submode', ['GmSubmode','GmSubmode']), + ('get_expand', ['Expand']), + ('get_range_scale', ['RangeScale']), + ('get_bars_num', ['BarsNum']), + ('get_scan_width', ['ScanWidth','AzimuthScanWidth']), + ): if hasattr(p1, fn): try: - out.append(f" {fn}: {getattr(p1, fn)()}" ) + v = getattr(p1, fn)() + v_str = self._fmt_enum(v, enum_names) + self._add_detail_row(fn, v_str) except Exception: pass - if out: - self.detail_text.insert(tk.END, "param1:\n") - self.detail_text.insert(tk.END, "\n".join(out) + "\n") except Exception: pass try: if hasattr(msg, 'param2_tellback'): p2 = msg.param2_tellback - self.detail_text.insert(tk.END, f"param2 raw: {getattr(p2,'raw', '')}\n") + self._add_detail_row('param2_raw', str(getattr(p2,'raw', ''))) except Exception: pass return # Default: dump raw fields of ctypes message - self.detail_text.insert(tk.END, f"Raw message fields for {label}:\n") - for name in dir(msg): - if name.startswith('_'): - continue + self.detail_tree.insert('', tk.END, values=(f"# {label}", "")) + # default: show top-level public attributes as rows + try: + for name in dir(msg): + if name.startswith('_'): + continue + try: + val = getattr(msg, name) + except Exception: + continue + if callable(val): + continue + # add row and remember item id + vstr = self._format_value_for_table(val) + iid = self.detail_tree.insert('', tk.END, values=(name, vstr)) + self.detail_rows[name] = iid + except Exception as e: + # fallback: ensure at least one row try: - val = getattr(msg, name) - self.detail_text.insert(tk.END, f"{name}: {val}\n") + self.detail_tree.insert('', tk.END, values=("", str(e))) except Exception: pass except Exception as e: - self.detail_text.insert(tk.END, f"Error while decoding: {e}\n") + try: + self.detail_tree.insert('', tk.END, values=("", str(e))) + except Exception: + pass + try: + self.log.insert(tk.END, f"Error while decoding: {e}\n") + except Exception: + pass def periodic_update(self): while self.manager.is_running():