DAQ/GUI: trying to make error handling in satatus pollign more robust. WIP

This commit is contained in:
2026-02-27 16:26:01 +01:00
parent 3fcd084b21
commit 2fa0b2a6e6
4 changed files with 137 additions and 27 deletions
+6
View File
@@ -719,6 +719,12 @@ class DAQStatusModel(BaseModel):
last_best_b_factor: float | None = None
crystal_size: CrystalSize = CrystalSize(x=0,y=0,z=0)
tell_connected: bool = True
tell_error: str | None = None
smargon_connected: bool = True
smargon_error: str | None = None
class BeamlineSettingsModel(BaseModel):
dtz_max: float | None = 1600.0
dtz_min: float | None = 120.0
+55 -1
View File
@@ -42,7 +42,7 @@ from aare.common.exception_handler import (
MountingFailed,
WarningTellException,
CriticalTellException,
AXCFailed,
AXCFailed, SmargonCommunicationError, TellCommunicationError,
)
logger = setup_logger("aareDAQ")
@@ -1418,8 +1418,62 @@ class AareDAQ:
dtz_max=1000,
)
def _safe_sample(self) -> tuple[SampleShortInfo | None, bool, str | None]:
"""
Return (sample, tell_connected, tell_error) without raising.
"""
try:
return self.sample, True, None
except TellCommunicationError as e:
return None, False, str(e)
except Exception as e:
# Keep status flowing even if Tell code throws something unexpected
return None, False, f"TELL unavailable: {e}"
def _safe_geom(self) -> tuple[SampleGeometryModel, bool, str | None]:
"""
Return (geom, smargon_connected, smargon_error) without raising.
Uses a conservative fallback geometry if Smargon access fails.
"""
try:
return self.sample_geometry, True, None
except SmargonCommunicationError as e:
zoom = self.__devs.zoom
fallback = SampleGeometryModel(
beam_location_pxl=self.__cfg.beam_mark_coeff.apply(zoom),
pixel_in_mm=self.__cfg.pixel_to_mm(zoom),
omega_deg=self.__devs.aerotech_omega,
smargon=SmargonCoordinate(
sh_mm=Coordinate(x=0.0, y=0.0, z=0.0),
phi_deg=0.0,
chi_deg=0.0,
),
beam_size_mm=self.__cfg.beam_size_mm,
aerotech=Coordinate(x=0.0, y=0.0, z=0.0),
aerotech_meas=Coordinate(x=0.0, y=0.0, z=0.0),
)
return fallback, False, str(e)
except Exception as e:
zoom = self.__devs.zoom
fallback = SampleGeometryModel(
beam_location_pxl=self.__cfg.beam_mark_coeff.apply(zoom),
pixel_in_mm=self.__cfg.pixel_to_mm(zoom),
omega_deg=self.__devs.aerotech_omega,
smargon=SmargonCoordinate(
sh_mm=Coordinate(x=0.0, y=0.0, z=0.0),
phi_deg=0.0,
chi_deg=0.0,
),
beam_size_mm=self.__cfg.beam_size_mm,
aerotech=Coordinate(x=0.0, y=0.0, z=0.0),
aerotech_meas=Coordinate(x=0.0, y=0.0, z=0.0),
)
return fallback, False, f"Smargon unavailable: {e}"
@property
def status(self) -> DAQStatusModel:
safe_sample, tell_ok, tell_err = self._safe_sample()
safe_geom, smargon_ok, smargon_err = self._safe_geom()
# session should be set by FastAPI server
return DAQStatusModel(
state=self.state,
+1 -9
View File
@@ -55,19 +55,11 @@ async def login(form_data: OAuth2PasswordRequestForm = Depends()):
return {"access_token": data, "token_type": "bearer"}
@app.get("/meta/error-codes")
async def meta_error_codes() -> dict[str, str]:
async def meta_error_codes() -> dict[str, dict[str, str]]:
"""
Public, stable registry of machine-readable error codes.
Useful for GUIs, tests, and diagnostics.
"""
return export_error_codes()
@app.get("/meta/error-codes/grouped")
async def meta_error_codes_grouped() -> dict[str, dict[str, str]]:
"""
Grouped registry of machine-readable error codes, by enum class.
Preferred for GUIs that want multiple code categories without collisions.
"""
return export_error_codes_grouped()
@app.get("/status")
+75 -17
View File
@@ -2,6 +2,7 @@ import copy
import random
import time
import json
from collections import deque
from PySide6.QtCore import Signal, QUrl, Slot, QTimer, QObject, QByteArray
from PySide6.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply
@@ -26,6 +27,7 @@ class DAQWorker(QObject):
spreadsheet = Signal(SampleShortInfoList)
reference_tools = Signal(SampleShortInfoList)
http_error = Signal(str)
status_message = Signal(str, bool)
auth_error = Signal()
sample_missing = Signal(str)
automated_scan_done = Signal(int, bool, str) # sample ID, success
@@ -40,6 +42,7 @@ class DAQWorker(QObject):
error_codes_loaded = Signal(dict)
last_error_payload_changed = Signal(dict)
last_error_payloads_changed = Signal(list)
def __init__(self, base_url: str | None, token: str, parent=None):
super().__init__(parent)
@@ -56,17 +59,51 @@ class DAQWorker(QObject):
self._auth_error_min_interval = 10.0
self._last_status_request_ts = 0.0
self._status_request_min_interval = 10.0
self._status_request_min_interval = 0.5
self._smargon_retry_interval_s = 2.0
self._smargon_log_min_interval_s = 10.0
self._last_status_error = None
self._smargon_error_active = False
self._last_smargon_log_ts = 0.0
self._last_smargon_log_key: str | None = None
self._last_error_payload: dict = {}
self._last_error_payloads = deque(maxlen=10)
self._last_tell_connected: bool | None = None
self._last_smargon_connected: bool | None = None
def get_last_error_payload(self) -> dict:
return dict(self._last_error_payload or {})
def get_last_error_payloads(self) -> list[dict]:
return [dict(p or {}) for p in list(self._last_error_payloads)]
def _set_last_error_payload(self, payload: dict) -> None:
self._last_error_payload = payload or {}
payload = payload or {}
self._last_error_payload = payload
self._last_error_payloads.append(payload)
self.last_error_payload_changed.emit(self.get_last_error_payload())
self.last_error_payloads_changed.emit(self.get_last_error_payloads())
def _log_smargon_throttled(self, *, endpoint: str | None, message: str) -> None:
"""
Log immediately if endpoint/message changed; otherwise at most every N seconds.
"""
key = f"{endpoint or ''}|{message or ''}"
now = time.monotonic()
if self._last_smargon_log_key != key:
self._last_smargon_log_key = key
self._last_smargon_log_ts = now
logger.error(f"Smargon connection error (endpoint={endpoint}): {message}")
return
if now - self._last_smargon_log_ts >= self._smargon_log_min_interval_s:
self._last_smargon_log_ts = now
logger.error(f"Smargon connection error (endpoint={endpoint}): {message}")
@Slot()
def regular_update(self):
@@ -80,6 +117,11 @@ class DAQWorker(QObject):
if self.__base_url is None:
return
now = time.monotonic()
if now - self._last_status_request_ts < self._status_request_min_interval:
return
self._last_status_request_ts = now
request = QNetworkRequest(QUrl(f"{self.__base_url}/status"))
request.setRawHeader(b"Authorization", f"Bearer {self.__token}".encode("utf-8"))
reply = self.__net_manager.get(request)
@@ -102,16 +144,31 @@ class DAQWorker(QObject):
response_data = self.handle_response(reply)
parsed_response = DAQStatusModel.model_validate_json(response_data)
self.update.emit(parsed_response)
tell_conn = bool(getattr(parsed_response, "tell_connected", True))
smargon_conn = bool(getattr(parsed_response, "smargon_connected", True))
if self._last_tell_connected is None:
self._last_tell_connected = tell_conn
elif self._last_tell_connected != tell_conn:
self._last_tell_connected = tell_conn
if tell_conn:
self.status_message.emit("TELL connection restored.", False)
else:
self.status_message.emit("TELL connection error, please inform your local contact.", True)
if self._last_smargon_connected is None:
self._last_smargon_connected = smargon_conn
elif self._last_smargon_connected != smargon_conn:
self._last_smargon_connected = smargon_conn
if smargon_conn:
self.status_message.emit("Smargon connection restored.", False)
else:
self.status_message.emit("Smargon connection error, please inform your local contact.", True)
except Exception as e:
now = time.monotonic()
if str(e) == str(self._last_status_error):
if now - self._last_auth_error_log_ts > self._auth_error_min_interval:
self._last_auth_error_log_ts = now
logger.error(f"Exception from status response: {e}")
else:
self._last_status_error = e
self._last_auth_error_log_ts = now
logger.error(f"Exception from status response: {e}")
# keep your existing throttled logging behavior here if you want
logger.error(f"Exception from status response: {e}")
self.http_error.emit(str(e))
@Slot(QNetworkReply)
@@ -719,24 +776,26 @@ class DAQWorker(QObject):
out[str(k)] = str(v)
return out
@Slot()
@Slot()
def get_error_codes(self) -> None:
"""
Fetch server error codes registry for developer/help UI.
Emits error_codes_loaded(dict).
Server default is grouped. We flatten grouped payloads for existing UI.
"""
if self.__base_url is None:
self.error_codes_loaded.emit(export_error_codes())
return
# Prefer grouped (newer servers), but we'll gracefully fall back if missing.
request = QNetworkRequest(QUrl(f"{self.__base_url}/meta/error-codes/grouped"))
request = QNetworkRequest(QUrl(f"{self.__base_url}/meta/error-codes"))
request.setRawHeader(b"Authorization", f"Bearer {self.__token}".encode("utf-8"))
reply = self.__net_manager.get(request)
reply.finished.connect(lambda: self._handle_error_codes_response(reply))
def _retry_error_codes_legacy(self) -> None:
request = QNetworkRequest(QUrl(f"{self.__base_url}/meta/error-codes"))
request = QNetworkRequest(QUrl(f"{self.__base_url}/meta/error-codes/flat"))
request.setRawHeader(b"Authorization", f"Bearer {self.__token}".encode("utf-8"))
reply = self.__net_manager.get(request)
reply.finished.connect(lambda: self._handle_error_codes_response(reply))
@@ -746,15 +805,14 @@ class DAQWorker(QObject):
try:
status = reply.attribute(QNetworkRequest.Attribute.HttpStatusCodeAttribute)
# If the grouped endpoint isn't available on this server, fall back to legacy.
try:
url = reply.request().url().toString()
except Exception:
url = ""
if int(status) == 404 and url.endswith("/meta/error-codes/grouped"):
if int(status) == 404 and url.endswith("/meta/error-codes"):
reply.deleteLater()
self._retry_error_codes_legacy()
logger.error(f"Error codes not found on server.")
return
payload = self.handle_response(reply)