Files
sedona_editor/connect_dialog.py
T
ArdaandClaude Opus 4.6 ef5437ed36 Add DASP device discovery to the connect dialog
Send DISCOVER broadcast (type 0) to 255.255.255.255 and the Sedona
multicast address 239.255.18.76 to find devices on the network.
Responses are listed in a Treeview showing IP and platform ID.
Selecting a device fills the Host field; double-click connects.
Wait time is configurable (default 10s, range 1-60s).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-09-19 13:28:17 +03:00

306 lines
12 KiB
Python

"""
Connection dialog for connecting to a live Sedona device.
Modal Toplevel with host, port, username, password fields and device
discovery. Connect runs in a background thread; progress and errors
shown inline. The Discover button sends a DASP broadcast and lists
responding devices so the user can pick one.
"""
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._discover_thread = None
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)
# ── Discovery section ──────────────────────────────────────
row += 1
sep = ttk.Separator(frame, orient="horizontal")
sep.grid(row=row, column=0, columnspan=3, sticky="ew", pady=(12, 4))
row += 1
disc_header = ttk.Frame(frame)
disc_header.grid(row=row, column=0, columnspan=3, sticky="ew", pady=(0, 4))
ttk.Label(disc_header, text="Discovered Devices:").pack(side=tk.LEFT)
self._btn_discover = ttk.Button(
disc_header, text="Discover", command=self._do_discover, width=10)
self._btn_discover.pack(side=tk.RIGHT)
self._wait_var = tk.StringVar(value="10")
wait_entry = ttk.Entry(disc_header, textvariable=self._wait_var, width=4,
justify="center")
wait_entry.pack(side=tk.RIGHT, padx=(0, 4))
ttk.Label(disc_header, text="Wait (s):").pack(side=tk.RIGHT, padx=(8, 2))
row += 1
list_frame = ttk.Frame(frame)
list_frame.grid(row=row, column=0, columnspan=3, sticky="nsew", pady=(0, 4))
frame.rowconfigure(row, weight=1)
columns = ("ip", "platform")
self._dev_tree = ttk.Treeview(
list_frame, columns=columns, show="headings", height=5, selectmode="browse")
self._dev_tree.heading("ip", text="IP Address")
self._dev_tree.heading("platform", text="Platform ID")
self._dev_tree.column("ip", width=140, minwidth=100)
self._dev_tree.column("platform", width=180, minwidth=100)
self._dev_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
scrollbar = ttk.Scrollbar(list_frame, orient="vertical", command=self._dev_tree.yview)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
self._dev_tree.configure(yscrollcommand=scrollbar.set)
self._dev_tree.bind("<<TreeviewSelect>>", self._on_device_selected)
self._dev_tree.bind("<Double-1>", lambda e: self._do_connect())
# ── Status and buttons ─────────────────────────────────────
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_cancel = ttk.Button(btn_frame, text="Cancel", command=self._do_cancel)
self._btn_cancel.pack(side=tk.RIGHT)
self._btn_connect = ttk.Button(btn_frame, text="Connect", command=self._do_connect)
self._btn_connect.pack(side=tk.RIGHT, padx=(0, 8))
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}")
# ── Discovery ──────────────────────────────────────────────────
def _do_discover(self):
try:
port_int = int(self._port_var.get().strip())
except ValueError:
port_int = 1876
try:
wait_time = max(1, min(60, int(self._wait_var.get().strip())))
except ValueError:
wait_time = 10
self._btn_discover.config(state="disabled")
self._status_label.config(text="Discovering devices...", foreground="#666666")
for item in self._dev_tree.get_children():
self._dev_tree.delete(item)
self._discover_thread = threading.Thread(
target=self._discover_worker, args=(port_int, wait_time), daemon=True)
self._discover_thread.start()
self._poll_discover()
def _discover_worker(self, port, wait_time):
from dasp import DaspSocket
sock = None
try:
sock = DaspSocket()
devices = sock.discover(port=port, wait_time=wait_time)
self._discover_result = ("ok", devices)
except Exception as e:
self._discover_result = ("error", str(e))
finally:
if sock is not None:
try:
sock.close()
except Exception:
pass
def _poll_discover(self):
if self._discover_thread is not None and self._discover_thread.is_alive():
self._dlg.after(100, self._poll_discover)
return
status, data = self._discover_result
self._btn_discover.config(state="normal")
if status == "ok":
if data:
for ip, platform_id in data:
self._dev_tree.insert("", tk.END, values=(ip, platform_id))
self._status_label.config(
text=f"Found {len(data)} device(s)", foreground="#006600")
else:
self._status_label.config(
text="No devices found", foreground="#666666")
else:
self._status_label.config(
text=f"Discovery error: {data}", foreground="#c00000")
def _on_device_selected(self, event):
sel = self._dev_tree.selection()
if not sel:
return
values = self._dev_tree.item(sel[0], "values")
if values:
self._host_var.set(values[0])
# ── Connect ────────────────────────────────────────────────────
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._btn_discover.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)
if self._cancelled:
return
self._result = ("ok", result, host, port, user)
except Exception as e:
if self._cancelled:
return
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")
self._btn_discover.config(state="normal")
def _do_cancel(self):
self._cancelled = True
if self._thread is not None and self._thread.is_alive():
import app_state as _as
def _cleanup():
self._thread.join(timeout=5)
if _as.device_session is not None and _as.live_mode:
try:
_as.device_session.disconnect()
except Exception:
pass
_as.live_mode = False
_as.device_session = None
_as.connection_status = "offline"
threading.Thread(target=_cleanup, daemon=True).start()
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