Live Editor: add DASP/Sox device connection with full CRUD and COV
Port the DASP transport and Sox protocol stack from sedonaOntrol Java to Python, enabling live device editing alongside the existing offline SAX editor. Lazy-loads components on navigation, caches them, and subscribes to COV events for the active wiresheet. New files: dasp.py, sox.py, device_session.py, connect_dialog.py, live_hooks.py. Modified: app_state.py (live-mode state), main.py (Device menu, COV polling, lazy tree expand), properties.py (Sox write-through), sax_file.py (structural mutations route through live_hooks), schema.py (find_manifest helper). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,169 @@
|
||||
"""
|
||||
Connection dialog for connecting to a live Sedona device.
|
||||
|
||||
Modal Toplevel with host, port, username, password fields.
|
||||
Connect runs in a background thread; progress and errors shown inline.
|
||||
"""
|
||||
|
||||
import configparser
|
||||
import threading
|
||||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
|
||||
import app_state
|
||||
|
||||
|
||||
class ConnectDialog:
|
||||
"""Modal dialog for entering device connection parameters."""
|
||||
|
||||
def __init__(self, parent, on_connect_callback):
|
||||
self._callback = on_connect_callback
|
||||
self._thread = None
|
||||
self._cancelled = False
|
||||
|
||||
self._dlg = tk.Toplevel(parent)
|
||||
self._dlg.title("Connect to Device")
|
||||
self._dlg.resizable(False, False)
|
||||
self._dlg.transient(parent)
|
||||
self._dlg.grab_set()
|
||||
|
||||
host, port, user = _load_last_connection()
|
||||
|
||||
frame = ttk.Frame(self._dlg, padding=16)
|
||||
frame.pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
row = 0
|
||||
ttk.Label(frame, text="Host:").grid(row=row, column=0, sticky="e", padx=(0, 8), pady=4)
|
||||
self._host_var = tk.StringVar(value=host)
|
||||
self._host_entry = ttk.Entry(frame, textvariable=self._host_var, width=28)
|
||||
self._host_entry.grid(row=row, column=1, columnspan=2, sticky="ew", pady=4)
|
||||
|
||||
row += 1
|
||||
ttk.Label(frame, text="Port:").grid(row=row, column=0, sticky="e", padx=(0, 8), pady=4)
|
||||
self._port_var = tk.StringVar(value=port)
|
||||
ttk.Entry(frame, textvariable=self._port_var, width=8).grid(
|
||||
row=row, column=1, sticky="w", pady=4)
|
||||
|
||||
row += 1
|
||||
ttk.Label(frame, text="Username:").grid(row=row, column=0, sticky="e", padx=(0, 8), pady=4)
|
||||
self._user_var = tk.StringVar(value=user)
|
||||
ttk.Entry(frame, textvariable=self._user_var, width=28).grid(
|
||||
row=row, column=1, columnspan=2, sticky="ew", pady=4)
|
||||
|
||||
row += 1
|
||||
ttk.Label(frame, text="Password:").grid(row=row, column=0, sticky="e", padx=(0, 8), pady=4)
|
||||
self._pass_var = tk.StringVar()
|
||||
ttk.Entry(frame, textvariable=self._pass_var, width=28, show="*").grid(
|
||||
row=row, column=1, columnspan=2, sticky="ew", pady=4)
|
||||
|
||||
row += 1
|
||||
self._status_label = ttk.Label(frame, text="", foreground="#666666")
|
||||
self._status_label.grid(row=row, column=0, columnspan=3, sticky="w", pady=(8, 4))
|
||||
|
||||
row += 1
|
||||
btn_frame = ttk.Frame(frame)
|
||||
btn_frame.grid(row=row, column=0, columnspan=3, sticky="e", pady=(8, 0))
|
||||
|
||||
self._btn_connect = ttk.Button(btn_frame, text="Connect", command=self._do_connect)
|
||||
self._btn_connect.pack(side=tk.RIGHT, padx=(8, 0))
|
||||
|
||||
self._btn_cancel = ttk.Button(btn_frame, text="Cancel", command=self._do_cancel)
|
||||
self._btn_cancel.pack(side=tk.RIGHT)
|
||||
|
||||
self._host_entry.focus_set()
|
||||
self._dlg.bind("<Return>", lambda e: self._do_connect())
|
||||
self._dlg.bind("<Escape>", lambda e: self._do_cancel())
|
||||
|
||||
self._dlg.update_idletasks()
|
||||
x = parent.winfo_x() + (parent.winfo_width() - self._dlg.winfo_width()) // 2
|
||||
y = parent.winfo_y() + (parent.winfo_height() - self._dlg.winfo_height()) // 2
|
||||
self._dlg.geometry(f"+{x}+{y}")
|
||||
|
||||
def _do_connect(self):
|
||||
host = self._host_var.get().strip()
|
||||
port = self._port_var.get().strip()
|
||||
user = self._user_var.get().strip()
|
||||
password = self._pass_var.get()
|
||||
|
||||
if not host:
|
||||
self._status_label.config(text="Host is required", foreground="#c00000")
|
||||
return
|
||||
|
||||
try:
|
||||
port_int = int(port)
|
||||
except ValueError:
|
||||
self._status_label.config(text="Invalid port number", foreground="#c00000")
|
||||
return
|
||||
|
||||
self._btn_connect.config(state="disabled")
|
||||
self._status_label.config(text="Connecting...", foreground="#666666")
|
||||
|
||||
self._cancelled = False
|
||||
self._thread = threading.Thread(
|
||||
target=self._connect_worker,
|
||||
args=(host, port_int, user, password),
|
||||
daemon=True
|
||||
)
|
||||
self._thread.start()
|
||||
self._poll_connect()
|
||||
|
||||
def _connect_worker(self, host, port, user, password):
|
||||
try:
|
||||
result = self._callback(host, port, user, password)
|
||||
self._result = ("ok", result, host, port, user)
|
||||
except Exception as e:
|
||||
self._result = ("error", str(e), host, port, user)
|
||||
|
||||
def _poll_connect(self):
|
||||
if self._cancelled:
|
||||
return
|
||||
if self._thread is not None and self._thread.is_alive():
|
||||
self._dlg.after(100, self._poll_connect)
|
||||
return
|
||||
|
||||
status, *rest = self._result
|
||||
if status == "ok":
|
||||
_, host, port, user = rest
|
||||
_save_last_connection(host, str(port), user)
|
||||
self._dlg.destroy()
|
||||
else:
|
||||
msg = rest[0]
|
||||
self._status_label.config(text=f"Error: {msg}", foreground="#c00000")
|
||||
self._btn_connect.config(state="normal")
|
||||
|
||||
def _do_cancel(self):
|
||||
self._cancelled = True
|
||||
self._dlg.destroy()
|
||||
|
||||
|
||||
def _load_last_connection():
|
||||
"""Reads last connection params from editor.properties."""
|
||||
config = configparser.ConfigParser()
|
||||
host = "192.168.1.100"
|
||||
port = "1876"
|
||||
user = "admin"
|
||||
try:
|
||||
if app_state.PROPERTIES_FILE:
|
||||
config.read(app_state.PROPERTIES_FILE)
|
||||
host = config.get("Device", "host", fallback=host)
|
||||
port = config.get("Device", "port", fallback=port)
|
||||
user = config.get("Device", "user", fallback=user)
|
||||
except Exception:
|
||||
pass
|
||||
return host, port, user
|
||||
|
||||
|
||||
def _save_last_connection(host, port, user):
|
||||
"""Saves connection params to editor.properties."""
|
||||
config = configparser.ConfigParser()
|
||||
try:
|
||||
config.read(app_state.PROPERTIES_FILE)
|
||||
if "Device" not in config:
|
||||
config["Device"] = {}
|
||||
config["Device"]["host"] = host
|
||||
config["Device"]["port"] = port
|
||||
config["Device"]["user"] = user
|
||||
with open(app_state.PROPERTIES_FILE, "w") as f:
|
||||
config.write(f)
|
||||
except Exception:
|
||||
pass
|
||||
Reference in New Issue
Block a user