Skip to content

Commit 9da34d1

Browse files
committed
address non-legacy VW error by resetting num_arms to main behavior
1 parent f5434a5 commit 9da34d1

2 files changed

Lines changed: 94 additions & 6 deletions

File tree

pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1441,9 +1441,7 @@ def core96_head_installed(self) -> bool:
14411441

14421442
@property
14431443
def num_arms(self) -> int:
1444-
has_iswap = self.extended_conf.left_x_drive.iswap_installed
1445-
has_core_grippers = self._deck is not None and self._deck.has_resource("core_grippers")
1446-
return 1 if (has_iswap or has_core_grippers) else 0
1444+
return 1 if self.extended_conf.left_x_drive.iswap_installed else 0
14471445

14481446
@property
14491447
def head96_installed(self) -> Optional[bool]:
@@ -1551,6 +1549,16 @@ async def _pip_channel_request_configuration(self, channel: int) -> PipChannelIn
15511549
pressure_adc="Analog_Devices_AD5263" if hw_tokens[3] == "1" else "Renesas_X9268",
15521550
)
15531551

1552+
def _is_unsupported_pip_channel_configuration_error(self, error: STARFirmwareError) -> bool:
1553+
"""Return whether a `VW` query failed because the firmware does not support that command."""
1554+
1555+
return (
1556+
len(error.errors) == 1
1557+
and "Pipetting channel 1" in error.errors
1558+
and isinstance(error.errors["Pipetting channel 1"], UnknownHamiltonError)
1559+
and error.errors["Pipetting channel 1"].message == "Unknown command"
1560+
)
1561+
15541562
def get_id_from_fw_response(self, resp: str) -> Optional[int]:
15551563
"""Get the id from a firmware response."""
15561564
parsed = parse_star_fw_string(resp, "id####")
@@ -1792,9 +1800,14 @@ async def set_up_pip():
17921800
self._channels_minimum_y_spacing = await self.channels_request_y_minimum_spacing()
17931801

17941802
# Cache per-channel hardware configuration for version-specific behavior
1795-
self._pip_channel_information = [
1796-
await self._pip_channel_request_configuration(ch) for ch in range(self.num_channels)
1797-
]
1803+
try:
1804+
self._pip_channel_information = [
1805+
await self._pip_channel_request_configuration(ch) for ch in range(self.num_channels)
1806+
]
1807+
except STARFirmwareError as error:
1808+
if not self._is_unsupported_pip_channel_configuration_error(error):
1809+
raise
1810+
self._pip_channel_information = []
17981811

17991812
async def set_up_autoload():
18001813
if self.machine_conf.auto_load_installed and not skip_autoload:

pylabrobot/liquid_handling/backends/hamilton/STAR_tests.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,81 @@ def test_valid_y_bounds_are_kept(self):
200200
self.assertEqual(star.extended_conf.pip_maximal_y_position, 606.5)
201201
self.assertEqual(star.extended_conf.left_arm_min_y_position, 6.0)
202202

203+
def test_num_arms_depends_only_on_iswap(self):
204+
star = STARBackend()
205+
star._extended_conf = replace(
206+
_DEFAULT_EXTENDED_CONFIGURATION,
207+
left_x_drive=replace(_DEFAULT_EXTENDED_CONFIGURATION.left_x_drive, iswap_installed=False),
208+
)
209+
star._deck = unittest.mock.Mock()
210+
star._deck.has_resource.return_value = True
211+
212+
self.assertEqual(star.num_arms, 0)
213+
214+
215+
class TestSTARSetup(unittest.IsolatedAsyncioTestCase):
216+
def _make_backend_for_setup(self) -> STARBackend:
217+
backend = STARBackend()
218+
backend.request_machine_configuration = unittest.mock.AsyncMock( # type: ignore
219+
return_value=_DEFAULT_MACHINE_CONFIGURATION
220+
)
221+
backend.request_extended_configuration = unittest.mock.AsyncMock( # type: ignore
222+
return_value=_DEFAULT_EXTENDED_CONFIGURATION
223+
)
224+
backend.request_instrument_initialization_status = unittest.mock.AsyncMock(return_value=True)
225+
backend.move_all_channels_in_z_safety = unittest.mock.AsyncMock()
226+
backend.move_core_96_to_safe_position = unittest.mock.AsyncMock()
227+
backend.request_tip_presence = unittest.mock.AsyncMock(return_value=[False] * 8)
228+
backend.request_firmware_version = unittest.mock.AsyncMock(return_value="C0RFid0001rf2024.01.01")
229+
backend.channels_request_y_minimum_spacing = unittest.mock.AsyncMock(return_value=[9.0] * 8)
230+
return backend
231+
232+
def _firmware_error(self, response: str) -> STARFirmwareError:
233+
backend = STARBackend()
234+
with self.assertRaises(STARFirmwareError) as ctx:
235+
backend.check_fw_string_error(response)
236+
return ctx.exception
237+
238+
async def test_setup_ignores_unsupported_pip_channel_configuration_query(self):
239+
backend = self._make_backend_for_setup()
240+
backend._pip_channel_request_configuration = unittest.mock.AsyncMock( # type: ignore
241+
side_effect=self._firmware_error("P1VWid1111er30")
242+
)
243+
244+
with unittest.mock.patch(
245+
"pylabrobot.liquid_handling.backends.hamilton.STAR_backend.HamiltonLiquidHandler.setup",
246+
new=unittest.mock.AsyncMock(),
247+
):
248+
await backend.setup(skip_autoload=True, skip_iswap=True, skip_core96_head=True)
249+
250+
self.assertEqual(backend._pip_channel_information, [])
251+
252+
async def test_setup_reraises_other_pip_channel_configuration_firmware_errors(self):
253+
backend = self._make_backend_for_setup()
254+
backend._pip_channel_request_configuration = unittest.mock.AsyncMock( # type: ignore
255+
side_effect=self._firmware_error("P1VWid1111er31")
256+
)
257+
258+
with unittest.mock.patch(
259+
"pylabrobot.liquid_handling.backends.hamilton.STAR_backend.HamiltonLiquidHandler.setup",
260+
new=unittest.mock.AsyncMock(),
261+
):
262+
with self.assertRaises(STARFirmwareError):
263+
await backend.setup(skip_autoload=True, skip_iswap=True, skip_core96_head=True)
264+
265+
async def test_setup_reraises_malformed_pip_channel_configuration_responses(self):
266+
backend = self._make_backend_for_setup()
267+
backend._pip_channel_request_configuration = unittest.mock.AsyncMock( # type: ignore
268+
side_effect=ValueError("bad VW response")
269+
)
270+
271+
with unittest.mock.patch(
272+
"pylabrobot.liquid_handling.backends.hamilton.STAR_backend.HamiltonLiquidHandler.setup",
273+
new=unittest.mock.AsyncMock(),
274+
):
275+
with self.assertRaises(ValueError):
276+
await backend.setup(skip_autoload=True, skip_iswap=True, skip_core96_head=True)
277+
203278

204279
class STARCommandCatcher(STARBackend):
205280
"""Mock backend for star that catches commands and saves them instead of sending them to the

0 commit comments

Comments
 (0)