Files
sedona_editor/connect_dialog.py
T
ArdaandClaude Opus 4.6 dafebcebda 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>
2026-09-11 23:31:36 +03:00

170 lines
5.9 KiB
Python

"""
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