diff --git a/src/aare/gui/panels/data_collection_settings.py b/src/aare/gui/panels/data_collection_settings.py index fc833c2c..6929d5de 100644 --- a/src/aare/gui/panels/data_collection_settings.py +++ b/src/aare/gui/panels/data_collection_settings.py @@ -195,6 +195,8 @@ class DataCollectionSettings(QFrame): # startup must not trigger an auto-centering (hardware would move # uninvited on every restart). self._status_seen = False + # Set on mount, fired once the hw lock frees (see _track_sample). + self._center_after_mount_pending = False self._tab_bar.currentChanged.connect(self._stack.setCurrentIndex) self._tab_bar.currentChanged.connect(self._on_tab_changed) @@ -242,23 +244,31 @@ class DataCollectionSettings(QFrame): self.raster.update_daq_status(s) self.screening.update_daq_status(s) self.simple.update_daq_status(s) - self._track_sample(None if s.sample is None else s.sample.db_id) + self._track_sample(None if s.sample is None else s.sample.db_id, s.busy) - def _track_sample(self, db_id: int | None): + def _track_sample(self, db_id: int | None, busy: bool = False): # Separate from update_daq_status so tests can drive mount detection # without building a full DAQStatusModel. if db_id is None: # Forget on unmount so remounting the same sample counts as a # fresh mount below. self._sample_id = None + self._center_after_mount_pending = False elif db_id != self._sample_id: self._sample_id = db_id self._tab_bar.setCurrentIndex(0) - if self._status_seen and self.auto_center_after_mount.isChecked(): - # Reuse the button wiring (clicked -> daq.center_loop in - # main_window) instead of a second signal path. - self.find_tip.click() + self._center_after_mount_pending = ( + self._status_seen and self.auto_center_after_mount.isChecked() + ) self._status_seen = True + # The server publishes the new sample while the mount still holds the + # hw lock, and /alc/center_loop refuses (non-blocking lock) while busy, + # so firing on the id change alone was silently rejected. Wait for idle. + if self._center_after_mount_pending and not busy: + self._center_after_mount_pending = False + # Reuse the button wiring (clicked -> daq.center_loop in + # main_window) instead of a second signal path. + self.find_tip.click() @Slot(int) def _on_tab_changed(self, idx: int): diff --git a/tests/unit/gui/test_data_collection_settings.py b/tests/unit/gui/test_data_collection_settings.py index 252566c5..0a7990bd 100644 --- a/tests/unit/gui/test_data_collection_settings.py +++ b/tests/unit/gui/test_data_collection_settings.py @@ -353,6 +353,29 @@ def test_auto_center_fires_on_mount_only_when_armed(settings_panel): assert len(clicks) == 3 +def test_auto_center_waits_for_mount_to_release_hw_lock(settings_panel): + # Server shows the new sample while the mount still holds the hw lock; + # center_loop would be refused then, so the click must wait for idle. + clicks = [] + settings_panel.find_tip.clicked.connect(lambda: clicks.append(1)) + settings_panel.auto_center_after_mount.setChecked(True) + settings_panel._track_sample(1) + settings_panel._track_sample(None) + + settings_panel._track_sample(2, busy=True) + settings_panel._track_sample(2, busy=True) + assert not clicks + settings_panel._track_sample(2, busy=False) + assert len(clicks) == 1 + settings_panel._track_sample(2, busy=False) + assert len(clicks) == 1 + + # Unmount before the lock frees drops the pending centering. + settings_panel._track_sample(3, busy=True) + settings_panel._track_sample(None, busy=False) + assert len(clicks) == 1 + + # --------------------------------------------------------------------------- # Pending color while typing (movestate property, committed on Enter/focus-out) # ---------------------------------------------------------------------------