""" Connection and discovery dialogs for live Sedona devices. ConnectDialog — manual host/port/user/password entry (Device > Connect). DiscoverDialog — DASP broadcast discovery with device list; selecting a device opens a credentials prompt, then connects (Device > Discover). """ import configparser import threading import tkinter as tk from tkinter import ttk import app_state # ──────────────────────────────────────────────────────────────────── # ConnectDialog (Device > Connect…) # ──────────────────────────────────────────────────────────────────── 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_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("", lambda e: self._do_connect()) self._dlg.bind("", 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 if not user: self._status_label.config(text="Username is required", foreground="#c00000") return if not password: self._status_label.config(text="Password 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) 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") 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() # ──────────────────────────────────────────────────────────────────── # DiscoverDialog (Device > Discover…) # ──────────────────────────────────────────────────────────────────── class DiscoverDialog: """Modal dialog that broadcasts DASP DISCOVER, lists responding devices, and lets the user pick one to connect to.""" def __init__(self, parent, on_connect_callback): self._parent = parent self._callback = on_connect_callback self._discover_thread = None self._dlg = tk.Toplevel(parent) self._dlg.title("Discover Devices") self._dlg.resizable(False, False) self._dlg.transient(parent) self._dlg.grab_set() frame = ttk.Frame(self._dlg, padding=16) frame.pack(fill=tk.BOTH, expand=True) # ── Header row: label + wait + discover button ───────────── row = 0 hdr = ttk.Frame(frame) hdr.grid(row=row, column=0, sticky="ew", pady=(0, 8)) ttk.Label(hdr, text="Discovered Devices:").pack(side=tk.LEFT) self._btn_discover = ttk.Button( hdr, text="Discover", command=self._do_discover, width=10) self._btn_discover.pack(side=tk.RIGHT) self._wait_var = tk.StringVar(value="10") ttk.Entry(hdr, textvariable=self._wait_var, width=4, justify="center").pack( side=tk.RIGHT, padx=(0, 4)) ttk.Label(hdr, text="Wait (s):").pack(side=tk.RIGHT, padx=(8, 2)) _, port, _ = _load_last_connection() self._port_var = tk.StringVar(value=port) ttk.Entry(hdr, textvariable=self._port_var, width=6, justify="center").pack( side=tk.RIGHT, padx=(0, 4)) ttk.Label(hdr, text="Port:").pack(side=tk.RIGHT, padx=(8, 2)) # ── Device list ──────────────────────────────────────────── row += 1 list_frame = ttk.Frame(frame) list_frame.grid(row=row, column=0, 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=6, selectmode="browse") self._dev_tree.heading("ip", text="IP Address") self._dev_tree.heading("platform", text="Platform ID") self._dev_tree.column("ip", width=150, minwidth=100) self._dev_tree.column("platform", width=220, minwidth=120) 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("", lambda e: self._do_connect_selected()) # ── Status + buttons ─────────────────────────────────────── row += 1 self._status_label = ttk.Label(frame, text="", foreground="#666666") self._status_label.grid(row=row, column=0, sticky="w", pady=(8, 4)) row += 1 btn_frame = ttk.Frame(frame) btn_frame.grid(row=row, column=0, sticky="e", pady=(8, 0)) ttk.Button(btn_frame, text="Cancel", command=self._dlg.destroy).pack(side=tk.RIGHT) self._btn_connect = ttk.Button( btn_frame, text="Connect", command=self._do_connect_selected) self._btn_connect.pack(side=tk.RIGHT, padx=(0, 8)) self._dlg.bind("", lambda e: self._dlg.destroy()) 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") # ── Connect selected device ──────────────────────────────────── def _do_connect_selected(self): sel = self._dev_tree.selection() if not sel: self._status_label.config(text="Select a device first", foreground="#c00000") return values = self._dev_tree.item(sel[0], "values") host = values[0] port = self._port_var.get().strip() self._dlg.destroy() _CredentialsDialog(self._parent, self._callback, host, port) # ──────────────────────────────────────────────────────────────────── # _CredentialsDialog (username / password prompt after discovery) # ──────────────────────────────────────────────────────────────────── class _CredentialsDialog: """Small modal that asks for username and password, then connects.""" def __init__(self, parent, on_connect_callback, host, port): self._callback = on_connect_callback self._host = host self._port = port self._thread = None self._cancelled = False _, _, last_user = _load_last_connection() self._dlg = tk.Toplevel(parent) self._dlg.title(f"Connect to {host}") self._dlg.resizable(False, False) self._dlg.transient(parent) self._dlg.grab_set() 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) ttk.Label(frame, text=f"{host}:{port}").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=last_user) user_entry = ttk.Entry(frame, textvariable=self._user_var, width=24) user_entry.grid(row=row, column=1, 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=24, show="*").grid( row=row, column=1, sticky="ew", pady=4) row += 1 self._status_label = ttk.Label(frame, text="", foreground="#666666") self._status_label.grid(row=row, column=0, columnspan=2, sticky="w", pady=(8, 4)) row += 1 btn_frame = ttk.Frame(frame) btn_frame.grid(row=row, column=0, columnspan=2, sticky="e", pady=(8, 0)) ttk.Button(btn_frame, text="Cancel", command=self._do_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)) user_entry.focus_set() self._dlg.bind("", lambda e: self._do_connect()) self._dlg.bind("", 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): user = self._user_var.get().strip() password = self._pass_var.get() if not user: self._status_label.config(text="Username is required", foreground="#c00000") return if not password: self._status_label.config(text="Password is required", foreground="#c00000") return try: port_int = int(self._port) except ValueError: self._status_label.config(text="Invalid port", 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=(self._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") 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() # ──────────────────────────────────────────────────────────────────── # Persistence helpers # ──────────────────────────────────────────────────────────────────── 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