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)