feat: show AUTO CENTERING in the sample camera view while a loop centering runs
CI / lint (push) Skipped
CI / test (3.12) (push) Skipped
CI / test (3.13) (push) Skipped
CI / test-with-beamline-plugins (pxi_bec) (push) Skipped
CI / test-with-beamline-plugins (pxii_bec) (push) Skipped
CI / test-with-beamline-plugins (pxiii_bec) (push) Skipped
CI / lint (pull_request) Failing after 1m2s
CI / test (3.12) (pull_request) Failing after 1m5s
CI / test (3.13) (pull_request) Failing after 1m10s
CI / test (3.14) (pull_request) Failing after 1m19s
CI / test-with-beamline-plugins (pxi_bec) (pull_request) Failing after 1m19s
CI / test-with-beamline-plugins (pxiii_bec) (pull_request) Failing after 1m20s
CI / test-with-beamline-plugins (pxii_bec) (pull_request) Failing after 1m25s
CI / test-with-coverage (pull_request) Failing after 1m25s
CI / coverage-analysis (pull_request) Skipped

/status cannot tell an auto centering from a manual alignment move (both
are busy + SampleAlignment, the combo the overlay deliberately hides), so
the DAQ worker emits its own auto_centering flag: True while its own
/alc/center_loop POST is in flight (the endpoint blocks until done) or
while the automation progress reports the Center step running. The
sample camera views pass it to build_busy_overlay_style, which paints a
purple AUTO CENTERING wave text like the ROBOT MOUNTING/COOLING ones.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
2026-09-08 11:35:26 +02:00
co-authored by Claude Fable 5.1
parent 2fdb1952b6
commit 2df433b33c
5 changed files with 78 additions and 2 deletions
+2
View File
@@ -1202,6 +1202,8 @@ class MainWindow(QMainWindow):
self.daq.update.connect(self.sample_camera.update_daq_status)
self.daq.update.connect(self.compact_sample_camera.update_daq_status)
self.daq.update.connect(self.portrait_sample_camera.update_daq_status)
for camera in (self.sample_camera, self.compact_sample_camera, self.portrait_sample_camera):
self.daq.auto_centering.connect(camera.set_auto_centering)
self.daq.update.connect(self.tell_samples.update_daq_status)
self.daq.update.connect(self.ref_tools_panel.update_daq_status)
if self.prediction_thread is not None:
+30 -2
View File
@@ -84,6 +84,9 @@ class DAQWorker(QObject):
http_error = Signal(str)
status_message = Signal(str, bool)
automation_progress = Signal(object)
# True while a loop centering runs — /status alone cannot tell it from
# a manual alignment move (see build_busy_overlay_style).
auto_centering = Signal(bool)
gui_sessions_loaded = Signal(list)
gui_close_requested = Signal(int, int, str)
recovery_action_completed = Signal(str)
@@ -154,6 +157,10 @@ class DAQWorker(QObject):
self._timer.start()
self._counter = 0
self._automation_progress_buffer = ""
# Two centering sources OR-ed into auto_centering: our own
# /alc/center_loop POST in flight, and the automation Center step.
self._manual_centering = False
self._automation_centering = False
self._cleanup_done = False
self._last_auth_error_log_ts = 0.0
@@ -747,7 +754,7 @@ class DAQWorker(QObject):
"""
if self._base_url is None:
logger.info(f"POST /{url}: {body}")
return
return None
request = QNetworkRequest(QUrl(f"{self._base_url}/{url}"))
request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode())
@@ -755,6 +762,7 @@ class DAQWorker(QObject):
request.setRawHeader(b"Content-Type", b"application/json")
reply = self._net_manager.post(request, QByteArray(body.encode("utf-8")))
reply.finished.connect(lambda: self.handle_req_response(reply))
return reply
def generic_put(self, url: str, body: str = ""):
"""
@@ -844,7 +852,20 @@ class DAQWorker(QObject):
@Slot()
def center_loop(self):
self.generic_post("alc/center_loop")
# The endpoint blocks server-side until centering ends, so the reply
# doubles as the "done" edge for the camera overlay.
reply = self.generic_post("alc/center_loop")
if reply is None:
return
self._set_centering(manual=True)
reply.finished.connect(lambda: self._set_centering(manual=False))
def _set_centering(self, *, manual: bool | None = None, automation: bool | None = None):
if manual is not None:
self._manual_centering = manual
if automation is not None:
self._automation_centering = automation
self.auto_centering.emit(self._manual_centering or self._automation_centering)
@Slot()
def force_session(self):
@@ -1968,6 +1989,13 @@ class DAQWorker(QObject):
progress = self._parse_automation_progress(progress_payload)
self.automation_progress.emit(progress)
self._emit_automation_progress_events(progress)
self._set_centering(
automation=not progress.finished
and any(
step.step == WorkflowStateKind.LOOP_CENTRE and step.status == StepStatus.RUNNING
for step in progress.steps
)
)
current = progress.current_step or "Idle"
if progress.finished:
+22
View File
@@ -17,6 +17,9 @@ from aare.gui.styles import (
BUSY_PSI_RED,
BUSY_PSI_RED_BORDER,
BUSY_PSI_RED_DOT,
BUSY_PURPLE,
BUSY_PURPLE_BORDER,
BUSY_PURPLE_DOT,
BUSY_RED_BADGE,
BUSY_RED_BORDER,
BUSY_RED_DOT,
@@ -174,6 +177,7 @@ def build_busy_overlay_style(
is_busy: bool,
tell_state: TellStateModel | None,
session_state: SessionsStateEnum | None = None,
auto_centering: bool = False,
) -> BusyOverlayStyle | None:
if session_state in {
SessionsStateEnum.OwnedByElse,
@@ -190,6 +194,24 @@ def build_busy_overlay_style(
accent_dot=BUSY_YELLOW_DOT,
subtext="Grab the baton if you need to interact with GUI",
)
# Auto loop centering is busy + SampleAlignment in /status — the exact
# combo the callers hide (manual omega moves look identical), so it
# gets its own flag: the GUI knows it POSTed /alc/center_loop, or the
# automation progress says the Center step is running. Checked before
# is_busy because the callers pass is_busy=False during alignment.
if auto_centering:
return BusyOverlayStyle(
text="AUTO CENTERING",
badge_bg=BUSY_PURPLE,
badge_fg=WHITE,
overlay_fill=qcolor(BUSY_PURPLE, 190),
overlay_border=qcolor(BUSY_PURPLE_BORDER, 235),
overlay_text=qcolor(WHITE),
accent_dot=BUSY_PURPLE_DOT,
animate=True,
)
if not is_busy:
return None
+8
View File
@@ -130,6 +130,7 @@ class SampleCameraImageLabel(QGraphicsView):
self._last_grid_update_ts = 0.0
self._grid_update_min_interval_s = 1.0 / 25.0
self._tell_state = None
self._auto_centering = False
self._busy_overlay_style: BusyOverlayStyle | None = None
self._geom = geom
@@ -295,6 +296,12 @@ class SampleCameraImageLabel(QGraphicsView):
self._smoothed_target_point = None
self.update()
@Slot(bool)
def set_auto_centering(self, active: bool) -> None:
# ponytail: only stored — the overlay style is rebuilt on the next
# /status tick (500 ms), well within the seconds a centering takes.
self._auto_centering = active
def _busy_overlay_text(self) -> str:
tell_state = self._tell_state
if tell_state is None:
@@ -843,6 +850,7 @@ class SampleCameraImageLabel(QGraphicsView):
is_busy=bool(s.busy) and s.state != BeamlineStateEnum.SampleAlignment,
tell_state=s.tell_state,
session_state=self._session_state,
auto_centering=self._auto_centering,
)
if new_busy_style != self._busy_overlay_style:
self._busy_overlay_style = new_busy_style
+16
View File
@@ -2,6 +2,7 @@
the badge pill (which reads as a button). These checks fail if the text
renderer stops painting or the video view stops routing through it."""
from aarecommon.models.models import SessionsStateEnum
from PySide6.QtCore import Qt
from PySide6.QtGui import QPainter, QPixmap
@@ -34,3 +35,18 @@ def test_video_view_paints_busy_text(qtbot):
assert view.grab().toImage() != idle, "busy style must change the rendered view"
view.set_busy_overlay_style(None)
assert view.grab().toImage() == idle, "clearing the style must restore the view"
def test_auto_centering_flag_overrides_alignment_gate():
# Callers pass is_busy=False during SampleAlignment; the flag must still
# produce the overlay, and the viewing-mode badge must still win.
style = build_busy_overlay_style(is_busy=False, tell_state=None, auto_centering=True)
assert style is not None and style.text == "AUTO CENTERING"
assert build_busy_overlay_style(is_busy=False, tell_state=None) is None
viewing = build_busy_overlay_style(
is_busy=False,
tell_state=None,
session_state=SessionsStateEnum.OwnedByElse,
auto_centering=True,
)
assert viewing is not None and viewing.text.startswith("Viewing mode")