Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions pylabrobot/bmg_labtech/clariostar/driver/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ def parse_warning_codes(reply: bytes) -> Optional[Tuple[int, int, int, int, int]
return _quit_codes(reply, WARNING_BIT)


# What the first quit code means, for the codes seen in replies from a reader. 1-3 mean the reader
# received a malformed frame.
# What the first quit code means, for the codes seen in replies from a reader, and 242 and 243 not
# seen yet. 1-3 mean the reader received a malformed frame.
QUIT_CODE_DESCRIPTIONS: Dict[int, str] = {
1: "the reader received a frame with a wrong checksum",
2: "the reader received a frame with a wrong end byte",
Expand All @@ -48,6 +48,8 @@ def parse_warning_codes(reply: bytes) -> Optional[Tuple[int, int, int, int, int]
202: "the reader is busy",
213: "no measurement results are available",
214: "no test run is active",
242: "the wellplate sensor failed or could not be read",
243: "no wellplate is inserted, or the focal height is too low for the one detected",
250: "the motor controller reported a collision",
}

Expand Down
33 changes: 31 additions & 2 deletions pylabrobot/bmg_labtech/clariostar/driver/features/drawer.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import logging
from typing import TYPE_CHECKING, Optional

from pylabrobot.resources import Coordinate, PlateHolder
from pylabrobot.resources import Coordinate, Plate, PlateHolder

if TYPE_CHECKING:
from ..master import CLARIOstarDriver
Expand All @@ -25,6 +25,9 @@ class Drawer:
default_move_timeout: float = 20.0
# Seconds between status polls while the drawer moves; as the driver's `default_poll_interval`.
default_poll_interval: float = 0.25
# The tallest plate, lid included, the drawer closes with, in mm: a cautious limit until the
# reader's clearance is measured.
_maximum_plate_size_z: float = 20.0

def __init__(self, driver: "CLARIOstarDriver"):
"""
Expand Down Expand Up @@ -102,7 +105,33 @@ async def open(self) -> None:
await self._move(open_drawer=True)
logger.info("drawer open")

def _check_plate_size_z(self) -> None:
"""Raise if the plate on the plate holder, with its lid, is taller than the drawer takes.

Raises:
ValueError: if the plate is above `_maximum_plate_size_z`.
"""
plate = self.plate_holder.resource if self.plate_holder is not None else None
if plate is None:
return
height = plate.get_size_z()
lid = plate.lid if isinstance(plate, Plate) else None
if lid is not None:
height = max(height, plate.get_lid_location(lid).z + lid.get_size_z())
if height > self._maximum_plate_size_z:
with_lid = " with its lid" if lid is not None else ""
raise ValueError(
f"cannot close the drawer: {plate.name} is {height:g} mm high{with_lid}; the drawer takes"
f" at most {self._maximum_plate_size_z:g} mm"
)

async def close(self) -> None:
"""Move the drawer in (about 8 s)."""
"""Move the drawer in (about 8 s).

Raises:
ValueError: if the plate on the plate holder, with its lid, is above
`_maximum_plate_size_z`; nothing is sent.
"""
self._check_plate_size_z()
await self._move(open_drawer=False)
logger.info("drawer closed")
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
make_driver,
)
from pylabrobot.resources import Coordinate, PlateHolder
from pylabrobot.resources.corning.plates import cor_96_wellplate_2mL_Vb, cor_96_wellplate_360uL_Fb

# Frames this driver exchanged with a CLARIOstar Plus (FTDI, firmware 1.35) on its first runs.
OPEN_DRAWER_FRAME = bytes.fromhex("02000e0c0301000000000000200d")
Expand Down Expand Up @@ -72,6 +73,30 @@ async def test_close_updates_in(self):
)
self.assertEqual(self.plate_holder.location, LOCATION_IN)

async def test_close_with_a_plate_too_tall_sends_nothing(self):
self.plate_holder.assign_child_resource(cor_96_wellplate_2mL_Vb("deep_well")) # 43.5 mm
with self.assertRaises(ValueError):
await self.driver.drawer.close()
self.assertEqual(self.io.written, [])

async def test_close_with_a_plate_at_the_limit_closes(self):
plate = cor_96_wellplate_360uL_Fb("plate")
self.plate_holder.assign_child_resource(plate)
self.driver.drawer._maximum_plate_size_z = plate.get_size_z()
self.io.replies = [STATUS_DRAWER_OPEN, CLOSE_DRAWER_REPLY, STATUS_IDLE, STATUS_IDLE]
await self.driver.drawer.close()
self.assertEqual(
self.io.written, [STATUS_QUERY_FRAME, CLOSE_DRAWER_FRAME] + [STATUS_QUERY_FRAME] * 2
)

async def test_a_lid_counts_towards_the_height(self):
plate = cor_96_wellplate_360uL_Fb("lidded", with_lid=True)
self.plate_holder.assign_child_resource(plate)
self.driver.drawer._maximum_plate_size_z = plate.get_size_z() # the lid stands above it
with self.assertRaisesRegex(ValueError, "with its lid"):
await self.driver.drawer.close()
self.assertEqual(self.io.written, [])

async def test_idle_but_not_open_times_out(self):
self.driver.drawer.default_move_timeout = 0
self.io.replies = [STATUS_IDLE, OPEN_DRAWER_REPLY, STATUS_IDLE, STATUS_IDLE]
Expand Down
2 changes: 2 additions & 0 deletions pylabrobot/bmg_labtech/clariostar/driver/master.py
Original file line number Diff line number Diff line change
Expand Up @@ -1219,6 +1219,8 @@ async def stop(self, accept_plate_left_in_device: bool = False) -> None:
Raises:
RuntimeError: if a plate is still in the reader, unless `accept_plate_left_in_device`; the
drawer is opened and the connection stays open.
ValueError: if the plate on the plate holder is too tall to close the drawer with; the
drawer stays open and the connection stays open.
"""
await self._shut_down(accept_plate_left_in_device)
self._stop_queued_reads() # before the device handle under them is closed
Expand Down
11 changes: 11 additions & 0 deletions pylabrobot/bmg_labtech/clariostar/driver/simulator_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
)
from pylabrobot.resources import Coordinate, Plate
from pylabrobot.resources.biorad.plates import biorad_96_wellplate_200uL_Vb
from pylabrobot.resources.corning.plates import cor_96_wellplate_2mL_Vb

LOCATION_IN = Coordinate(10, 20, 30) # made up: the real ones are not measured yet
LOCATION_OUT = Coordinate(10, -200, 30)
Expand Down Expand Up @@ -133,6 +134,16 @@ async def test_stop_with_a_plate_inside_opens_the_drawer_and_raises(self):
self.assertTrue(await self.reader.drawer.sense_drawer_open())
await self.reader.stop(accept_plate_left_in_device=True)

async def test_stop_with_a_plate_too_tall_raises_and_leaves_the_drawer_open(self):
await self.reader.drawer.open()
tall = cor_96_wellplate_2mL_Vb("deep_well") # 43.5 mm
self.reader.plate_holder.assign_child_resource(tall)
with self.assertRaises(ValueError):
await self.reader.stop()
self.assertTrue(await self.reader.drawer.sense_drawer_open())
tall.unassign()
await self.reader.stop()

async def test_stop_switches_heating_off_and_closes_the_drawer(self):
temperature_controller = self.reader.temperature_controller
assert temperature_controller is not None
Expand Down
Loading