diff --git a/CHANGELOG.md b/CHANGELOG.md index 04cc747a..a1744da0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ - `/status` now publishes `pss_prohibited`/`pss_alarm`; the GUI blocks manual mount/unmount and the automation Run button immediately (with a pop-up) when the hutch door is open, instead of letting the robot fail to move server-side. - Centralised the per-action precondition checks (ring current, safety shutter, hutch door) into one combined "continue?" dialog used by all data-collection buttons (Evaluate grid, X-ray centering, Run screening/rotation/fluorescence, simple rotation), with a session-global "Don't ask me again for 1 hour" snooze. - Live automation now pauses and auto-resumes when the beam, experiment shutter, hutch door or robot (TELL) are not ready (with Continue-now / Stop overrides), gated by a "Pause on bad conditions" checkbox (default on) that replaces the dead `CHECK_ENABLED` constant. + - Restored "No Pin in Gripper" detection: a mount that returns on the intermediate TELL "Busy" state event now verifies the robot's `get_mounted_sample()` after the command completes, so an empty-gripper mount surfaces `MountingFailed` (and the GUI pop-up) instead of silently reporting success. ## 0.3.1 (2026-04-29) - **DAQ/Server**: diff --git a/src/aare/devices/tell_client.py b/src/aare/devices/tell_client.py index 1d6452cb..85743544 100755 --- a/src/aare/devices/tell_client.py +++ b/src/aare/devices/tell_client.py @@ -293,6 +293,15 @@ class TellClient: timeout=wait_timeout, msg=f"Mount {segment}{puck}-{sample}: " ) logger.info(f"Check command okay response: {msg}") + # check_command_ok blocks (wait_not_busy) until the whole + # mount sequence has finished, so the gripper-detection event + # we observed as an intermediate "state"/Busy event is now + # resolved. The robot's own record is authoritative: if it + # reports nothing mounted, the gripper was empty (no pin), + # which we must surface rather than reporting success. + if self.get_mounted_sample() is None: + logger.warning("Mount command completed but robot reports no sample mounted") + return TellEventValueEnum.NO_PIN_IN_GRIPPER return TellEventValueEnum.SUCCESS elif ( event == TellEventTypeEnum.GIPPER_DETECTION.value diff --git a/tests/unit/devices/test_tell_client.py b/tests/unit/devices/test_tell_client.py index 4c1beb6e..29aa002d 100644 --- a/tests/unit/devices/test_tell_client.py +++ b/tests/unit/devices/test_tell_client.py @@ -52,6 +52,8 @@ def test_mount_state_event_succeeds_when_command_completes( "id": 3017907, } client = TellClient(mock_beamline, backend=mock_backend) + # Robot reports the requested sample as mounted -> genuine success. + client.get_mounted_sample = lambda: _mount_address() result = client.mount(_mount_address(), wait=True) @@ -59,6 +61,27 @@ def test_mount_state_event_succeeds_when_command_completes( mock_backend.get_result.assert_called() # check_command_ok consulted the result +def test_mount_state_event_reports_no_pin_when_nothing_mounted( + mock_beamline, mock_backend +): + """If the command completes but the robot reports nothing mounted, the + gripper was empty: mount() must return NO_PIN_IN_GRIPPER (so the service + raises MountingFailed) rather than reporting success.""" + mock_backend.wait_events.return_value = ("state", "Busy") + mock_backend.get_result.return_value = { + "status": "completed", + "return": "A39", + "exception": None, + "id": 3017907, + } + client = TellClient(mock_beamline, backend=mock_backend) + client.get_mounted_sample = lambda: None + + result = client.mount(_mount_address(), wait=True) + + assert result == TellEventValueEnum.NO_PIN_IN_GRIPPER + + def test_mount_state_event_raises_when_command_not_completed( mock_beamline, mock_backend ):