Wire Stop button to cancel blocked firmware sweeps
Stop handler now calls cancel_operation() on the device bridge, which sets a threading.Event that interrupts the 2s-timeout serial read loop in send_with_timeout(). InterruptedError is caught separately to prevent falling back to software sweep on cancel. disconnect() uses acquire(timeout=5) with force-close fallback instead of blocking lock acquisition — prevents deadlock when a stuck worker holds the serial lock during shutdown. Add 3 Textual async tests (pytest-asyncio) to verify Stop behavior: firmware sweep stop, software sweep stop, and sweep restart.
This commit is contained in:
parent
972c26b22f
commit
e7e71c47d7
5 changed files with 204 additions and 2 deletions
|
|
@ -158,6 +158,18 @@ class SerialBridge:
|
|||
self._connected = True
|
||||
self._menu = self._detect_menu()
|
||||
|
||||
def cancel_operation(self) -> None:
|
||||
"""Signal any in-progress long-running operation to abort.
|
||||
|
||||
Safe to call from any thread. The cancel event is checked every
|
||||
~2 seconds by ``send_with_timeout``.
|
||||
"""
|
||||
self._cancel.set()
|
||||
|
||||
def clear_cancel(self) -> None:
|
||||
"""Reset the cancel event so future operations proceed normally."""
|
||||
self._cancel.clear()
|
||||
|
||||
def disconnect(self) -> None:
|
||||
"""Close the serial connection.
|
||||
|
||||
|
|
@ -166,13 +178,26 @@ class SerialBridge:
|
|||
the port cleanly.
|
||||
"""
|
||||
self._cancel.set()
|
||||
with self._lock:
|
||||
if not self._lock.acquire(timeout=5):
|
||||
# Lock held by dead/stuck worker — force-close the port
|
||||
# so the blocked serial read raises an exception.
|
||||
logger.warning("Lock acquisition timed out, force-closing port")
|
||||
with contextlib.suppress(Exception):
|
||||
self._proto.disconnect()
|
||||
self._connected = False
|
||||
self._menu = Menu.UNKNOWN
|
||||
self._cancel.clear()
|
||||
return
|
||||
|
||||
try:
|
||||
with contextlib.suppress(Exception):
|
||||
self._go_to_root()
|
||||
self._proto.disconnect()
|
||||
self._connected = False
|
||||
self._menu = Menu.UNKNOWN
|
||||
self._cancel.clear() # reset for potential reconnect
|
||||
self._cancel.clear()
|
||||
finally:
|
||||
self._lock.release()
|
||||
|
||||
@property
|
||||
def is_connected(self) -> bool:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue