Fixes to alert banner

This commit is contained in:
appleb_m committed 2026-03-25 13:23:20 +01:00
1 parent 624a8365a9
commit 3d4e252438
3 files changed
+105 -49

No files matched your search

+13 -20
View File
@@ -70,6 +70,7 @@ class MainWindow(QMainWindow):
gonio_cam_id: int | None
):
super().__init__()
self.__base_url = base_url
self.__token = token
self.__mounting = False
@@ -78,8 +79,6 @@ 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)
@@ -115,9 +114,12 @@ class MainWindow(QMainWindow):
root_layout.setContentsMargins(0, 0, 0, 0)
root_layout.setSpacing(0)
self.alert_banner = AlertBanner(parent=root_widget)
self.alert_banner = AlertBanner(parent=root_widget, error_timeout_ms=30000, recover_timeout_ms=5000)
root_layout.addWidget(self.alert_banner)
self.alert_banner_secondary = AlertBanner(parent=root_widget, error_timeout_ms=10000, recover_timeout_ms=5000)
root_layout.addWidget(self.alert_banner_secondary)
top_widget = QWidget(parent=root_widget)
top_widget_layout = QHBoxLayout(top_widget)
top_widget.setLayout(top_widget_layout)
@@ -483,27 +485,18 @@ class MainWindow(QMainWindow):
self.daq.fluorimeter_spectrum_update.connect(self.fluor_panel.update_plot)
self.daq.fluorimeter_spectrum_update.connect(lambda: self.fluor_panel_dock.setVisible(True))
# === Alert/Status Message Routing ===
# Primary alert banner: Infrastructure devices (Server/Tell/Smargon/Aerotech)
self.daq.polled_devices_status.connect(self.alert_banner.show_message)
# Secondary alert banner: Detector errors (JFJoch)
self.daq.detector_error.connect(self.alert_banner_secondary.show_message)
# Status bar: General status messages (not device connection status)
self.daq.status_message.connect(self.status_bar.show_connection_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()
+87 -25
View File
@@ -10,9 +10,10 @@ from jfjoch_client import ScanResult, ScanResultImagesInner
from aare.common.coordinate import SmargonCoordinate, Coordinate, AerotechCoordinate
from aare.common.error_codes import export_error_codes
from aare.common.exception_handler import JFJochCommunicationError
from aare.common.models import DAQStatusModel, SampleShortInfoList, SampleShortInfo, SampleCameraSettings, \
AutofocusSettings, SimpleScanParameters, FluorescenceSpectrumParameterModel, FluorescenceSpectrumOutputModel
from aare.common.raster_grid import RasterGridRequest, CompletedRasterGrid
from aare.common.raster_grid import RasterGridRequest, CompletedRasterGrid, CompletedRasterGridElem
from aare.common.rotation_scan import RotationScanRequest, CompletedRotationScan
from aare.common.logger_config import setup_logger
@@ -28,8 +29,13 @@ class DAQWorker(QObject):
reference_tools = Signal(SampleShortInfoList)
http_error = Signal(str)
status_message = Signal(str, bool)
# New dedicated signals for polled device errors and request-time errors
polled_devices_status = Signal(str, bool) # (message, is_error)
detector_error = Signal(str, bool) # (message, is_error)
auth_error = Signal()
sample_missing = Signal(str)
automated_scan_done = Signal(int, bool, str) # sample ID, success
run_number_incremented = Signal()
raster_scan_completed = Signal(CompletedRasterGrid)
@@ -86,6 +92,17 @@ class DAQWorker(QObject):
self._server_connected: bool | None = None
self._last_server_error: str | None = None
self._has_seen_disconnection: bool = False
self._server_was_disconnected: bool = False
# Deduplication tracking for polled device messages
self._last_polled_msg: str | None = None
self._last_polled_is_error: bool | None = None
# Deduplication tracking for detector/request-time messages
self._last_detector_msg: str | None = None
self._last_detector_is_error: bool | None = None
self._face_detection_stream_reply: QNetworkReply | None = None
if self.__base_url is not None:
@@ -105,6 +122,46 @@ class DAQWorker(QObject):
self.last_error_payload_changed.emit(self.get_last_error_payload())
self.last_error_payloads_changed.emit(self.get_last_error_payloads())
def _emit_polled_device_message(self, msg: str, is_error: bool) -> None:
"""Emit polled device status message with deduplication."""
if (msg, is_error) == (self._last_polled_msg, self._last_polled_is_error):
return
self._last_polled_msg = msg
self._last_polled_is_error = is_error
self.polled_devices_status.emit(msg, is_error)
def _emit_detector_message(self, msg: str, is_error: bool) -> None:
"""Emit detector/request-time error message with deduplication."""
if (msg, is_error) == (self._last_detector_msg, self._last_detector_is_error):
return
self._last_detector_msg = msg
self._last_detector_is_error = is_error
self.detector_error.emit(msg, is_error)
def _emit_status_if_changed(self, key: str | None, message: str | None, is_error: bool) -> None:
"""
Emit polled device status to the primary alert banner.
Used for Server/Tell/Smargon/Aerotech connection status.
"""
if not message:
self._active_status_error_key = None
self._emit_polled_device_message("", False)
return
if is_error:
self._has_seen_disconnection = True
if self._active_status_error_key != key:
self._active_status_error_key = key
self._emit_polled_device_message(message, True)
return
if not self._has_seen_disconnection:
self._active_status_error_key = None
return
self._active_status_error_key = None
self._emit_polled_device_message(message, False)
def _log_smargon_throttled(self, *, endpoint: str | None, message: str) -> None:
"""
Log immediately if endpoint/message changed; otherwise at most every N seconds.
@@ -180,20 +237,6 @@ class DAQWorker(QObject):
logger.error(f"Error in response: {reply.errorString()}")
raise RuntimeError(reply.errorString())
def _emit_status_if_changed(self, key: str | None, message: str | None, is_error: bool) -> None:
if not message:
self._active_status_error_key = None
return
if is_error:
if self._active_status_error_key != key:
self._active_status_error_key = key
self.status_message.emit(message, True)
return
self._active_status_error_key = None
self.status_message.emit(message, False)
def _compose_device_status_message(
self,
*,
@@ -258,8 +301,15 @@ class DAQWorker(QObject):
parsed_response = DAQStatusModel.model_validate_json(response_data)
self.update.emit(parsed_response)
if self._server_connected is False:
# Handle server reconnection - show "Server reconnected" not device messages
if self._server_connected is False and self._has_seen_disconnection:
self._emit_status_if_changed(None, "Server reconnected.", False)
# Reset device states so we don't also emit device reconnection messages
self._last_tell_connected = None
self._last_smargon_connected = None
self._last_aerotech_connected = None
self._server_was_disconnected = False
self._server_connected = True
self._last_server_error = None
@@ -274,20 +324,29 @@ class DAQWorker(QObject):
smargon_err_text = None if smargon_err is None else str(smargon_err).strip()
aerotech_err_text = None if aerotech_err is None else str(aerotech_err).strip()
# Skip device status processing if we just reconnected from server down
# (we already showed "Server reconnected")
if self._last_tell_connected is None and self._last_smargon_connected is None and self._last_aerotech_connected is None:
# First status after startup or server reconnect - just record states, don't emit
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
return
tell_changed = (
self._last_tell_connected is None
or self._last_tell_connected != tell_conn
or self._last_tell_error != tell_err_text
self._last_tell_connected != tell_conn
or self._last_tell_error != tell_err_text
)
smargon_changed = (
self._last_smargon_connected is None
or self._last_smargon_connected != smargon_conn
or self._last_smargon_error != smargon_err_text
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_aerotech_connected != aerotech_conn
or self._last_aerotech_error != aerotech_err_text
)
self._last_tell_connected = tell_conn
@@ -318,6 +377,8 @@ class DAQWorker(QObject):
err_msg = str(e)
if self._server_connected is not False:
self._has_seen_disconnection = True
self._server_was_disconnected = True
self._emit_status_if_changed(
"server-down",
"Server disconnected. Reconnecting...",
@@ -326,6 +387,7 @@ class DAQWorker(QObject):
self._server_connected = False
self._last_server_error = err_msg
# Clear device states so we show "Server reconnected" on recovery
self._last_tell_connected = None
self._last_smargon_connected = None
self._last_aerotech_connected = None
+5 -4
View File
@@ -8,11 +8,13 @@ logger = setup_logger("aareGUI")
class AlertBanner(QFrame):
def __init__(self, parent=None):
def __init__(self, parent=None, error_timeout_ms: int = 15000, recover_timeout_ms: int = 5000):
super().__init__(parent)
self._current_message: str | None = None
self._current_is_error: bool | None = None
self._error_timeout_ms = error_timeout_ms
self._recovery_timeout_ms = recover_timeout_ms
self._clear_timer = QTimer(self)
self._clear_timer.setSingleShot(True)
@@ -62,9 +64,8 @@ class AlertBanner(QFrame):
" padding: 2px 6px 2px 6px;"
"}"
)
self._clear_timer.start(self._error_timeout_ms)
else:
if msg == self._current_message:
return
decorated = f"✅ {msg} ✅"
self.setStyleSheet(
"QFrame {"
@@ -80,7 +81,7 @@ class AlertBanner(QFrame):
" padding: 2px 6px 2px 6px;"
"}"
)
self._clear_timer.start(5000)
self._clear_timer.start(self._recovery_timeout_ms)
self._current_message = msg
self._current_is_error = is_error