Skip to content

Commit 2e4d3b1

Browse files
rtuck99DominicOram
andauthored
Allow triggering an unload by calling robot.set(None) (#1678)
Co-authored-by: Dominic Oram <[email protected]>
1 parent 5fe631f commit 2e4d3b1

File tree

2 files changed

+80
-8
lines changed

2 files changed

+80
-8
lines changed

src/dodal/devices/robot.py

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ async def raise_if_error(self, raise_from: Exception):
6161
raise RobotLoadError(int(error_code), error_string) from raise_from
6262

6363

64-
class BartRobot(StandardReadable, Movable[SampleLocation]):
64+
class BartRobot(StandardReadable, Movable[SampleLocation | None]):
6565
"""The sample changing robot."""
6666

6767
# How long to wait for the robot if it is busy soaking/drying
@@ -104,7 +104,6 @@ def __init__(self, prefix: str, name: str = "") -> None:
104104
self.init = epics_signal_x(prefix + "INIT.PROC")
105105
self.soak = epics_signal_x(prefix + "SOAK.PROC")
106106
self.home = epics_signal_x(prefix + "GOHM.PROC")
107-
self.unload = epics_signal_x(prefix + "UNLD.PROC")
108107
self.dry = epics_signal_x(prefix + "DRY.PROC")
109108
self.open = epics_signal_x(prefix + "COLO.PROC")
110109
self.close = epics_signal_x(prefix + "COLC.PROC")
@@ -175,12 +174,23 @@ async def _load_pin_and_puck(self, sample_location: SampleLocation):
175174
await self.pin_mounted_or_no_pin_found()
176175

177176
@AsyncStatus.wrap
178-
async def set(self, value: SampleLocation):
177+
async def set(self, value: SampleLocation | None):
178+
"""
179+
Perform a sample load from the specified sample location
180+
Args:
181+
value: The pin and puck to load, or None to unload the sample.
182+
Raises:
183+
RobotLoadError if a timeout occurs, or if an error occurs loading the smaple.
184+
"""
179185
try:
180-
await wait_for(
181-
self._load_pin_and_puck(value),
182-
timeout=self.LOAD_TIMEOUT + self.NOT_BUSY_TIMEOUT,
183-
)
186+
if value is not None:
187+
await wait_for(
188+
self._load_pin_and_puck(value),
189+
timeout=self.LOAD_TIMEOUT + self.NOT_BUSY_TIMEOUT,
190+
)
191+
else:
192+
await self.unload.trigger(timeout=self.LOAD_TIMEOUT)
193+
await wait_for_value(self.program_running, False, self.NOT_BUSY_TIMEOUT)
184194
except TimeoutError as e:
185195
await self.prog_error.raise_if_error(e)
186196
await self.controller_error.raise_if_error(e)

tests/devices/test_bart_robot.py

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1+
import asyncio
12
import traceback
2-
from asyncio import create_task
3+
from asyncio import Event, create_task
34
from functools import partial
45
from unittest.mock import ANY, AsyncMock, MagicMock, call, patch
56

@@ -22,6 +23,29 @@
2223
)
2324

2425

26+
@pytest.fixture
27+
async def robot_for_unload():
28+
device = BartRobot("robot", "-MO-ROBOT-01:")
29+
device.NOT_BUSY_TIMEOUT = 0.3 # type: ignore
30+
device.LOAD_TIMEOUT = 0.3 # type: ignore
31+
await device.connect(mock=True)
32+
33+
trigger_complete = Event()
34+
drying_complete = Event()
35+
36+
async def finish_later():
37+
await drying_complete.wait()
38+
set_mock_value(device.program_running, False)
39+
40+
async def fake_unload(*args, **kwargs):
41+
set_mock_value(device.program_running, True)
42+
await trigger_complete.wait()
43+
asyncio.create_task(finish_later())
44+
45+
get_mock_put(device.unload).side_effect = fake_unload
46+
return device, trigger_complete, drying_complete
47+
48+
2549
async def _get_bart_robot() -> BartRobot:
2650
device = BartRobot("robot", "-MO-ROBOT-01:")
2751
device.LOAD_TIMEOUT = 1 # type: ignore
@@ -206,3 +230,41 @@ async def test_moving_the_robot_will_reset_error_if_light_curtain_is_tripped_and
206230
call.load.put(None, wait=True),
207231
]
208232
)
233+
234+
235+
async def test_unloading_the_robot_waits_for_drying_to_complete(robot_for_unload):
236+
robot, trigger_completed, drying_completed = robot_for_unload
237+
drying_completed.set()
238+
unload_status = robot.set(None)
239+
240+
await asyncio.sleep(0.1)
241+
assert not unload_status.done
242+
get_mock_put(robot.unload).assert_called_once()
243+
244+
trigger_completed.set()
245+
await unload_status
246+
assert unload_status.done
247+
248+
249+
async def test_unloading_the_robot_times_out_if_unloading_takes_too_long(
250+
robot_for_unload,
251+
):
252+
robot, trigger_completed, drying_completed = robot_for_unload
253+
drying_completed.set()
254+
unload_status = robot.set(None)
255+
256+
with pytest.raises(RobotLoadError) as exc_info:
257+
await unload_status
258+
259+
assert isinstance(exc_info.value.__cause__, TimeoutError)
260+
261+
262+
async def test_unloading_the_robot_times_out_if_drying_takes_too_long(robot_for_unload):
263+
robot, trigger_completed, drying_completed = robot_for_unload
264+
trigger_completed.set()
265+
unload_status = robot.set(None)
266+
267+
with pytest.raises(RobotLoadError) as exc_info:
268+
await unload_status
269+
270+
assert isinstance(exc_info.value.__cause__, TimeoutError)

0 commit comments

Comments
 (0)