import tkinter as tk from tkinter import ttk import queue # <--- AGGIUNTA import time from typing import TYPE_CHECKING from pymsc.core.introspection import inspect_structure if TYPE_CHECKING: from ..core.app_controller import AppController class MainWindow: def __init__(self, root: tk.Tk, controller: "AppController"): self.root = root self.controller = controller # Flag GUI: use new raw monitor buffer (off by default) self.use_raw_buffer_var = tk.BooleanVar(value=False) style = ttk.Style() style.theme_use('clam') self.notebook = ttk.Notebook(root) self.notebook.pack(fill='both', expand=True, padx=5, pady=5) self.tab_control = ttk.Frame(self.notebook) self.notebook.add(self.tab_control, text='Control Panel') self._init_control_tab() self.tab_monitor = ttk.Frame(self.notebook) self.notebook.add(self.tab_monitor, text='Bus Monitor') self._init_monitor_tab() self.tab_settings = ttk.Frame(self.notebook) self.notebook.add(self.tab_settings, text='Settings') self._init_settings_tab() self.status_var = tk.StringVar() self.status_var.set("Ready") self.status_bar = ttk.Label(root, textvariable=self.status_var, relief=tk.SUNKEN, anchor=tk.W) self.status_bar.pack(side=tk.BOTTOM, fill=tk.X) self._refresh_msg_list() # AVVIO POLLING CODA self._poll_queue() # <--- AVVIO POLLING def _poll_queue(self): """Controlla se ci sono nuovi messaggi dal Core da mostrare nel Monitor.""" try: if self.use_raw_buffer_var.get(): # Consume MonitorBuffer snapshot (clears buffer in controller) try: snap = self.controller.getMonitorRawBuffer() # snap.buffer is list of MonDecoded-like objects for md in reversed(list(getattr(snap, 'buffer', []))): # Build display row: time, label, direction, snippet ts = time.strftime("%H:%M:%S", time.localtime(md.timetag)) sa = getattr(md, 'sa', 0) label = f"A{sa}" if 1 <= sa <= 8 else (f"B{sa-10}" if 11 <= sa <= 28 else f"UNKNOWN({sa})") dir_str = "RT->BC" if getattr(md, 'tr', 0) == 1 else "BC->RT" snippet = (getattr(md, 'data', b'').hex().upper())[:240] self.tree.insert("", 0, values=(ts, label, dir_str, snippet)) if len(self.tree.get_children()) > 50: self.tree.delete(self.tree.get_children()[-1]) except Exception: pass else: # Process up to 100 messages per cycle to avoid UI freeze processed = 0 while processed < 100: # (timestamp, label, direction, snippet) item = self.controller.monitor_queue.get_nowait() # Skip FROM entries to reduce clutter if len(item) > 1 and not str(item[1]).startswith('FROM'): self.tree.insert("", 0, values=item) # Keep detail log small (max 50 entries) if len(self.tree.get_children()) > 50: self.tree.delete(self.tree.get_children()[-1]) # DEBUG print(f"[GUI] Displayed: {item[1]} - {item[2]}") processed += 1 except queue.Empty: pass finally: # Refresh summary and reschedule after 250ms (slower to reduce CPU load) try: self._refresh_summary() except Exception: pass self.root.after(250, self._poll_queue) def _refresh_summary(self): """Aggiorna la vista riepilogativa con le statistiche per SA dal controller.""" try: stats = getattr(self.controller, 'stats', {}) or {} except Exception: stats = {} # Map existing items by name for efficient update existing = {} for iid in self.summary_tree.get_children(): vals = self.summary_tree.item(iid, 'values') if vals: existing[vals[0]] = iid # Insert/update rows sorted by SA for sa, st in sorted(stats.items()): name = st.get('name', f'A{sa}') cw = f"{st.get('rt', '')}-R-{sa}-{st.get('wc', '')}" sw = f"{st.get('last_sw') if st.get('last_sw') is not None else 0}" num = st.get('count', 0) errs = st.get('last_err', 0) or 0 period = f"{st.get('period_ms'):.1f}" if st.get('period_ms') else "-" wc = st.get('wc', '') rt = st.get('rt', '') values = (name, cw, sw, num, errs, period, wc, rt) if name in existing: self.summary_tree.item(existing[name], values=values) else: self.summary_tree.insert('', 'end', values=values) # Optionally remove rows that no longer exist in stats for name, iid in list(existing.items()): if not any(name == st.get('name', f'A{sa}') for sa, st in stats.items()): self.summary_tree.delete(iid) def _init_control_tab(self): # Toolbar frame in alto con pulsanti di controllo toolbar = ttk.Frame(self.tab_control) toolbar.pack(fill='x', padx=5, pady=5) # Pulsante per inizializzare il radar self.btn_init_radar = ttk.Button( toolbar, text="🔧 Initialize Radar", command=self._on_init_radar ) self.btn_init_radar.pack(side='left', padx=5) # Pulsanti Run / Stop per avviare e fermare l'invio periodico (replica C++ Run) self.btn_run = ttk.Button(toolbar, text="▶ Run", command=self._on_run) self.btn_run.pack(side='left', padx=5) self.btn_stop = ttk.Button(toolbar, text="⏸ Stop", command=self._on_stop) self.btn_stop.pack(side='left', padx=5) # Label stato inizializzazione self.lbl_radar_status = ttk.Label( toolbar, text="⚠ Radar not initialized", foreground="orange" ) self.lbl_radar_status.pack(side='left', padx=10) # ... (uguale a prima) ... paned = ttk.PanedWindow(self.tab_control, orient=tk.HORIZONTAL) paned.pack(fill='both', expand=True) frame_list = ttk.LabelFrame(paned, text="Active Messages", padding=5) paned.add(frame_list, weight=1) self.msg_listbox = tk.Listbox(frame_list) self.msg_listbox.pack(fill='both', expand=True) frame_params = ttk.LabelFrame(paned, text="Parameters", padding=5) paned.add(frame_params, weight=3) self.lbl_placeholder = ttk.Label(frame_params, text="Select a message to edit parameters") self.lbl_placeholder.pack(pady=20) self.msg_listbox.bind('<>', self._on_msg_select) self.param_canvas = tk.Canvas(frame_params) self.scrollbar = ttk.Scrollbar(frame_params, orient="vertical", command=self.param_canvas.yview) self.scrollable_frame = ttk.Frame(self.param_canvas) self.scrollable_frame.bind( "", lambda e: self.param_canvas.configure(scrollregion=self.param_canvas.bbox("all")) ) self.param_canvas.create_window((0, 0), window=self.scrollable_frame, anchor="nw") self.param_canvas.configure(yscrollcommand=self.scrollbar.set) self.param_canvas.pack(side="left", fill="both", expand=True) self.scrollbar.pack(side="right", fill="y") # ... (Il resto dei metodi _init_monitor_tab, _init_settings_tab, _refresh_msg_list, ecc. rimane invariato) ... # Ricordati di copiare tutto il resto del file main_window.py che hai già scritto/approvato. def _init_monitor_tab(self): # Summary frame on top (per-SA stats) - expanded to fill available space summary_frame = ttk.Frame(self.tab_monitor) summary_frame.pack(fill='both', expand=True, padx=5, pady=5) summary_cols = ("Name", "CW", "SW", "Num", "Errs", "Period (ms)", "WC", "RT") self.summary_tree = ttk.Treeview(summary_frame, columns=summary_cols, show='headings') for col in summary_cols: self.summary_tree.heading(col, text=col) # set reasonable widths if col == 'Name': self.summary_tree.column(col, width=140) elif col == 'CW': self.summary_tree.column(col, width=140) elif col == 'Period (ms)': self.summary_tree.column(col, width=90) else: self.summary_tree.column(col, width=70) summary_scrollbar = ttk.Scrollbar(summary_frame, orient=tk.VERTICAL, command=self.summary_tree.yview) self.summary_tree.configure(yscroll=summary_scrollbar.set) self.summary_tree.pack(side=tk.LEFT, fill='both', expand=True) summary_scrollbar.pack(side=tk.RIGHT, fill='y') # The detailed monitor (raw messages) below - reduced size columns = ("Time", "Label", "Direction", "Data Snippet") self.tree = ttk.Treeview(self.tab_monitor, columns=columns, show='headings', height=8) for col in columns: self.tree.heading(col, text=col) self.tree.column(col, width=100) self.tree.column("Data Snippet", width=300) self.tree.pack(fill='x', padx=5, pady=(0,5)) def _init_settings_tab(self): frm = ttk.Frame(self.tab_settings, padding=20) frm.pack(fill='both') ttk.Label(frm, text="UDP Target IP:").grid(row=0, column=0, sticky=tk.W, pady=5) self.ent_ip = ttk.Entry(frm) self.ent_ip.insert(0, self.controller.udp_ip) self.ent_ip.grid(row=0, column=1, sticky=tk.EW, pady=5) ttk.Label(frm, text="Send Port:").grid(row=1, column=0, sticky=tk.W, pady=5) self.ent_tx_port = ttk.Entry(frm) self.ent_tx_port.insert(0, str(self.controller.udp_send_port)) self.ent_tx_port.grid(row=1, column=1, sticky=tk.EW, pady=5) # Option: use new raw monitor buffer (keeps old queue as fallback) ttk.Checkbutton(frm, text="Use Raw Monitor Buffer", variable=self.use_raw_buffer_var).grid(row=2, column=0, columnspan=2, sticky=tk.W, pady=5) ttk.Button(frm, text="Apply & Restart Core", command=self._apply_settings).grid(row=3, column=0, columnspan=2, pady=20) def _apply_settings(self): self.status_var.set("Settings applied (Not implemented yet)") def _refresh_msg_list(self): self.msg_listbox.delete(0, tk.END) for label in self.controller.messages.keys(): self.msg_listbox.insert(tk.END, label) def _on_msg_select(self, event): selection = self.msg_listbox.curselection() if not selection: return label = self.msg_listbox.get(selection[0]) msg = self.controller.messages.get(label) if msg: self._build_dynamic_form(msg) def _build_dynamic_form(self, msg): for widget in self.scrollable_frame.winfo_children(): widget.destroy() structure_map = inspect_structure(msg.payload) self._render_node(self.scrollable_frame, structure_map, level=0) def _render_node(self, parent_widget, node, level=0): node_type = node.get("type") if node_type == "compound": for name, child in node["children"].items(): child_type = child.get("type") if child_type == "compound": lf = ttk.LabelFrame(parent_widget, text=name, padding=5) lf.pack(fill='x', padx=5 + (level*5), pady=2) self._render_node(lf, child, level + 1) elif child_type in ["enum", "primitive", "smart_value", "smart_angle"]: row = ttk.Frame(parent_widget) row.pack(fill='x', padx=10 + (level*5), pady=1) lbl = ttk.Label(row, text=name, width=20) lbl.pack(side='left') self._create_input_widget(row, child) elif child_type == "readonly": row = ttk.Frame(parent_widget) row.pack(fill='x', padx=10 + (level*5), pady=1) lbl = ttk.Label(row, text=name, width=20) lbl.pack(side='left') val_lbl = ttk.Label(row, text=str(child.get("value")), foreground="gray") val_lbl.pack(side='left') def _create_input_widget(self, parent, node): node_type = node.get("type") obj_ref = node.get("obj_ref") attr_name = node.get("attr_name") current_val = node.get("value") if node_type == "enum": enum_cls = node["enum_cls"] options = {e.name: e for e in enum_cls} combo = ttk.Combobox(parent, values=list(options.keys()), state="readonly") try: if hasattr(current_val, 'name'): combo.set(current_val.name) else: combo.set(str(current_val)) except: combo.set("INVALID") combo.pack(side='left', fill='x', expand=True) def on_combo_change(event): sel_name = combo.get() new_enum_val = options[sel_name] setattr(obj_ref, attr_name, new_enum_val.value) # print(f"Updated {attr_name} -> {new_enum_val}") combo.bind("<>", on_combo_change) elif node_type in ["primitive", "smart_value", "smart_angle"]: var = tk.StringVar(value=str(current_val)) entry = ttk.Entry(parent, textvariable=var) entry.pack(side='left', fill='x', expand=True) def on_entry_change(event): try: val_str = var.get() if node_type == "primitive": if isinstance(current_val, int): new_val = int(val_str) else: new_val = float(val_str) else: new_val = float(val_str) setattr(obj_ref, attr_name, new_val) entry.configure(foreground="black") # print(f"Updated {attr_name} -> {new_val}") except ValueError: entry.configure(foreground="red") entry.bind("", on_entry_change) entry.bind("", on_entry_change) elif node_type == "readonly": lbl = ttk.Label(parent, text=str(current_val), foreground="gray") lbl.pack(side='left') def _on_init_radar(self): """ Handler per il pulsante Initialize Radar. Chiama il metodo initialize_radar del controller. """ self.btn_init_radar.config(state='disabled', text="⏳ Initializing...") self.lbl_radar_status.config(text="⏳ Sending initialization commands...", foreground="blue") # Esegui in un thread separato per non bloccare la GUI def init_thread(): success = self.controller.initialize_radar() # Aggiorna GUI nel thread principale self.root.after(0, lambda: self._update_radar_status(success)) import threading threading.Thread(target=init_thread, daemon=True).start() def _update_radar_status(self, success): """ Aggiorna lo stato della GUI dopo l'inizializzazione. """ if success: self.btn_init_radar.config(state='normal', text="✓ Radar Initialized") self.lbl_radar_status.config(text="✓ Radar ready", foreground="green") self.status_var.set("Radar initialized successfully") else: self.btn_init_radar.config(state='normal', text="🔧 Initialize Radar") self.lbl_radar_status.config(text="✗ Initialization failed", foreground="red") self.status_var.set("Radar initialization failed - check console") def _on_run(self): """Start periodic transmissions (calls controller.go()).""" try: ok = self.controller.go() if ok: self.status_var.set("Running (periodic transmission enabled)") self.lbl_radar_status.config(text="▶ Running", foreground="green") except Exception as e: self.status_var.set(f"Run failed: {e}") def _on_stop(self): """Stop periodic transmissions (calls controller.stop_sending()).""" try: ok = self.controller.stop_sending() if ok: self.status_var.set("Stopped (periodic transmission disabled)") self.lbl_radar_status.config(text="⏸ Stopped", foreground="orange") except Exception as e: self.status_var.set(f"Stop failed: {e}")