GUI/DAQ: added a sample resync to recovery, trying to limit amount we poll Tell for current sample psoiton - work in progress.
This commit is contained in:
1 parent
cd04619814
commit
50b8dbf054
4 files changed
+228
-26
No files matched your search
+80
-8
@@ -64,6 +64,8 @@ class AareDAQ:
|
||||
self.__saved_box = None
|
||||
self._smargon_trace_path = Path("logs") / "smargon_trace.csv"
|
||||
self._face_detection_progress_cb: Callable[[dict], None] | None = None
|
||||
self._last_sample_sync_ts = 0.0
|
||||
self._sample_sync_min_interval_s = 2.0
|
||||
|
||||
def set_face_detection_progress_callback(self, cb: Callable[[dict], None] | None) -> None:
|
||||
self._face_detection_progress_cb = cb
|
||||
@@ -109,6 +111,80 @@ class AareDAQ:
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to append smargon trace: {e}")
|
||||
|
||||
def _sample_matches_mounted_address(
|
||||
self,
|
||||
sample: SampleShortInfo | None,
|
||||
mounted_address,
|
||||
) -> bool:
|
||||
if sample is None or sample.location is None or mounted_address is None:
|
||||
return False
|
||||
return (
|
||||
sample.location.segment == mounted_address.puck.segment
|
||||
and sample.location.pos == mounted_address.puck.pos
|
||||
and sample.pin == mounted_address.pin
|
||||
)
|
||||
|
||||
def _find_sample_by_mounted_address(self, mounted_address) -> SampleShortInfo | None:
|
||||
for sample in self.__cfg.spreadsheet.s:
|
||||
if self._sample_matches_mounted_address(sample, mounted_address):
|
||||
return sample
|
||||
|
||||
for sample in self.__cfg.reference_tools.s:
|
||||
if self._sample_matches_mounted_address(sample, mounted_address):
|
||||
return sample
|
||||
|
||||
return None
|
||||
|
||||
def _placeholder_sample_from_mounted_address(self, mounted_address) -> SampleShortInfo:
|
||||
return SampleShortInfo(
|
||||
db_id=-1,
|
||||
puck_name="",
|
||||
dewar_name="",
|
||||
sample_name=f"Mounted sample {mounted_address.puck.segment}{mounted_address.puck.pos}-{mounted_address.pin}",
|
||||
run_number=0,
|
||||
user="",
|
||||
pin=mounted_address.pin,
|
||||
location=mounted_address.puck,
|
||||
)
|
||||
|
||||
def sync_current_sample_from_tell(self, force: bool = False) -> SampleShortInfo | None:
|
||||
current_sample = self.__cfg.current_sample
|
||||
|
||||
if current_sample is not None and current_sample.location is None:
|
||||
return current_sample
|
||||
|
||||
now = time.monotonic()
|
||||
if not force and (now - self._last_sample_sync_ts) < self._sample_sync_min_interval_s:
|
||||
return current_sample
|
||||
|
||||
self._last_sample_sync_ts = now
|
||||
mounted_address = self.__devs.tell.get_mounted_sample()
|
||||
|
||||
if mounted_address is None:
|
||||
if current_sample is not None and current_sample.location is not None:
|
||||
logger.warning("TELL reports no mounted sample; clearing cached current_sample")
|
||||
self.__cfg.current_sample = None
|
||||
return self.__cfg.current_sample
|
||||
|
||||
if self._sample_matches_mounted_address(current_sample, mounted_address):
|
||||
return current_sample
|
||||
|
||||
resolved_sample = self._find_sample_by_mounted_address(mounted_address)
|
||||
if resolved_sample is None:
|
||||
resolved_sample = self._placeholder_sample_from_mounted_address(mounted_address)
|
||||
logger.warning(
|
||||
"Mounted sample from TELL was not found in known sample lists; using placeholder",
|
||||
extra={"mounted_address": str(mounted_address)},
|
||||
)
|
||||
else:
|
||||
logger.info(
|
||||
f"Reconciled cached sample from TELL to {resolved_sample.sample_name}",
|
||||
extra={"db_id": resolved_sample.db_id},
|
||||
)
|
||||
|
||||
self.__cfg.current_sample = resolved_sample
|
||||
return resolved_sample
|
||||
|
||||
@property
|
||||
def state(self) -> BeamlineStateEnum:
|
||||
return self.__cfg.state
|
||||
@@ -272,11 +348,6 @@ class AareDAQ:
|
||||
|
||||
@property
|
||||
def sample(self) -> SampleShortInfo | None:
|
||||
#TODO figure out why this works and what it should be
|
||||
if self.__cfg.current_sample is not None and self.__cfg.current_sample.location is None:
|
||||
return self.__cfg.current_sample
|
||||
if self.__devs.tell.get_mounted_sample() is None:
|
||||
return None
|
||||
return self.__cfg.current_sample
|
||||
|
||||
@property
|
||||
@@ -347,6 +418,7 @@ class AareDAQ:
|
||||
self.__devs.tell.set_in_mount_position(True)
|
||||
if target is None:
|
||||
self.__devs.tell.unmount(wait=True, timeout=60.0)
|
||||
self.__cfg.current_sample = None
|
||||
else:
|
||||
value = self.__devs.tell.mount(address=target.tell_address(), force=True, auto_unmount=True, read_dm=False,
|
||||
wait=True, timeout=360.0)
|
||||
@@ -1553,12 +1625,12 @@ class AareDAQ:
|
||||
Return (sample, tell_connected, tell_error) without raising.
|
||||
"""
|
||||
try:
|
||||
return self.sample, True, None
|
||||
return self.sync_current_sample_from_tell(), True, None
|
||||
except TellCommunicationError as e:
|
||||
return None, False, str(e)
|
||||
return self.__cfg.current_sample, False, str(e)
|
||||
except Exception as e:
|
||||
# Keep status flowing even if Tell code throws something unexpected
|
||||
return None, False, f"TELL unavailable: {e}"
|
||||
return self.__cfg.current_sample, False, f"TELL unavailable: {e}"
|
||||
|
||||
def _safe_geom(self) -> tuple[SampleGeometryModel, bool, str | None]:
|
||||
"""
|
||||
|
||||
+20
-6
@@ -34,7 +34,7 @@ from aare.common.exception_handler import (
|
||||
SampleException,
|
||||
UserRightsException,
|
||||
)
|
||||
|
||||
logger = setup_logger("aareDAQ")
|
||||
app = FastAPI()
|
||||
register_exception_handlers(app)
|
||||
|
||||
@@ -45,10 +45,14 @@ bl = mx_beamline()
|
||||
cfg = BeamlineConfig(bl)
|
||||
daq = AareDAQ(cfg, bl)
|
||||
|
||||
try:
|
||||
daq.sync_current_sample_from_tell(force=True)
|
||||
except Exception as e:
|
||||
logger.warning(f"Initial sample sync from TELL failed: {e}")
|
||||
|
||||
_all_pgroups_cache: dict[str, tuple[list[str], float]] = {}
|
||||
_ALL_PGROUPS_TTL_S = 60.0 # adjust TTL as needed
|
||||
|
||||
logger = setup_logger("aareDAQ")
|
||||
_face_detection_state: dict = {
|
||||
"seq": 0,
|
||||
"running": False,
|
||||
@@ -355,6 +359,16 @@ async def manual(s: SampleShortInfo, token: str = Depends(oauth2_scheme)):
|
||||
daq.create_sample(s)
|
||||
print(f"DB ID after creating {s.db_id}")
|
||||
|
||||
@app.post("/sample/resync")
|
||||
async def sample_resync(token: str = Depends(oauth2_scheme)) -> dict:
|
||||
auth.check_jwt_rw(cfg, auth.parse_token(token))
|
||||
daq.sync_current_sample_from_tell(force=True)
|
||||
logger.info("TELL sample cache resynced via API request.")
|
||||
return {
|
||||
"ok": True,
|
||||
"message": "TELL sample cache resynced.",
|
||||
}
|
||||
|
||||
|
||||
def get_spreadsheet(data: TokenData) -> SampleShortInfoList:
|
||||
if data.staff:
|
||||
@@ -454,7 +468,7 @@ async def maintenance(token: str = Depends(oauth2_scheme)) -> str:
|
||||
@app.post("/access/take_over_beamline")
|
||||
async def take_over_beamline(payload: RecoveryActionRequest, token: str = Depends(oauth2_scheme)) -> str:
|
||||
data = auth.parse_token(token)
|
||||
auth.check_jwt_staff(cfg, data)
|
||||
auth.check_jwt_staff_only(data)
|
||||
_validate_recovery_code(payload.confirmation_code)
|
||||
auth.force_current_sesion(cfg, data)
|
||||
logger.warning(
|
||||
@@ -466,7 +480,7 @@ async def take_over_beamline(payload: RecoveryActionRequest, token: str = Depend
|
||||
@app.post("/state/free_beamline")
|
||||
async def free_beamline(payload: RecoveryActionRequest, token: str = Depends(oauth2_scheme)) -> str:
|
||||
data = auth.parse_token(token)
|
||||
auth.check_jwt_staff(cfg, data)
|
||||
auth.check_jwt_staff_only(data)
|
||||
_validate_recovery_code(payload.confirmation_code)
|
||||
cfg.state_busy = False
|
||||
logger.warning(
|
||||
@@ -478,7 +492,7 @@ async def free_beamline(payload: RecoveryActionRequest, token: str = Depends(oau
|
||||
@app.post("/recovery/recover_beamline")
|
||||
async def recover_beamline(payload: RecoveryActionRequest, token: str = Depends(oauth2_scheme)) -> dict:
|
||||
data = auth.parse_token(token)
|
||||
auth.check_jwt_staff(cfg, data)
|
||||
auth.check_jwt_staff_only(data)
|
||||
_validate_recovery_code(payload.confirmation_code)
|
||||
|
||||
sample_mounted = _sample_is_mounted()
|
||||
@@ -509,7 +523,7 @@ async def recover_beamline(payload: RecoveryActionRequest, token: str = Depends(
|
||||
@app.post("/recovery/unmount_sample")
|
||||
async def recovery_unmount_sample(payload: RecoveryActionRequest, token: str = Depends(oauth2_scheme)) -> dict:
|
||||
data = auth.parse_token(token)
|
||||
auth.check_jwt_staff(cfg, data)
|
||||
auth.check_jwt_staff_only(data)
|
||||
_validate_recovery_code(payload.confirmation_code)
|
||||
|
||||
auth.force_current_sesion(cfg, data)
|
||||
|
||||
@@ -72,6 +72,20 @@ class RecoveryPanel(QWidget):
|
||||
)
|
||||
layout.addWidget(self._status)
|
||||
|
||||
self._last_action = QLabel("Last action: -", self)
|
||||
self._last_action.setWordWrap(True)
|
||||
self._last_action.setStyleSheet(
|
||||
"QLabel {"
|
||||
" background: #eef6ff;"
|
||||
" color: #12406a;"
|
||||
" border: 1px solid #a8c7e6;"
|
||||
" border-radius: 6px;"
|
||||
" padding: 8px;"
|
||||
" font-weight: 600;"
|
||||
"}"
|
||||
)
|
||||
layout.addWidget(self._last_action)
|
||||
|
||||
self._take_over_btn = QPushButton("Take over beamline", self)
|
||||
self._take_over_btn.setStyleSheet(
|
||||
"QPushButton {"
|
||||
@@ -126,9 +140,24 @@ class RecoveryPanel(QWidget):
|
||||
self._recovery_unmount_btn.clicked.connect(self._recovery_unmount_sample)
|
||||
layout.addWidget(self._recovery_unmount_btn)
|
||||
|
||||
self._resync_sample_btn = QPushButton("Resync sample from TELL", self)
|
||||
self._resync_sample_btn.setStyleSheet(
|
||||
"QPushButton {"
|
||||
" background: #eef6ff;"
|
||||
" color: #12406a;"
|
||||
" border: 1px solid #a8c7e6;"
|
||||
" border-radius: 6px;"
|
||||
" padding: 8px;"
|
||||
" font-weight: 600;"
|
||||
"}"
|
||||
)
|
||||
self._resync_sample_btn.clicked.connect(self._resync_sample)
|
||||
layout.addWidget(self._resync_sample_btn)
|
||||
|
||||
layout.addStretch(1)
|
||||
|
||||
self._daq.update.connect(self._set_daq_status)
|
||||
self._daq.sample_resync_completed.connect(self._set_last_action)
|
||||
self._refresh_buttons()
|
||||
|
||||
def _prompt_recovery_code(self, action_name: str) -> str | None:
|
||||
@@ -185,6 +214,9 @@ class RecoveryPanel(QWidget):
|
||||
"" if beamline_busy else "Disabled because beamline does not currently appear busy."
|
||||
)
|
||||
|
||||
self._resync_sample_btn.setEnabled(True)
|
||||
self._resync_sample_btn.setToolTip("Force a one-shot sample reconciliation against TELL.")
|
||||
|
||||
def _confirm(self, title: str, msg: str) -> bool:
|
||||
reply = QMessageBox.warning(
|
||||
self,
|
||||
@@ -211,6 +243,7 @@ class RecoveryPanel(QWidget):
|
||||
code = self._prompt_recovery_code("Take over beamline")
|
||||
if not code:
|
||||
return
|
||||
self._last_action.setText("Last action: Taking over beamline session...")
|
||||
self._daq.take_over_beamline(code)
|
||||
|
||||
@Slot()
|
||||
@@ -223,6 +256,7 @@ class RecoveryPanel(QWidget):
|
||||
code = self._prompt_recovery_code("Free beamline")
|
||||
if not code:
|
||||
return
|
||||
self._last_action.setText("Last action: Clearing beamline busy flag...")
|
||||
self._daq.free_beamline(code)
|
||||
|
||||
@Slot()
|
||||
@@ -246,6 +280,7 @@ class RecoveryPanel(QWidget):
|
||||
code = self._prompt_recovery_code("Recover beamline")
|
||||
if not code:
|
||||
return
|
||||
self._last_action.setText("Last action: Recovering beamline to Maintenance...")
|
||||
self._daq.recover_beamline(code)
|
||||
|
||||
@Slot()
|
||||
@@ -260,8 +295,18 @@ class RecoveryPanel(QWidget):
|
||||
code = self._prompt_recovery_code("Unmount sample (recovery)")
|
||||
if not code:
|
||||
return
|
||||
self._last_action.setText("Last action: Performing recovery sample unmount...")
|
||||
self._daq.recovery_unmount_sample(code)
|
||||
|
||||
@Slot()
|
||||
def _resync_sample(self) -> None:
|
||||
self._last_action.setText("Last action: Resyncing sample cache from TELL...")
|
||||
self._daq.resync_sample()
|
||||
|
||||
@Slot(str)
|
||||
def _set_last_action(self, message: str) -> None:
|
||||
self._last_action.setText(f"Last action: {message}")
|
||||
|
||||
|
||||
class BeamlineRecoveryDialog(QDialog):
|
||||
def __init__(self, *, daq: DAQWorker, parent=None):
|
||||
|
||||
@@ -39,6 +39,7 @@ class DAQWorker(QObject):
|
||||
staff_pgroups_loaded = Signal(list)
|
||||
fluorimeter_update = Signal(list, list, int)
|
||||
fluorimeter_spectrum_update = Signal(FluorescenceSpectrumOutputModel)
|
||||
sample_resync_completed = Signal(str)
|
||||
|
||||
error_codes_loaded = Signal(dict)
|
||||
last_error_payload_changed = Signal(dict)
|
||||
@@ -361,6 +362,32 @@ class DAQWorker(QObject):
|
||||
|
||||
reply.deleteLater()
|
||||
|
||||
def _handle_sample_resync_response(self, reply: QNetworkReply):
|
||||
try:
|
||||
response_data = self.handle_response(reply)
|
||||
payload = json.loads(response_data) if response_data else {}
|
||||
message = str(payload.get("message") or "TELL sample cache resynced.")
|
||||
logger.info(message)
|
||||
self.status_message.emit(message, False)
|
||||
self.sample_resync_completed.emit(message)
|
||||
self.send_status_request()
|
||||
except Exception as e:
|
||||
logger.error(f"Sample resync failed: {e}")
|
||||
self.http_error.emit(str(e))
|
||||
|
||||
def _handle_recovery_action_response(self, reply: QNetworkReply, default_message: str):
|
||||
try:
|
||||
response_data = self.handle_response(reply)
|
||||
payload = json.loads(response_data) if response_data else {}
|
||||
message = str(payload.get("message") or default_message)
|
||||
logger.info(message)
|
||||
self.status_message.emit(message, False)
|
||||
self.recovery_action_completed.emit(message)
|
||||
self.send_status_request()
|
||||
except Exception as e:
|
||||
logger.error(f"Recovery action failed: {e}")
|
||||
self.http_error.emit(str(e))
|
||||
|
||||
def generic_post(self, url: str, body: str = ""):
|
||||
if self.__base_url is None:
|
||||
logger.info(f"POST /{url}: {body}")
|
||||
@@ -456,30 +483,62 @@ class DAQWorker(QObject):
|
||||
|
||||
@Slot(str)
|
||||
def free_beamline(self, confirmation_code: str):
|
||||
self.generic_post(
|
||||
"state/free_beamline",
|
||||
json.dumps({"confirmation_code": confirmation_code}),
|
||||
if self.__base_url is None:
|
||||
logger.info("POST /state/free_beamline")
|
||||
return
|
||||
|
||||
request = QNetworkRequest(QUrl(f"{self.__base_url}/state/free_beamline"))
|
||||
request.setRawHeader(b"Authorization", f"Bearer {self.__token}".encode("utf-8"))
|
||||
request.setRawHeader(b"Content-Type", b"application/json")
|
||||
body = json.dumps({"confirmation_code": confirmation_code})
|
||||
reply = self.__net_manager.post(request, QByteArray(body.encode("utf-8")))
|
||||
reply.finished.connect(
|
||||
lambda: self._handle_recovery_action_response(reply, "Beamline busy flag cleared.")
|
||||
)
|
||||
|
||||
@Slot(str)
|
||||
def take_over_beamline(self, confirmation_code: str):
|
||||
self.generic_post(
|
||||
"access/take_over_beamline",
|
||||
json.dumps({"confirmation_code": confirmation_code}),
|
||||
if self.__base_url is None:
|
||||
logger.info("POST /access/take_over_beamline")
|
||||
return
|
||||
|
||||
request = QNetworkRequest(QUrl(f"{self.__base_url}/access/take_over_beamline"))
|
||||
request.setRawHeader(b"Authorization", f"Bearer {self.__token}".encode("utf-8"))
|
||||
request.setRawHeader(b"Content-Type", b"application/json")
|
||||
body = json.dumps({"confirmation_code": confirmation_code})
|
||||
reply = self.__net_manager.post(request, QByteArray(body.encode("utf-8")))
|
||||
reply.finished.connect(
|
||||
lambda: self._handle_recovery_action_response(reply, "Beamline session taken over.")
|
||||
)
|
||||
|
||||
@Slot(str)
|
||||
def recover_beamline(self, confirmation_code: str):
|
||||
self.generic_post(
|
||||
"recovery/recover_beamline",
|
||||
json.dumps({"confirmation_code": confirmation_code}),
|
||||
if self.__base_url is None:
|
||||
logger.info("POST /recovery/recover_beamline")
|
||||
return
|
||||
|
||||
request = QNetworkRequest(QUrl(f"{self.__base_url}/recovery/recover_beamline"))
|
||||
request.setRawHeader(b"Authorization", f"Bearer {self.__token}".encode("utf-8"))
|
||||
request.setRawHeader(b"Content-Type", b"application/json")
|
||||
body = json.dumps({"confirmation_code": confirmation_code})
|
||||
reply = self.__net_manager.post(request, QByteArray(body.encode("utf-8")))
|
||||
reply.finished.connect(
|
||||
lambda: self._handle_recovery_action_response(reply, "Beamline recovered to Maintenance.")
|
||||
)
|
||||
|
||||
@Slot(str)
|
||||
def recovery_unmount_sample(self, confirmation_code: str):
|
||||
self.generic_post(
|
||||
"recovery/unmount_sample",
|
||||
json.dumps({"confirmation_code": confirmation_code}),
|
||||
if self.__base_url is None:
|
||||
logger.info("POST /recovery/unmount_sample")
|
||||
return
|
||||
|
||||
request = QNetworkRequest(QUrl(f"{self.__base_url}/recovery/unmount_sample"))
|
||||
request.setRawHeader(b"Authorization", f"Bearer {self.__token}".encode("utf-8"))
|
||||
request.setRawHeader(b"Content-Type", b"application/json")
|
||||
body = json.dumps({"confirmation_code": confirmation_code})
|
||||
reply = self.__net_manager.post(request, QByteArray(body.encode("utf-8")))
|
||||
reply.finished.connect(
|
||||
lambda: self._handle_recovery_action_response(reply, "Recovery unmount completed.")
|
||||
)
|
||||
|
||||
@Slot(str)
|
||||
@@ -732,6 +791,18 @@ class DAQWorker(QObject):
|
||||
def beam_size_mm(self, x: float, y: float):
|
||||
self.generic_post(f"beamline/beam_size_mm?x={x}&y={y}")
|
||||
|
||||
@Slot()
|
||||
def resync_sample(self):
|
||||
if self.__base_url is None:
|
||||
logger.info("POST /sample/resync")
|
||||
return
|
||||
|
||||
request = QNetworkRequest(QUrl(f"{self.__base_url}/sample/resync"))
|
||||
request.setRawHeader(b"Authorization", f"Bearer {self.__token}".encode("utf-8"))
|
||||
request.setRawHeader(b"Content-Type", b"application/json")
|
||||
reply = self.__net_manager.post(request, QByteArray(b""))
|
||||
reply.finished.connect(lambda: self._handle_sample_resync_response(reply))
|
||||
|
||||
@Slot()
|
||||
def unmount(self):
|
||||
self.generic_post("sample/unmount")
|
||||
|
||||
Reference in new issue
Block a user