From 5634554c00db00aaa9725a1bdf0ffa8003b2511b Mon Sep 17 00:00:00 2001 From: appleb_m Date: Tue, 24 Mar 2026 11:06:32 +0100 Subject: [PATCH] GUI/DAQ: fixed connection error handling for tell, daq, smargon. Added handling for aerotech. Fixed bug where connection messaged flashed when polling status, fixed bug where tell connected would quash other messages and appear as an error. --- src/aare/daq/daq.py | 13 ++++++++ src/aare/gui/main_window.py | 20 +++++++++++- src/aare/gui/threads/daq_worker.py | 46 +++++++++++++++++++++++----- src/aare/gui/widgets/alert_banner.py | 8 ++++- 4 files changed, 78 insertions(+), 9 deletions(-) diff --git a/src/aare/daq/daq.py b/src/aare/daq/daq.py index 6fc07354..67c47868 100644 --- a/src/aare/daq/daq.py +++ b/src/aare/daq/daq.py @@ -1622,6 +1622,16 @@ class AareDAQ: # Keep status flowing even if Tell code throws something unexpected return self.__cfg.current_sample, False, f"TELL unavailable: {e}" + def _aerotech_status(self) -> tuple[bool, str | None]: + aerotech_ok = True + aerotech_err: str | None = None + try: + _ = self.__devs.aerotech.status() + except Exception as e: + aerotech_ok = False + aerotech_err = str(e) + return aerotech_ok, aerotech_err + def _safe_geom(self) -> tuple[SampleGeometryModel, bool, str | None]: """ Return (geom, smargon_connected, smargon_error) without raising. @@ -1704,6 +1714,7 @@ class AareDAQ: def status(self) -> DAQStatusModel: safe_sample, tell_ok, tell_err = self._safe_sample() safe_geom, smargon_ok, smargon_err = self._safe_geom() + aerotech_ok, aerotech_err = self._aerotech_status() return DAQStatusModel( state=self.state, @@ -1721,6 +1732,8 @@ class AareDAQ: tell_error=tell_err, smargon_connected=smargon_ok, smargon_error=smargon_err, + aerotech_connected=aerotech_ok, + aerotech_error=aerotech_err, ) def cancel(self): diff --git a/src/aare/gui/main_window.py b/src/aare/gui/main_window.py index 4d1bc2a7..05069a1d 100644 --- a/src/aare/gui/main_window.py +++ b/src/aare/gui/main_window.py @@ -78,6 +78,8 @@ class MainWindow(QMainWindow): self._controls_help_dialog = None self._cleanup_done = False self._default_window_state = None + self._last_status_message: str | None = None + self._last_status_is_error: bool | None = None # Tutorial manager (define tutorials after widgets exist) self._tutorial_event_bus = TutorialEventBus(self) @@ -482,10 +484,26 @@ class MainWindow(QMainWindow): self.daq.fluorimeter_spectrum_update.connect(lambda: self.fluor_panel_dock.setVisible(True)) self.daq.status_message.connect(self.status_bar.show_connection_message) - self.daq.status_message.connect(self.alert_banner.show_message) + self.daq.status_message.connect(self._show_status_message_once) register_tutorials(self, self.tutorial_manager) + @Slot(str, bool) + def _show_status_message_once(self, message: str, is_error: bool = True) -> None: + message = (message or "").strip() + if not message: + self.alert_banner.clear_message() + self._last_status_message = None + self._last_status_is_error = None + return + + if message == self._last_status_message and is_error == self._last_status_is_error: + return + + self._last_status_message = message + self._last_status_is_error = is_error + self.alert_banner.show_message(message, is_error=is_error) + @Slot(QPixmap) def _on_samcam_prediction_pixmap(self, pix: QPixmap) -> None: self._last_pred_image_ts = time.monotonic() diff --git a/src/aare/gui/threads/daq_worker.py b/src/aare/gui/threads/daq_worker.py index 7bf240cb..36fb7464 100644 --- a/src/aare/gui/threads/daq_worker.py +++ b/src/aare/gui/threads/daq_worker.py @@ -80,6 +80,9 @@ class DAQWorker(QObject): self._last_tell_connected: bool | None = None self._last_tell_error: str | None = None self._last_smargon_connected: bool | None = None + self._last_smargon_error: str | None = None + self._last_aerotech_connected: bool | None = None + self._last_aerotech_error: str | None = None self._server_connected: bool | None = None self._last_server_error: str | None = None @@ -196,8 +199,10 @@ class DAQWorker(QObject): *, tell_conn: bool, smargon_conn: bool, + aerotech_conn: bool, tell_changed: bool, smargon_changed: bool, + aerotech_changed: bool, ) -> tuple[str | None, str | None, bool]: disconnected: list[str] = [] restored: list[str] = [] @@ -212,11 +217,22 @@ class DAQWorker(QObject): elif smargon_changed: restored.append("Smargon") + if not aerotech_conn: + disconnected.append("Aerotech") + elif aerotech_changed: + restored.append("Aerotech") + if disconnected: + if len(disconnected) == 3: + return ( + "all-devices-down", + "TELL, Smargon, and Aerotech connection errors, please inform your local contact.", + True, + ) if len(disconnected) == 2: return ( - "tell+smargon-down", - "TELL and Smargon connection errors, please inform your local contact.", + "+".join(sorted(d.lower() for d in disconnected)) + "-down", + f"{disconnected[0]} and {disconnected[1]} connection errors, please inform your local contact.", True, ) device = disconnected[0] @@ -227,10 +243,11 @@ class DAQWorker(QObject): ) if restored: + if len(restored) == 3: + return (None, "TELL, Smargon, and Aerotech connections restored.", False) if len(restored) == 2: - return (None, "TELL and Smargon connections restored.", False,) - device = restored[0] - return (None, f"{device} connection restored.",False) + return (None, f"{restored[0]} and {restored[1]} connections restored.", False) + return (None, f"{restored[0]} connection restored.", False) return (None, None, False) @@ -258,19 +275,31 @@ class DAQWorker(QObject): or self._last_tell_connected != tell_conn or self._last_tell_error != tell_err_text ) - smargon_changed = ( - self._last_smargon_connected is not None and self._last_smargon_connected != smargon_conn + self._last_smargon_connected is None + or self._last_smargon_connected != smargon_conn + or self._last_smargon_error != smargon_err_text + ) + aerotech_changed = ( + self._last_aerotech_connected is None + or self._last_aerotech_connected != aerotech_conn + or self._last_aerotech_error != aerotech_err_text ) self._last_tell_connected = tell_conn self._last_smargon_connected = smargon_conn + self._last_aerotech_connected = aerotech_conn + self._last_tell_error = tell_err_text + self._last_smargon_error = smargon_err_text + self._last_aerotech_error = aerotech_err_text status_key, status_msg, is_error = self._compose_device_status_message( tell_conn=tell_conn, smargon_conn=smargon_conn, + aerotech_conn=aerotech_conn, tell_changed=tell_changed, smargon_changed=smargon_changed, + aerotech_changed=aerotech_changed, ) self._emit_status_if_changed(status_key, status_msg, is_error) @@ -278,6 +307,8 @@ class DAQWorker(QObject): self._log_device_error_throttled(device="tell", message=tell_err) if not smargon_conn: self._log_device_error_throttled(device="smargon", message=smargon_err) + if not aerotech_conn: + self._log_device_error_throttled(device="aerotech", message=aerotech_err) except Exception as e: err_msg = str(e) @@ -293,6 +324,7 @@ class DAQWorker(QObject): self._last_server_error = err_msg self._last_tell_connected = None self._last_smargon_connected = None + self._last_aerotech_connected = None logger.error(f"Exception from status response: {e}") diff --git a/src/aare/gui/widgets/alert_banner.py b/src/aare/gui/widgets/alert_banner.py index 5a440057..86e5c791 100644 --- a/src/aare/gui/widgets/alert_banner.py +++ b/src/aare/gui/widgets/alert_banner.py @@ -12,6 +12,7 @@ class AlertBanner(QFrame): super().__init__(parent) self._current_message: str | None = None + self._current_is_error: bool | None = None self._clear_timer = QTimer(self) self._clear_timer.setSingleShot(True) @@ -42,7 +43,7 @@ class AlertBanner(QFrame): self.clear_message() return - if msg == self._current_message: + if self._current_message == msg and self._current_is_error == is_error: return if is_error: @@ -62,6 +63,8 @@ class AlertBanner(QFrame): "}" ) else: + if msg == self._current_message: + return decorated = f"✅ {msg} ✅" self.setStyleSheet( "QFrame {" @@ -79,12 +82,15 @@ class AlertBanner(QFrame): ) self._clear_timer.start(5000) + self._current_message = msg + self._current_is_error = is_error self._label.setText(decorated) self.setVisible(True) @Slot() def clear_message(self): self._current_message = None + self._current_is_error = None self._clear_timer.stop() self._label.clear() self.setVisible(False)