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
106 changes: 106 additions & 0 deletions pylabrobot/hamilton/star/driver/features/pipettes.py
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,7 @@ class LLDMode(enum.Enum):
CAPACITIVE = 1
PRESSURE = 2
DUAL = 3
ZTOUCH = 4

class PressureLLDMode(enum.Enum):
"""What a pressure search stops at: the liquid, or the foam and then the liquid under it."""
Expand Down Expand Up @@ -4488,3 +4489,108 @@ async def read_tadm_curve(self, channel: int, points_per_read: int = 50) -> Opti
break
pressures.extend(values)
return TADMCurve(measurement_id, operation, had_error, pressures)

# ----------------------------------------
# Liquid handling
# ----------------------------------------

# -- aspirating ---------------------------------------------------------------------------------

async def _unchecked_fw_aspirate(
self,
tip_pattern: List[bool],
aspiration_type: List[int],
x_positions: List[int],
y_positions: List[int],
minimum_traverse_height_start: int,
minimum_z_end_position: int,
lld_search_height: List[int],
clot_detection_height: List[int],
liquid_surface_no_lld: List[int],
pull_out_distance_transport_air: List[int],
second_section_height: List[int],
second_section_ratio: List[int],
minimum_height: List[int],
immersion_depth: List[int],
immersion_depth_direction: List[int],
surface_following_distance: List[int],
aspiration_volumes: List[int],
aspiration_speed: List[int],
transport_air_volume: List[int],
blow_out_air_volume: List[int],
pre_wetting_volume: List[int],
lld_mode: List[int],
clld_sensitivity: List[int],
plld_sensitivity: List[int],
aspirate_position_above_z_touch_off: List[int],
detection_height_difference_for_dual_lld: List[int],
swap_speed: List[int],
settling_time: List[int],
mix_volume: List[int],
mix_cycles: List[int],
mix_position_from_liquid_surface: List[int],
mix_speed: List[int],
mix_surface_following_distance: List[int],
limit_curve_index: List[int],
tadm_algorithm: bool,
recording_mode: int,
use_2nd_section_aspiration: List[bool],
retract_height_over_2nd_section_to_empty_tip: List[int],
dispensation_speed_during_emptying_tip: List[int],
dosing_drive_speed_during_2nd_section_search: List[int],
z_drive_speed_during_2nd_section_search: List[int],
cup_upper_edge: List[int],
read_timeout: int = 300,
):
"""Send the aspiration as it is given: heights and distances in tenths of a millimetre,
volumes in tenths of a microlitre, speeds in tenths per second, times in tenths of a second,
one value per channel of the pattern. `C0 AS`."""
return await self._driver.send_command(
module="C0",
command="AS",
subsystem=_FirmwareLock.CHANNELS,
tip_pattern=tip_pattern,
read_timeout=read_timeout,
at=[f"{at:01}" for at in aspiration_type],
tm=tip_pattern,
xp=[f"{xp:05}" for xp in x_positions],
yp=[f"{yp:04}" for yp in y_positions],
th=f"{minimum_traverse_height_start:04}",
te=f"{minimum_z_end_position:04}",
lp=[f"{lp:04}" for lp in lld_search_height],
ch=[f"{ch:03}" for ch in clot_detection_height],
zl=[f"{zl:04}" for zl in liquid_surface_no_lld],
po=[f"{po:04}" for po in pull_out_distance_transport_air],
zu=[f"{zu:04}" for zu in second_section_height],
zr=[f"{zr:05}" for zr in second_section_ratio],
zx=[f"{zx:04}" for zx in minimum_height],
ip=[f"{ip:04}" for ip in immersion_depth],
it=[f"{it}" for it in immersion_depth_direction],
fp=[f"{fp:04}" for fp in surface_following_distance],
av=[f"{av:05}" for av in aspiration_volumes],
as_=[f"{as_:04}" for as_ in aspiration_speed],
ta=[f"{ta:03}" for ta in transport_air_volume],
ba=[f"{ba:04}" for ba in blow_out_air_volume],
oa=[f"{oa:03}" for oa in pre_wetting_volume],
lm=[f"{lm}" for lm in lld_mode],
ll=[f"{ll}" for ll in clld_sensitivity],
lv=[f"{lv}" for lv in plld_sensitivity],
zo=[f"{zo:03}" for zo in aspirate_position_above_z_touch_off],
ld=[f"{ld:02}" for ld in detection_height_difference_for_dual_lld],
de=[f"{de:04}" for de in swap_speed],
wt=[f"{wt:02}" for wt in settling_time],
mv=[f"{mv:05}" for mv in mix_volume],
mc=[f"{mc:02}" for mc in mix_cycles],
mp=[f"{mp:03}" for mp in mix_position_from_liquid_surface],
ms=[f"{ms:04}" for ms in mix_speed],
mh=[f"{mh:04}" for mh in mix_surface_following_distance],
gi=[f"{gi:03}" for gi in limit_curve_index],
gj=tadm_algorithm,
gk=recording_mode,
lk=[1 if lk else 0 for lk in use_2nd_section_aspiration],
ik=[f"{ik:04}" for ik in retract_height_over_2nd_section_to_empty_tip],
sd=[f"{sd:04}" for sd in dispensation_speed_during_emptying_tip],
se=[f"{se:04}" for se in dosing_drive_speed_during_2nd_section_search],
sz=[f"{sz:04}" for sz in z_drive_speed_during_2nd_section_search],
io=[f"{io:04}" for io in cup_upper_edge],
)
75 changes: 75 additions & 0 deletions pylabrobot/hamilton/star/driver/features/pipettes_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -2662,6 +2662,81 @@ async def test_read_tadm_curve_from_an_empty_fifo(self):
self.assertIsNone(await self.pipettes.read_tadm_curve(7))


class TestAspirateFirmware(unittest.IsolatedAsyncioTestCase):
"""`C0 AS` as legacy sends it: every field in the same order, at the same width."""

async def asyncSetUp(self):
self.pipettes = await simulated_channels()
self.sent: List[str] = []

async def recorded(module: str, command: str, **kwargs: Any):
for swallowed in ("fmt", "read_timeout", "subsystem", "tip_pattern"):
kwargs.pop(swallowed, None)
self.sent.append(assemble_command(module=module, command=command, id_=None, **kwargs))

self.pipettes._driver.send_command = recorded # type: ignore[assignment]

async def test_the_raw_aspirate_is_legacy_aspirate_pip_to_the_character(self):
n = 2
await self.pipettes._unchecked_fw_aspirate(
tip_pattern=[True, True],
aspiration_type=[0] * n,
x_positions=[1234] * n,
y_positions=[2160, 1980],
minimum_traverse_height_start=2450,
minimum_z_end_position=2450,
lld_search_height=[1500] * n,
clot_detection_height=[60] * n,
liquid_surface_no_lld=[1200] * n,
pull_out_distance_transport_air=[100] * n,
second_section_height=[32] * n,
second_section_ratio=[6180] * n,
minimum_height=[1000] * n,
immersion_depth=[0] * n,
immersion_depth_direction=[0] * n,
surface_following_distance=[0] * n,
aspiration_volumes=[1000] * n,
aspiration_speed=[1000] * n,
transport_air_volume=[0] * n,
blow_out_air_volume=[0] * n,
pre_wetting_volume=[0] * n,
lld_mode=[0] * n,
clld_sensitivity=[1] * n,
plld_sensitivity=[1] * n,
aspirate_position_above_z_touch_off=[0] * n,
detection_height_difference_for_dual_lld=[0] * n,
swap_speed=[1000] * n,
settling_time=[0] * n,
mix_volume=[0] * n,
mix_cycles=[0] * n,
mix_position_from_liquid_surface=[0] * n,
mix_speed=[1000] * n,
mix_surface_following_distance=[0] * n,
limit_curve_index=[0] * n,
tadm_algorithm=False,
recording_mode=0,
use_2nd_section_aspiration=[False] * n,
retract_height_over_2nd_section_to_empty_tip=[60] * n,
dispensation_speed_during_emptying_tip=[500] * n,
dosing_drive_speed_during_2nd_section_search=[500] * n,
z_drive_speed_during_2nd_section_search=[300] * n,
cup_upper_edge=[0] * n,
)
self.assertEqual(
self.sent,
[
"C0ASat0 0tmTrue Truexp01234 01234yp2160 1980th2450te2450lp1500 1500ch060 060zl1200 1200"
"po0100 0100zu0032 0032zr06180 06180zx1000 1000ip0000 0000it0 0fp0000 0000av01000 01000"
"as1000 1000ta000 000ba0000 0000oa000 000lm0 0ll1 1lv1 1zo000 000ld00 00de1000 1000wt00 00"
"mv00000 00000mc00 00mp000 000ms1000 1000mh0000 0000gi000 000gj0gk0lk0 0ik0060 0060"
"sd0500 0500se0500 0500sz0300 0300io0000 0000"
],
)

def test_z_touch_is_the_firmwares_fourth_lld_mode(self):
self.assertEqual(Pipettes.LLDMode.ZTOUCH.value, 4)


class TestLiquidClassLookup(unittest.TestCase):
"""Every Hamilton tip finds its class, the ones without a filter included."""

Expand Down
Loading