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:
+1
-1
@@ -8,7 +8,7 @@ so later rebinding would only ever be seen by the module that did it.
|
|||||||
import configparser
|
import configparser
|
||||||
import os
|
import os
|
||||||
|
|
||||||
VERSION = "0.0.0.096"
|
VERSION = "0.0.0.097"
|
||||||
PROPERTIES_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "editor.properties")
|
PROPERTIES_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "editor.properties")
|
||||||
DEFAULT_GRID_SIZE = 10
|
DEFAULT_GRID_SIZE = 10
|
||||||
DEFAULT_ADD_TIMESTAMP = True
|
DEFAULT_ADD_TIMESTAMP = True
|
||||||
|
|||||||
+121
-3
@@ -1,8 +1,10 @@
|
|||||||
"""
|
"""
|
||||||
Connection dialog for connecting to a live Sedona device.
|
Connection dialog for connecting to a live Sedona device.
|
||||||
|
|
||||||
Modal Toplevel with host, port, username, password fields.
|
Modal Toplevel with host, port, username, password fields and device
|
||||||
Connect runs in a background thread; progress and errors shown inline.
|
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 configparser
|
||||||
@@ -20,6 +22,7 @@ class ConnectDialog:
|
|||||||
self._callback = on_connect_callback
|
self._callback = on_connect_callback
|
||||||
self._thread = None
|
self._thread = None
|
||||||
self._cancelled = False
|
self._cancelled = False
|
||||||
|
self._discover_thread = None
|
||||||
|
|
||||||
self._dlg = tk.Toplevel(parent)
|
self._dlg = tk.Toplevel(parent)
|
||||||
self._dlg.title("Connect to Device")
|
self._dlg.title("Connect to Device")
|
||||||
@@ -56,6 +59,49 @@ class ConnectDialog:
|
|||||||
ttk.Entry(frame, textvariable=self._pass_var, width=28, show="*").grid(
|
ttk.Entry(frame, textvariable=self._pass_var, width=28, show="*").grid(
|
||||||
row=row, column=1, columnspan=2, sticky="ew", pady=4)
|
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
|
row += 1
|
||||||
self._status_label = ttk.Label(frame, text="", foreground="#666666")
|
self._status_label = ttk.Label(frame, text="", foreground="#666666")
|
||||||
self._status_label.grid(row=row, column=0, columnspan=3, sticky="w", pady=(8, 4))
|
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
|
y = parent.winfo_y() + (parent.winfo_height() - self._dlg.winfo_height()) // 2
|
||||||
self._dlg.geometry(f"+{x}+{y}")
|
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):
|
def _do_connect(self):
|
||||||
host = self._host_var.get().strip()
|
host = self._host_var.get().strip()
|
||||||
port = self._port_var.get().strip()
|
port = self._port_var.get().strip()
|
||||||
@@ -96,6 +213,7 @@ class ConnectDialog:
|
|||||||
return
|
return
|
||||||
|
|
||||||
self._btn_connect.config(state="disabled")
|
self._btn_connect.config(state="disabled")
|
||||||
|
self._btn_discover.config(state="disabled")
|
||||||
self._status_label.config(text="Connecting...", foreground="#666666")
|
self._status_label.config(text="Connecting...", foreground="#666666")
|
||||||
|
|
||||||
self._cancelled = False
|
self._cancelled = False
|
||||||
@@ -134,11 +252,11 @@ class ConnectDialog:
|
|||||||
msg = rest[0]
|
msg = rest[0]
|
||||||
self._status_label.config(text=f"Error: {msg}", foreground="#c00000")
|
self._status_label.config(text=f"Error: {msg}", foreground="#c00000")
|
||||||
self._btn_connect.config(state="normal")
|
self._btn_connect.config(state="normal")
|
||||||
|
self._btn_discover.config(state="normal")
|
||||||
|
|
||||||
def _do_cancel(self):
|
def _do_cancel(self):
|
||||||
self._cancelled = True
|
self._cancelled = True
|
||||||
if self._thread is not None and self._thread.is_alive():
|
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
|
import app_state as _as
|
||||||
def _cleanup():
|
def _cleanup():
|
||||||
self._thread.join(timeout=5)
|
self._thread.join(timeout=5)
|
||||||
|
|||||||
@@ -945,8 +945,55 @@ class DaspSocket:
|
|||||||
continue
|
continue
|
||||||
break
|
break
|
||||||
|
|
||||||
|
def discover(self, port=DEFAULT_PORT, wait_time=3):
|
||||||
|
"""Send DASP DISCOVER broadcast and collect responding devices.
|
||||||
|
|
||||||
|
Returns a list of (ip_address, platform_id) tuples.
|
||||||
|
"""
|
||||||
|
self._discovered = []
|
||||||
|
self._discover_active = True
|
||||||
|
|
||||||
|
msg = DaspMsg()
|
||||||
|
msg.msg_type = DISCOVER
|
||||||
|
msg.session_id = 0xFFFF
|
||||||
|
msg.seq_num = 0xFFFF
|
||||||
|
msg.include_message_len = False
|
||||||
|
data = msg.encode()
|
||||||
|
|
||||||
|
with self._send_lock:
|
||||||
|
try:
|
||||||
|
self._sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
self._sock.sendto(data, ("255.255.255.255", port))
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
# also try the multicast address used by the Sedona spec
|
||||||
|
try:
|
||||||
|
self._sock.sendto(data, ("239.255.18.76", port))
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
time.sleep(wait_time)
|
||||||
|
self._discover_active = False
|
||||||
|
|
||||||
|
seen = set()
|
||||||
|
result = []
|
||||||
|
for addr, pid in self._discovered:
|
||||||
|
if addr not in seen:
|
||||||
|
seen.add(addr)
|
||||||
|
result.append((addr, pid or ""))
|
||||||
|
return result
|
||||||
|
|
||||||
def _dispatch(self, msg, addr, raw=None):
|
def _dispatch(self, msg, addr, raw=None):
|
||||||
_trace("R", msg, raw)
|
_trace("R", msg, raw)
|
||||||
|
|
||||||
|
# Handle DISCOVER responses (from devices replying to our broadcast)
|
||||||
|
if msg.msg_type == DISCOVER and getattr(self, '_discover_active', False):
|
||||||
|
self._discovered.append((addr[0], msg.platform_id))
|
||||||
|
return
|
||||||
|
|
||||||
session_id = msg.session_id
|
session_id = msg.session_id
|
||||||
with self._sessions_lock:
|
with self._sessions_lock:
|
||||||
session = self._sessions.get(session_id)
|
session = self._sessions.get(session_id)
|
||||||
|
|||||||
Reference in New Issue
Block a user