GUI: non-blocking disconnect, never close VISA sessions under the worker
Pressing Disconnect with the instruments powered off froze the GUI (apparent crash, 2026-07-06 15:52): worker.join(5) expired while the worker sat in multi-second VISA timeouts, then bench.close() ran on the GUI thread -- each driver close() first writes local() to the dead device (stacked timeouts) and closing a session under an in-flight read can also take NI-VISA down natively. _disconnect now detaches the UI immediately and runs all instrument teardown on a background thread; if the worker is still stuck in I/O after a 15s join the sessions are deliberately left open (OS reclaims them at exit) instead of being closed under a live read. Reconnecting while a teardown is still releasing the instruments is refused with a console hint. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+40
-9
@@ -156,6 +156,7 @@ class TestbenchGUI(tk.Tk):
|
|||||||
|
|
||||||
self.bench: MPPTTestbench | None = None
|
self.bench: MPPTTestbench | None = None
|
||||||
self.worker: InstrumentWorker | None = None
|
self.worker: InstrumentWorker | None = None
|
||||||
|
self._teardown_thread: threading.Thread | None = None
|
||||||
self._log_file = None
|
self._log_file = None
|
||||||
self._log_writer = None
|
self._log_writer = None
|
||||||
self._log_count = 0
|
self._log_count = 0
|
||||||
@@ -1141,6 +1142,10 @@ class TestbenchGUI(tk.Tk):
|
|||||||
# ── Connection ────────────────────────────────────────────────────
|
# ── Connection ────────────────────────────────────────────────────
|
||||||
|
|
||||||
def _connect(self) -> None:
|
def _connect(self) -> None:
|
||||||
|
if self._teardown_thread and self._teardown_thread.is_alive():
|
||||||
|
self._console("Previous disconnect is still releasing the "
|
||||||
|
"instruments - try again in a few seconds", "warn")
|
||||||
|
return
|
||||||
try:
|
try:
|
||||||
supply_addr = self._supply_addr.get().strip()
|
supply_addr = self._supply_addr.get().strip()
|
||||||
if supply_addr == "auto":
|
if supply_addr == "auto":
|
||||||
@@ -1193,16 +1198,9 @@ class TestbenchGUI(tk.Tk):
|
|||||||
|
|
||||||
def _disconnect(self) -> None:
|
def _disconnect(self) -> None:
|
||||||
self._stop_log()
|
self._stop_log()
|
||||||
if self.worker:
|
worker, bench = self.worker, self.bench
|
||||||
self.worker.stop()
|
|
||||||
self.worker.join(timeout=5)
|
|
||||||
self.worker = None
|
self.worker = None
|
||||||
if self.bench:
|
self.bench = None # _poll chain sees both None and ends
|
||||||
try:
|
|
||||||
self.bench.close()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
self.bench = None
|
|
||||||
|
|
||||||
self._btn_connect.config(state=tk.NORMAL)
|
self._btn_connect.config(state=tk.NORMAL)
|
||||||
self._btn_setup.config(state=tk.DISABLED)
|
self._btn_setup.config(state=tk.DISABLED)
|
||||||
@@ -1212,7 +1210,40 @@ class TestbenchGUI(tk.Tk):
|
|||||||
self._load_baud.config(state=tk.NORMAL)
|
self._load_baud.config(state=tk.NORMAL)
|
||||||
self._meter_addr.config(state=tk.NORMAL)
|
self._meter_addr.config(state=tk.NORMAL)
|
||||||
self._status_label.config(text="Disconnected")
|
self._status_label.config(text="Disconnected")
|
||||||
|
|
||||||
|
if not worker and not bench:
|
||||||
self._console("Disconnected")
|
self._console("Disconnected")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Instrument teardown runs OFF the GUI thread: with dead/absent
|
||||||
|
# devices every VISA/serial call blocks in multi-second timeouts,
|
||||||
|
# which froze the GUI ("not responding" -> apparent crash) when
|
||||||
|
# this was inline (2026-07-06 15:52). Closing a VISA session
|
||||||
|
# while the worker is still inside a read can also crash NI-VISA
|
||||||
|
# natively, so if the worker won't exit the sessions are left
|
||||||
|
# open -- the OS reclaims them at process exit.
|
||||||
|
def _teardown():
|
||||||
|
def say(msg, tag=None):
|
||||||
|
try:
|
||||||
|
self._console(msg, tag) if tag else self._console(msg)
|
||||||
|
except Exception:
|
||||||
|
pass # window already destroyed
|
||||||
|
if worker:
|
||||||
|
worker.stop()
|
||||||
|
worker.join(timeout=15)
|
||||||
|
if bench:
|
||||||
|
if worker and worker.is_alive():
|
||||||
|
say("Instrument worker stuck in an I/O timeout - "
|
||||||
|
"connections left open (freed on exit)", "warn")
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
bench.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
say("Disconnected")
|
||||||
|
|
||||||
|
self._teardown_thread = threading.Thread(target=_teardown, daemon=True)
|
||||||
|
self._teardown_thread.start()
|
||||||
|
|
||||||
def _setup_all(self) -> None:
|
def _setup_all(self) -> None:
|
||||||
self._send(Cmd.SETUP_ALL)
|
self._send(Cmd.SETUP_ALL)
|
||||||
|
|||||||
Reference in New Issue
Block a user