From 7d15cf143ae0a582f9f9c87d1e6e7fa52618b608 Mon Sep 17 00:00:00 2001 From: VALLONGOL Date: Mon, 12 Jan 2026 12:38:47 +0100 Subject: [PATCH] sistemati anche i tooltip dei messaggi B --- layouts/user_layout.json | 2 +- pymsc/PyBusMonitor1553/gui/monitor.py | 98 +++++++++++++ pymsc/gui/command_registry.py | 2 +- pymsc/gui/components/command_widgets.py | 184 +++++++++++++++++++++++- pymsc/gui/pages/mission_page.py | 4 +- 5 files changed, 282 insertions(+), 8 deletions(-) diff --git a/layouts/user_layout.json b/layouts/user_layout.json index eb98b90..3d441c1 100644 --- a/layouts/user_layout.json +++ b/layouts/user_layout.json @@ -1,5 +1,5 @@ { - "window_geometry": "1600x1099+52+52", + "window_geometry": "1600x1099+298+121", "main_sash_position": [ 1, 793 diff --git a/pymsc/PyBusMonitor1553/gui/monitor.py b/pymsc/PyBusMonitor1553/gui/monitor.py index a95ad2f..be051f1 100644 --- a/pymsc/PyBusMonitor1553/gui/monitor.py +++ b/pymsc/PyBusMonitor1553/gui/monitor.py @@ -481,6 +481,11 @@ class MonitorApp(tk.Frame): row_fr.pack(fill=tk.X, padx=2, pady=1) lbl = tk.Label(row_fr, text=name, width=28, anchor=tk.W) 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 = '' # determine per-field editability: make mission date/time readonly root_field = full.split('.')[0] readonly_roots = ('date_of_mission', 'time_of_mission') @@ -524,6 +529,21 @@ class MonitorApp(tk.Frame): if editable: cb.bind('<>', lambda e, path=full: self._on_field_changed(path)) 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) else: # numeric or text entry @@ -550,6 +570,21 @@ class MonitorApp(tk.Frame): ent.bind('', lambda e, path=full: self._on_entry_finished(path)) ent.bind('', lambda e, path=full: self._on_entry_finished(path)) 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) # register created widget for later updates; values will be filled @@ -559,6 +594,69 @@ class MonitorApp(tk.Frame): except Exception: 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): """Mark entry as being edited to prevent refresh from overwriting user input.""" try: diff --git a/pymsc/gui/command_registry.py b/pymsc/gui/command_registry.py index b04223c..581eff5 100644 --- a/pymsc/gui/command_registry.py +++ b/pymsc/gui/command_registry.py @@ -432,7 +432,7 @@ LIST_CONTROLS = [ "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" }, { diff --git a/pymsc/gui/components/command_widgets.py b/pymsc/gui/components/command_widgets.py index d2b9eb2..097ed7b 100644 --- a/pymsc/gui/components/command_widgets.py +++ b/pymsc/gui/components/command_widgets.py @@ -11,6 +11,44 @@ from pymsc.utils.converters import ( 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) WIDGET_MAP: Dict[str, Any] = {} @@ -61,6 +99,20 @@ class CommandFrameCheckBox(BaseCommandFrame): width=self.column_widths[1] ) self.command_check.grid(row=0, column=1) + # attach tooltip on the interactive control: " - " + 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']) self.command_var.set(bool(initial_val)) @@ -71,6 +123,20 @@ class CommandFrameCheckBox(BaseCommandFrame): width=self.column_widths[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): # Skip if message is None (disabled field) @@ -137,6 +203,16 @@ class CommandFrameComboBox(BaseCommandFrame): ) self.combo.grid(row=0, column=1) self.combo.bind("<>", 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'): self.tb_var = tk.StringVar(value="---") @@ -145,6 +221,20 @@ class CommandFrameComboBox(BaseCommandFrame): width=self.column_widths[2], relief="sunken" ) 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): self.start_time = time.time() @@ -171,7 +261,12 @@ class CommandFrameComboBox(BaseCommandFrame): val_tb_text = self.info["values_tb"][idx_tb] except (IndexError, KeyError, TypeError): 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 if idx_tb == self.combo.current(): self.label.config(bg="green") @@ -242,6 +337,17 @@ class CommandFrameSpinBox(BaseCommandFrame): # Also use variable trace to detect arrow changes 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'): self.tb_var = tk.StringVar(value="0") self.tb_label = tk.Label( @@ -249,6 +355,20 @@ class CommandFrameSpinBox(BaseCommandFrame): width=self.column_widths[2], relief="sunken" ) 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): if p == "" or p == "-": @@ -335,7 +455,16 @@ class CommandFrameSpinBox(BaseCommandFrame): raw_tb = msg_tb.get_value_for_field(self.info['field_tb']) readable_tb = get_correct_value(self.info, -1, raw_tb) 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 target = float(self.cmd_var.get()) if abs(readable_tb - target) < 0.0001: @@ -412,7 +541,16 @@ class CommandFrameLabels(tk.Frame): raw = msg.get_value_for_field(field) val = get_correct_value(self.info, i, raw) 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: 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) ) 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]} elif ctrl_type == "combobox": @@ -467,6 +614,18 @@ class CommandFrameControls(tk.Frame): ) cmb.grid(row=0, column=i) cmb.bind("<>", 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]} elif ctrl_type == "spinbox": @@ -492,6 +651,18 @@ class CommandFrameControls(tk.Frame): # Detect arrow button clicks sp.bind("", lambda e, f=field, m=msg, idx=i: self._on_spin_arrow(f, m, idx)) 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": self.vars[field] = tk.StringVar(value="0") @@ -586,7 +757,12 @@ class CommandFrameControls(tk.Frame): except IndexError: self.vars[field].set("ERR") 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": self.vars[field].set("%.4f" % readable) self.is_programmatic_change = False \ No newline at end of file diff --git a/pymsc/gui/pages/mission_page.py b/pymsc/gui/pages/mission_page.py index 449796c..faedf78 100644 --- a/pymsc/gui/pages/mission_page.py +++ b/pymsc/gui/pages/mission_page.py @@ -26,7 +26,7 @@ class MissionPage(ScrollableFrame): self.f2 = ttk.LabelFrame(self.scrollable_content, text="Attitude (SNU)") self.f2.grid(row=1, column=0, padx=10, pady=5, sticky="nsew") atts = [ - "attitude", "true_heading", "magnetic_heading", + "attitude", "nav_data_invalid", "true_heading", "magnetic_heading", "platform_azimuth", "yaw_snu", "pitch_snu", "roll_snu" ] for a in atts: @@ -50,7 +50,7 @@ class MissionPage(ScrollableFrame): # 5. Velocities self.f5 = ttk.LabelFrame(self.scrollable_content, text="Velocities") 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: create_widget_by_id(self.f5, v).pack(fill=tk.X, padx=5, pady=2)