Skip to content

Commit 611af93

Browse files
authored
Fix errors from testing mx_bluesky (#1036)
* Wait for setting ROI to finish before we call disarm finished * Use correct PV for checking robot light curtain and do it in the device * Add Stageable protocol for eiger * Fix unit tests for robot * Update names of tests
1 parent ffdd9e8 commit 611af93

File tree

3 files changed

+92
-23
lines changed

3 files changed

+92
-23
lines changed

src/dodal/devices/eiger.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from dataclasses import dataclass
33
from enum import Enum
44

5+
from bluesky.protocols import Stageable
56
from ophyd import Component, Device, EpicsSignalRO, Signal
67
from ophyd.areadetector.cam import EigerDetectorCam
78
from ophyd.status import AndStatus, Status, StatusBase
@@ -42,7 +43,7 @@ class InternalEigerTriggerMode(Enum):
4243
}
4344

4445

45-
class EigerDetector(Device):
46+
class EigerDetector(Device, Stageable):
4647
class ArmingSignal(Signal):
4748
def set(self, value, *, timeout=None, settle_time=None, **kwargs):
4849
assert isinstance(self.parent, EigerDetector)
@@ -161,7 +162,7 @@ def unstage(self) -> bool:
161162
status_ok = self.odin.check_and_wait_for_odin_state(
162163
self.timeouts.general_status_timeout
163164
)
164-
self.disable_roi_mode()
165+
self.disable_roi_mode().wait(self.timeouts.general_status_timeout)
165166
self.disarming_status.set_finished()
166167
return status_ok
167168

