From 2ee2f472758f6849ed392faf0da2abb45f8cf84d Mon Sep 17 00:00:00 2001 From: Ryan Malloy Date: Sat, 14 Feb 2026 16:50:08 -0700 Subject: [PATCH] Fix sweep/scan Stop button and state cleanup _sweeping/_scanning flags were never reset when workers finished, leaving the UI stuck in "Stopping..." forever. Both _do_sweep and _do_scan now use try/finally to always clear state and reset button styles. Firmware sweep checks the flag after the blocking serial call returns and discards results if Stop was pressed mid-execution. --- tui/src/birdcage_tui/screens/signal.py | 71 ++++++++++++++++++++------ 1 file changed, 54 insertions(+), 17 deletions(-) diff --git a/tui/src/birdcage_tui/screens/signal.py b/tui/src/birdcage_tui/screens/signal.py index 44e859e..1a5ab17 100644 --- a/tui/src/birdcage_tui/screens/signal.py +++ b/tui/src/birdcage_tui/screens/signal.py @@ -434,23 +434,28 @@ class SignalScreen(Container): if device is None: return - # Check if user forced software mode. - force_software = self.query_one("#sweep-software-mode", Checkbox).value + try: + # Check if user forced software mode. + force_software = self.query_one("#sweep-software-mode", Checkbox).value - if not force_software and hasattr(device, "az_sweep_firmware"): - try: - self._do_sweep_firmware(device) - return - except Exception: - log.warning( - "Firmware sweep failed, falling back to software", - exc_info=True, - ) - self.app.call_from_thread( - self._set_sweep_status, "Firmware sweep failed -- falling back..." - ) + if not force_software and hasattr(device, "az_sweep_firmware"): + try: + self._do_sweep_firmware(device) + return + except Exception: + log.warning( + "Firmware sweep failed, falling back to software", + exc_info=True, + ) + self.app.call_from_thread( + self._set_sweep_status, + "Firmware sweep failed -- falling back...", + ) - self._do_sweep_software(device) + self._do_sweep_software(device) + finally: + self._sweeping = False + self.app.call_from_thread(self._reset_sweep_buttons) def _do_sweep_firmware(self, device: DeviceLike) -> None: """Firmware-accelerated sweep via azscanwxp (runs in worker thread).""" @@ -494,6 +499,11 @@ class SignalScreen(Container): num_xponders=iterations, ) + # Check if Stop was pressed while firmware was executing. + if not self._sweeping or shutdown.is_set(): + self.app.call_from_thread(self._set_sweep_status, "Sweep stopped") + return + if not results: self.app.call_from_thread( self._set_sweep_status, "Firmware sweep returned no data" @@ -600,6 +610,11 @@ class SignalScreen(Container): self.query_one("#sweep-progress", ProgressBar).update(progress=pct) self.query_one("#sweep-status-text", Static).update(status_text) + def _reset_sweep_buttons(self) -> None: + """Restore sweep button styles to idle state (main thread).""" + self.query_one("#btn-start-sweep", Button).variant = "primary" + self.query_one("#btn-stop-sweep", Button).variant = "default" + # -- Sweep button handlers -- def _handle_sweep_start(self) -> None: @@ -616,12 +631,16 @@ class SignalScreen(Container): self.query_one("#sweep-progress", ProgressBar).update(progress=0) self._set_sweep_status("Starting sweep...") + self.query_one("#btn-start-sweep", Button).variant = "default" + self.query_one("#btn-stop-sweep", Button).variant = "warning" + self._sweeping = True self._do_sweep() def _handle_sweep_stop(self) -> None: self._sweeping = False self._set_sweep_status("Stopping...") + self._reset_sweep_buttons() def _export_sweep_csv(self) -> None: if not self._sweep_data: @@ -647,12 +666,21 @@ class SignalScreen(Container): @work(thread=True) def _do_scan(self) -> None: """Execute the AZ/EL grid scan in a background thread.""" - worker = get_current_worker() - shutdown = self.app.shutdown_event device = self._device if device is None: return + try: + self._do_scan_inner(device) + finally: + self._scanning = False + self.app.call_from_thread(self._reset_scan_buttons) + + def _do_scan_inner(self, device: DeviceLike) -> None: + """Inner scan logic (called from _do_scan worker thread).""" + worker = get_current_worker() + shutdown = self.app.shutdown_event + az_start = self._read_float("scan-az-start", 160.0) az_end = self._read_float("scan-az-end", 220.0) az_step = self._read_float("scan-az-step", 1.5) @@ -815,6 +843,11 @@ class SignalScreen(Container): self.query_one("#scan-progress", ProgressBar).update(progress=pct) self.query_one("#scan-status-text", Static).update(status_text) + def _reset_scan_buttons(self) -> None: + """Restore scan button styles to idle state (main thread).""" + self.query_one("#btn-start-scan", Button).variant = "primary" + self.query_one("#btn-stop-scan", Button).variant = "default" + # -- Scan button handlers -- def _handle_scan_start(self) -> None: @@ -831,12 +864,16 @@ class SignalScreen(Container): self.query_one("#scan-progress", ProgressBar).update(progress=0) self._set_scan_status("Starting scan...") + self.query_one("#btn-start-scan", Button).variant = "default" + self.query_one("#btn-stop-scan", Button).variant = "warning" + self._scanning = True self._do_scan() def _handle_scan_stop(self) -> None: self._scanning = False self._set_scan_status("Stopping...") + self._reset_scan_buttons() def _export_scan_csv(self) -> None: if not self._scan_data: