diff --git a/pylabrobot/hamilton/star/driver/features/pipettes.py b/pylabrobot/hamilton/star/driver/features/pipettes.py index 5e567880310..0bfcb4537c9 100644 --- a/pylabrobot/hamilton/star/driver/features/pipettes.py +++ b/pylabrobot/hamilton/star/driver/features/pipettes.py @@ -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.""" @@ -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], + ) diff --git a/pylabrobot/hamilton/star/driver/features/pipettes_tests.py b/pylabrobot/hamilton/star/driver/features/pipettes_tests.py index ac57b546305..14d938db3c4 100644 --- a/pylabrobot/hamilton/star/driver/features/pipettes_tests.py +++ b/pylabrobot/hamilton/star/driver/features/pipettes_tests.py @@ -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."""