src/dodal/devices/robot.py

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from bluesky.protocols import Movable
66
from ophyd_async.core import (
77
AsyncStatus,
8+
Device,
89
StandardReadable,
910
StrictEnum,
1011
set_and_wait_for_value,
@@ -38,23 +39,36 @@ class PinMounted(StrictEnum):
3839
PIN_MOUNTED = "Pin Mounted"
3940

4041

42+
class ErrorStatus(Device):
43+
def __init__(self, prefix: str) -> None:
44+
self.str = epics_signal_r(str, prefix + "_ERR_MSG")
45+
self.code = epics_signal_r(int, prefix + "_ERR_CODE")
46+
super().__init__()
47+
48+
async def raise_if_error(self, raise_from: Exception):
49+
error_code = await self.code.get_value()
50+
if error_code:
51+
error_string = await self.str.get_value()
52+
raise RobotLoadFailed(int(error_code), error_string) from raise_from
53+
54+
4155
class BartRobot(StandardReadable, Movable):
4256
"""The sample changing robot."""
4357

4458
# How long to wait for the robot if it is busy soaking/drying
4559
NOT_BUSY_TIMEOUT = 5 * 60
60+
4661
# How long to wait for the actual load to happen
4762
LOAD_TIMEOUT = 60
63+
64+
# Error codes that we do special things on
4865
NO_PIN_ERROR_CODE = 25
66+
LIGHT_CURTAIN_TRIPPED = 40
4967

5068
# How far the gonio position can be out before loading will fail
5169
LOAD_TOLERANCE_MM = 0.02
5270

53-
def __init__(
54-
self,
55-
name: str,
56-
prefix: str,
57-
) -> None:
71+
def __init__(self, name: str, prefix: str) -> None:
5872
self.barcode = epics_signal_r(str, prefix + "BARCODE")
5973
self.gonio_pin_sensor = epics_signal_r(PinMounted, prefix + "PIN_MOUNTED")
6074

@@ -69,8 +83,10 @@ def __init__(
6983
self.load = epics_signal_x(prefix + "LOAD.PROC")
7084
self.program_running = epics_signal_r(bool, prefix + "PROGRAM_RUNNING")
7185
self.program_name = epics_signal_r(str, prefix + "PROGRAM_NAME")
72-
self.error_str = epics_signal_r(str, prefix + "PRG_ERR_MSG")
73-
self.error_code = epics_signal_r(int, prefix + "PRG_ERR_CODE")
86+
87+
self.prog_error = ErrorStatus(prefix + "PRG")
88+
self.controller_error = ErrorStatus(prefix + "CNTL")
89+
7490
self.reset = epics_signal_x(prefix + "RESET.PROC")
7591
super().__init__(name=name)
7692

@@ -81,7 +97,7 @@ async def pin_mounted_or_no_pin_found(self):
8197
"""
8298

8399
async def raise_if_no_pin():
84-
await wait_for_value(self.error_code, self.NO_PIN_ERROR_CODE, None)
100+
await wait_for_value(self.prog_error.code, self.NO_PIN_ERROR_CODE, None)
85101
raise RobotLoadFailed(self.NO_PIN_ERROR_CODE, "Pin was not detected")
86102

87103
async def wfv():
@@ -108,6 +124,9 @@ async def wfv():
108124
raise
109125

110126
async def _load_pin_and_puck(self, sample_location: SampleLocation):
127+
if await self.controller_error.code.get_value() == self.LIGHT_CURTAIN_TRIPPED:
128+
LOGGER.info("Light curtain tripped, trying again")
129+
await self.reset.trigger()
111130
LOGGER.info(f"Loading pin {sample_location}")
112131
if await self.program_running.get_value():
113132
LOGGER.info(
@@ -137,6 +156,6 @@ async def set(self, value: SampleLocation):
137156
)
138157
except (asyncio.TimeoutError, TimeoutError) as e:
139158
# Will only need to catch asyncio.TimeoutError after https://github.com/bluesky/ophyd-async/issues/572
140-
error_code = await self.error_code.get_value()
141-
error_string = await self.error_str.get_value()
142-
raise RobotLoadFailed(int(error_code), error_string) from e
159+
await self.prog_error.raise_if_error(e)
160+
await self.controller_error.raise_if_error(e)
161+
raise RobotLoadFailed(0, "Robot timed out") from e

tests/devices/unit_tests/test_bart_robot.py

Lines changed: 59 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
11
from asyncio import create_task, sleep
2-
from unittest.mock import ANY, AsyncMock, MagicMock, patch
2+
from unittest.mock import ANY, AsyncMock, MagicMock, call, patch
33

44
import pytest
5-
from ophyd_async.testing import set_mock_value
5+
from ophyd_async.core import AsyncStatus
6+
from ophyd_async.testing import (
7+
callback_on_mock_put,
8+
get_mock,
9+
get_mock_put,
10+
set_mock_value,
11+
)
612

713
from dodal.devices.robot import BartRobot, PinMounted, RobotLoadFailed, SampleLocation
814

@@ -28,8 +34,8 @@ async def _sleep(*args):
2834

2935
device._load_pin_and_puck = AsyncMock(side_effect=_sleep)
3036

31-
set_mock_value(device.error_code, (expected_error_code := 10))
32-
set_mock_value(device.error_str, (expected_error_string := "BAD"))
37+
set_mock_value(device.prog_error.code, (expected_error_code := 10))
38+
set_mock_value(device.prog_error.str, (expected_error_string := "BAD"))
3339

3440
with pytest.raises(RobotLoadFailed) as e:
3541
await device.set(SampleLocation(0, 0))
@@ -82,32 +88,36 @@ async def test_given_program_not_running_and_pin_unmounting_but_new_pin_not_moun
8288
assert "Waiting on new pin loaded" in last_log
8389

8490

85-
async def test_given_program_not_running_and_pin_unmounts_then_mounts_when_load_pin_then_pin_loaded():
86-
device = await _get_bart_robot()
91+
async def set_with_happy_path(device: BartRobot) -> AsyncStatus:
92+
"""Mocks the logic that the robot would do on a successful load"""
8793
device.LOAD_TIMEOUT = 0.05 # type: ignore
8894
set_mock_value(device.program_running, False)
8995
set_mock_value(device.gonio_pin_sensor, PinMounted.PIN_MOUNTED)
90-
device.load = AsyncMock(side_effect=device.load)
9196
status = device.set(SampleLocation(15, 10))
9297
await sleep(0.025)
93-
device.load.trigger.assert_called_once() # type:ignore
9498

9599
set_mock_value(device.gonio_pin_sensor, PinMounted.NO_PIN_MOUNTED)
96100
await sleep(0.025)
97101

98102
set_mock_value(device.gonio_pin_sensor, PinMounted.PIN_MOUNTED)
103+
return status
104+
105+
106+
async def test_given_program_not_running_and_pin_unmounts_then_mounts_when_load_pin_then_pin_loaded():
107+
device = await _get_bart_robot()
108+
status = await set_with_happy_path(device)
99109
await status
100110
assert status.success
101111
assert (await device.next_puck.get_value()) == 15
102112
assert (await device.next_pin.get_value()) == 10
103-
device.load.trigger.assert_called_once() # type:ignore
113+
get_mock_put(device.load).assert_called_once()
104114

105115

106116
async def test_given_waiting_for_pin_to_mount_when_no_pin_mounted_then_error_raised():
107117
device = await _get_bart_robot()
108118
status = create_task(device.pin_mounted_or_no_pin_found())
109119
await sleep(0.2)
110-
set_mock_value(device.error_code, 25)
120+
set_mock_value(device.prog_error.code, 25)
111121
await sleep(0.01)
112122
with pytest.raises(RobotLoadFailed):
113123
await status
@@ -128,3 +138,42 @@ async def test_set_waits_for_both_timeouts(mock_wait_for: AsyncMock):
128138
device._load_pin_and_puck = MagicMock()
129139
await device.set(SampleLocation(1, 2))
130140
mock_wait_for.assert_awaited_once_with(ANY, timeout=0.02)
141+
142+
143+
async def test_moving_the_robot_will_reset_error_if_light_curtain_is_tripped_and_still_throw_if_error_not_cleared():
144+
device = await _get_bart_robot()
145+
set_mock_value(device.controller_error.code, BartRobot.LIGHT_CURTAIN_TRIPPED)
146+
147+
with pytest.raises(RobotLoadFailed) as e:
148+
await device.set(SampleLocation(1, 2))
149+
assert e.value.error_code == 40
150+
151+
get_mock(device).assert_has_calls(
152+
[
153+
call.reset.put(None, wait=True),
154+
call.next_puck.put(ANY, wait=True),
155+
call.next_pin.put(ANY, wait=True),
156+
call.load.put(None, wait=True),
157+
]
158+
)
159+
160+
161+
async def test_moving_the_robot_will_reset_error_if_light_curtain_is_tripped_and_continue_if_error_cleared():
162+
device = await _get_bart_robot()
163+
set_mock_value(device.controller_error.code, BartRobot.LIGHT_CURTAIN_TRIPPED)
164+
165+
callback_on_mock_put(
166+
device.reset,
167+
lambda *_, **__: set_mock_value(device.controller_error.code, 0),
168+
)
169+
170+
await (await set_with_happy_path(device))
171+
172+
get_mock(device).assert_has_calls(
173+
[
174+
call.reset.put(None, wait=True),
175+
call.next_puck.put(ANY, wait=True),
176+
call.next_pin.put(ANY, wait=True),
177+
call.load.put(None, wait=True),
178+
]
179+
)

0 commit comments

Comments
 (0)