391 lines
17 KiB
Python
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}") |