Compare commits

..
2 Commits
Author SHA1 Message Date
ArdaandClaude Opus 5.5 9b1d029b14 Connect to another device without disconnecting first
Connect and Discover stay enabled while a device is connected. Pressing
Connect in the dialog drops the current device before the new one is
tried; cancelling the dialog leaves the connection alone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 23:49:25 +03:00
ArdaandClaude Opus 5.5 dbeeb78916 Disconnect from the device when opening a file or closing the app
Opening a file now drops the live connection once a file is chosen, without
asking. File > Exit and the window close box go through quit_app, which
closes the Sox session properly instead of leaving it for the device to
time out.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 23:45:12 +03:00
4 changed files with 73 additions and 23 deletions
+1 -1
View File
@@ -8,7 +8,7 @@ so later rebinding would only ever be seen by the module that did it.
import configparser
import os
VERSION = "0.0.0.139"
VERSION = "0.0.0.141"
PROPERTIES_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "editor.properties")
DEFAULT_GRID_SIZE = 10
DEFAULT_ADD_TIMESTAMP = True
+17 -4
View File
@@ -21,8 +21,9 @@ import app_state
class ConnectDialog:
"""Modal dialog for entering device connection parameters."""
def __init__(self, parent, on_connect_callback):
def __init__(self, parent, on_connect_callback, on_before_connect=None):
self._callback = on_connect_callback
self._before_connect = on_before_connect
self._thread = None
self._cancelled = False
@@ -103,6 +104,11 @@ class ConnectDialog:
self._status_label.config(text="Invalid port number", foreground="#c00000")
return
# Runs here, on the main thread, rather than in the worker: it is where a
# device already connected is let go, and that takes the UI down with it.
if self._before_connect is not None:
self._before_connect()
self._btn_connect.config(state="disabled")
self._status_label.config(text="Connecting...", foreground="#666666")
@@ -169,9 +175,10 @@ 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):
def __init__(self, parent, on_connect_callback, on_before_connect=None):
self._parent = parent
self._callback = on_connect_callback
self._before_connect = on_before_connect
self._discover_thread = None
self._dlg = tk.Toplevel(parent)
@@ -318,7 +325,7 @@ class DiscoverDialog:
host = values[0]
port = self._port_var.get().strip()
self._dlg.destroy()
_CredentialsDialog(self._parent, self._callback, host, port)
_CredentialsDialog(self._parent, self._callback, host, port, self._before_connect)
# ────────────────────────────────────────────────────────────────────
@@ -328,8 +335,9 @@ class DiscoverDialog:
class _CredentialsDialog:
"""Small modal that asks for username and password, then connects."""
def __init__(self, parent, on_connect_callback, host, port):
def __init__(self, parent, on_connect_callback, host, port, on_before_connect=None):
self._callback = on_connect_callback
self._before_connect = on_before_connect
self._host = host
self._port = port
self._thread = None
@@ -397,6 +405,11 @@ class _CredentialsDialog:
self._status_label.config(text="Invalid port", foreground="#c00000")
return
# Runs here, on the main thread, rather than in the worker: it is where a
# device already connected is let go, and that takes the UI down with it.
if self._before_connect is not None:
self._before_connect()
self._btn_connect.config(state="disabled")
self._status_label.config(text="Connecting...", foreground="#666666")
+41 -5
View File
@@ -2316,7 +2316,8 @@ def do_device_connect():
wiresheet.render_components([])
app_state.window.title(app_state.base_window_title())
connect_dialog.ConnectDialog(app_state.window, _connect_to_device)
connect_dialog.ConnectDialog(app_state.window, _connect_to_device,
on_before_connect=_drop_device_before_connect)
def do_device_discover():
@@ -2337,7 +2338,18 @@ def do_device_discover():
wiresheet.render_components([])
app_state.window.title(app_state.base_window_title())
connect_dialog.DiscoverDialog(app_state.window, _connect_to_device)
connect_dialog.DiscoverDialog(app_state.window, _connect_to_device,
on_before_connect=_drop_device_before_connect)
def _drop_device_before_connect():
"""One device at a time: the one already connected goes before the next is tried.
Called when Connect is pressed, not when the dialog opens, so backing out of the
dialog leaves the current connection alone.
"""
if app_state.device_session is not None:
do_device_disconnect()
def _connect_to_device(host, port, user, password):
@@ -2358,8 +2370,6 @@ def _connect_to_device(host, port, user, password):
def _on_connected(ds, root_comp):
"""Main-thread callback after successful connect."""
app_state.window.title(app_state.base_window_title_live())
menu_device.entryconfig("Connect...", state="disabled")
menu_device.entryconfig("Discover...", state="disabled")
menu_device.entryconfig("Disconnect", state="normal")
menu_device.entryconfig("Download to SAX...", state="normal")
app_state.set_status(f"Connected to {app_state.device_host}:{app_state.device_port}")
@@ -2435,6 +2445,30 @@ def do_device_disconnect():
wiresheet.render_components([])
def quit_app():
"""File > Exit and the window's close box: let go of the device before going.
Leaving without a proper close keeps the device's Sox session and subscriptions
alive until it times them out, so the next connect may be turned away.
"""
global _cov_poll_id
if _cov_poll_id is not None:
app_state.window.after_cancel(_cov_poll_id)
_cov_poll_id = None
ds = app_state.device_session
app_state.device_session = None
app_state.live_mode = False
if ds is not None:
try:
ds.disconnect()
except Exception:
pass
app_state.window.destroy()
def _start_cov_polling():
"""Starts the 100ms COV event polling loop on the main thread."""
global _cov_poll_id
@@ -2616,7 +2650,7 @@ menu_file.add_command(label="Save As...", command=sax_file.save_file_as)
menu_file.add_separator()
menu_file.add_command(label="Print...", command=wiresheet.print_sheet, accelerator=f"{accel_mod}+P")
menu_file.add_separator()
menu_file.add_command(label="Exit", command=window.quit)
menu_file.add_command(label="Exit", command=quit_app)
menu_bar.add_cascade(label="File", menu=menu_file)
menu_edit = tk.Menu(menu_bar, tearoff=0)
@@ -3000,6 +3034,8 @@ paned_window.add(frame_editor, weight=3)
app_state.dirty_display_callback = refresh_dirty_display
undo.set_refresh_callback(refresh_all_views)
sax_file.set_tree_filled_callback(on_tree_filled)
sax_file.set_disconnect_device_callback(do_device_disconnect)
window.protocol("WM_DELETE_WINDOW", quit_app)
refresh_palette_kits()
apply_left_pane_width()
+14 -13
View File
@@ -31,6 +31,15 @@ def set_tree_filled_callback(callback):
_tree_filled = callback
_disconnect_device = None
def set_disconnect_device_callback(callback):
"""Registers how to drop the live device session before a file takes its place."""
global _disconnect_device
_disconnect_device = callback
def parse_xml_to_tree(treeview, parent_node, xml_element):
"""Recursively traverses XML elements and adds <comp> tags to the Treeview."""
for child in xml_element:
@@ -132,19 +141,6 @@ def open_file():
messagebox.showwarning("Action Blocked", "Cannot load SAX profiles because 'Sedona home folder' configuration is missing or empty.")
return
if app_state.live_mode:
answer = messagebox.askyesnocancel(
"Device Connected",
"A live device connection is active.\n\n"
"Disconnect from the device and open a file?\n\n"
"Choose Yes to disconnect and open, No to cancel.",
)
if not answer:
return
# Import here to avoid circular import at module level
from main import do_device_disconnect
do_device_disconnect()
file_path = filedialog.askopenfilename(
title="Select SAX File",
filetypes=[("SAX Files", "*.sax"), ("XML Files", "*.xml"), ("All Files", "*.*")],
@@ -152,6 +148,11 @@ def open_file():
if not file_path:
return
# A session is a file or a device, never both. The device goes only once a file has
# actually been chosen, so cancelling the dialog leaves the connection as it was.
if app_state.device_session is not None and _disconnect_device is not None:
_disconnect_device()
canvas = app_state.canvas
tree = app_state.tree