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>
This commit is contained in:
2026-09-19 13:28:17 +03:00
co-authored by Claude Opus 4.6
parent f3692e4587
commit ef5437ed36
3 changed files with 169 additions and 4 deletions
+121 -3
View File
@@ -1,8 +1,10 @@
"""
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.
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
@@ -20,6 +22,7 @@ class ConnectDialog:
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")
@@ -56,6 +59,49 @@ class ConnectDialog:
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))
@@ -79,6 +125,77 @@ class ConnectDialog:
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()
@@ -96,6 +213,7 @@ class ConnectDialog:
return
self._btn_connect.config(state="disabled")
self._btn_discover.config(state="disabled")
self._status_label.config(text="Connecting...", foreground="#666666")
self._cancelled = False
@@ -134,11 +252,11 @@ class ConnectDialog:
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():
# Connection attempt in progress — clean up after it finishes
import app_state as _as
def _cleanup():
self._thread.join(timeout=5)