SXXXXXXX_PyMsc/pymsc/gui/main_window.py
2025-12-10 11:47:46 +01:00

391 lines
17 KiB
Python

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('<<ListboxSelect>>', 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(
"<Configure>",
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("<<ComboboxSelected>>", 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("<Return>", on_entry_change)
entry.bind("<FocusOut>", 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}")