Port Hamilton Vantage to new Device/Driver/Backend architecture#984
Port Hamilton Vantage to new Device/Driver/Backend architecture#984rickwierenga wants to merge 3 commits intov1b1from
Conversation
62108ee to
c6b9278
Compare
Decomposes the legacy monolithic VantageBackend (~5,334 lines) into the same layered architecture established by the STAR port: - VantageDriver(HamiltonLiquidHandler) — USB I/O, firmware protocol, setup - VantagePIPBackend(PIPBackend) — independent channel operations - VantageHead96Backend(Head96Backend) — 96-head operations - IPGBackend(OrientableGripperArmBackend) — plate gripper - VantageXArm, VantageLoadingCover — auxiliary subsystems - VantageChatterboxDriver — mock driver for testing - Vantage(Device) — user-facing device wiring capabilities All firmware commands verified parameter-by-parameter against the legacy. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
c6b9278 to
7bcbdbd
Compare
There was a problem hiding this comment.
Pull request overview
Ports Hamilton Vantage support to the newer layered Device → Driver → CapabilityBackend architecture (consistent with the STAR port), while also advancing broader repo-wide refactors around capabilities, deprecation shims, and documentation updates.
Changes:
- Added new Hamilton Vantage driver/device/backends plus unit tests for firmware parsing + error conversion.
- Introduced/expanded the new
pylabrobot.capabilities.armsAPI and deprecated the legacypylabrobot.armsimport surface (plus doc updates). - Added multiple deprecation shims for legacy top-level modules and improved serialization for some resources.
Reviewed changes
Copilot reviewed 13 out of 14 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| pylabrobot/tilting/init.py | Deprecation shim re-exporting legacy tilting. |
| pylabrobot/thermocycling/init.py | Deprecation shim re-exporting legacy thermocycling. |
| pylabrobot/temperature_controlling/init.py | Deprecation shim re-exporting legacy temperature controlling. |
| pylabrobot/storage/init.py | Deprecation shim re-exporting legacy storage. |
| pylabrobot/shaking/init.py | Deprecation shim re-exporting legacy shaking. |
| pylabrobot/sealing/init.py | Deprecation shim re-exporting legacy sealing. |
| pylabrobot/scales/init.py | Deprecation shim re-exporting legacy scales. |
| pylabrobot/resources/trough.py | Adds serialize() overrides for trough metadata. |
| pylabrobot/resources/plate.py | Adds serialize() override to include non-default plate type. |
| pylabrobot/pumps/init.py | Deprecation shim re-exporting legacy pumps. |
| pylabrobot/powder_dispensing/init.py | Deprecation shim re-exporting legacy powder dispensing. |
| pylabrobot/plate_washing/init.py | Deprecation shim re-exporting legacy plate washing. |
| pylabrobot/plate_reading/init.py | Deprecation shim re-exporting legacy plate reading. |
| pylabrobot/peeling/init.py | Deprecation shim re-exporting legacy peeling. |
| pylabrobot/only_fans/init.py | Deprecation shim re-exporting legacy “only_fans”. |
| pylabrobot/microscopy/init.py | Deprecation shim re-exporting legacy microscopes. |
| pylabrobot/machines/init.py | Deprecation shim re-exporting legacy machines. |
| pylabrobot/liquid_handling/standard.py | Deprecation shim for legacy liquid handling standard ops. |
| pylabrobot/liquid_handling/liquid_classes/tecan.py | Deprecation shim re-exporting legacy Tecan liquid classes. |
| pylabrobot/liquid_handling/liquid_classes/hamilton/star.py | Deprecation shim re-exporting legacy STAR liquid classes. |
| pylabrobot/liquid_handling/liquid_classes/hamilton/init.py | Deprecation shim re-exporting legacy Hamilton liquid classes. |
| pylabrobot/liquid_handling/liquid_classes/init.py | Deprecation shim re-exporting legacy liquid classes package. |
| pylabrobot/liquid_handling/backends/tecan/init.py | Deprecation shim re-exporting legacy Tecan backends. |
| pylabrobot/liquid_handling/backends/opentrons_backend.py | Deprecation shim re-exporting legacy Opentrons backend. |
| pylabrobot/liquid_handling/backends/hamilton/STAR_chatterbox.py | Deprecation shim re-exporting legacy STAR chatterbox backend. |
| pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py | Deprecation shim re-exporting legacy STAR backend. |
| pylabrobot/liquid_handling/backends/hamilton/init.py | Deprecation shim re-exporting legacy Hamilton backends. |
| pylabrobot/liquid_handling/backends/chatterbox.py | Deprecation shim re-exporting legacy chatterbox backend. |
| pylabrobot/liquid_handling/backends/init.py | Deprecation shim re-exporting legacy backends package. |
| pylabrobot/liquid_handling/init.py | Deprecation shim re-exporting legacy liquid_handling package. |
| pylabrobot/legacy/machines/machine.py | Makes legacy Machine.stop() idempotent if setup never completed. |
| pylabrobot/legacy/liquid_handling/liquid_classes/hamilton/vantage.py | Updates legacy liquid class import to new canonical location. |
| pylabrobot/legacy/liquid_handling/liquid_classes/hamilton/star.py | Updates legacy liquid class import to new canonical location. |
| pylabrobot/legacy/liquid_handling/liquid_classes/hamilton/init.py | Legacy shim updated to new canonical liquid class locations. |
| pylabrobot/legacy/liquid_handling/liquid_classes/init.py | Legacy shim updated to new canonical liquid class locations. |
| pylabrobot/legacy/liquid_handling/errors.py | Re-exports shared liquid-handling error classes from capabilities. |
| pylabrobot/legacy/liquid_handling/backends/hamilton/STAR_backend.py | Delegates certain Head96/iSWAP operations to new backend APIs + doc tweaks. |
| pylabrobot/io/usb.py | Makes USB stop() idempotent (no error when already disconnected). |
| pylabrobot/heating_shaking/init.py | Deprecation shim re-exporting legacy heating_shaking. |
| pylabrobot/hamilton/liquid_handlers/vantage/x_arm.py | New Vantage X-arm helper (firmware command wrapper). |
| pylabrobot/hamilton/liquid_handlers/vantage/vantage.py | New Vantage(Device) wiring PIP/Head96/IPG capabilities. |
| pylabrobot/hamilton/liquid_handlers/vantage/tests/test_fw_parsing.py | Unit tests for Vantage firmware string parsing. |
| pylabrobot/hamilton/liquid_handlers/vantage/tests/test_errors.py | Unit tests for Vantage firmware error parsing + conversion. |
| pylabrobot/hamilton/liquid_handlers/vantage/tests/init.py | Test package marker for Vantage tests. |
| pylabrobot/hamilton/liquid_handlers/vantage/loading_cover.py | New loading cover helper (firmware command wrapper). |
| pylabrobot/hamilton/liquid_handlers/vantage/ipg.py | New IPG arm backend implementing OrientableGripperArmBackend. |
| pylabrobot/hamilton/liquid_handlers/vantage/head96_backend.py | New Vantage Core96/Head96 backend mapping high-level ops to firmware. |
| pylabrobot/hamilton/liquid_handlers/vantage/fw_parsing.py | New Vantage firmware response parser. |
| pylabrobot/hamilton/liquid_handlers/vantage/errors.py | New Vantage firmware error dictionaries, parsing, and PLR conversion. |
| pylabrobot/hamilton/liquid_handlers/vantage/driver.py | New Vantage driver built atop HamiltonLiquidHandler with setup/stop flow. |
| pylabrobot/hamilton/liquid_handlers/vantage/chatterbox.py | New Vantage chatterbox driver for no-hardware development. |
| pylabrobot/hamilton/liquid_handlers/vantage/init.py | Exposes Vantage driver/device entry points. |
| pylabrobot/hamilton/liquid_handlers/star/star.py | Updates STAR device imports to new arms capability modules. |
| pylabrobot/hamilton/liquid_handlers/star/pip_backend.py | Updates STAR liquid class imports to new canonical module. |
| pylabrobot/hamilton/liquid_handlers/star/liquid_classes/star_classes.py | Updates STAR liquid class imports to new canonical HamiltonLiquidClass. |
| pylabrobot/hamilton/liquid_handlers/star/liquid_classes/init.py | New public entry point for STAR liquid classes. |
| pylabrobot/hamilton/liquid_handlers/star/iswap.py | Updates imports to new arms capability modules. |
| pylabrobot/hamilton/liquid_handlers/star/head96_backend.py | Adds lifecycle + utilities for STAR Head96 backend and refactors position logic. |
| pylabrobot/hamilton/liquid_handlers/star/fw_parsing.py | Adds firmware-version-to-date parsing helper. |
| pylabrobot/hamilton/liquid_handlers/star/driver.py | Clarifies subsystem lifecycle ownership (notably head96). |
| pylabrobot/hamilton/liquid_handlers/star/core.py | Updates imports to new arms capability modules. |
| pylabrobot/hamilton/liquid_handlers/liquid_class.py | Introduces canonical HamiltonLiquidClass implementation for new architecture. |
| pylabrobot/hamilton/liquid_classes/hamilton/init.py | Removes old liquid class re-export surface. |
| pylabrobot/hamilton/liquid_classes/init.py | Removes old liquid class re-export surface. |
| pylabrobot/hamilton/lh/vantage/liquid_classes.py | Updates Vantage liquid class import to new canonical HamiltonLiquidClass. |
| pylabrobot/hamilton/lh/vantage/init.py | Package marker for Vantage LH module. |
| pylabrobot/hamilton/lh/init.py | Package marker for pylabrobot.hamilton.lh. |
| pylabrobot/device.py | Makes Device.stop() idempotent if setup never completed. |
| pylabrobot/centrifuge/init.py | Deprecation shim re-exporting legacy centrifuge. |
| pylabrobot/capabilities/weighing/weighing.py | Adds capability-ready guard decorators to public methods. |
| pylabrobot/capabilities/tilting/tilting.py | Adds capability-ready guard decorators to public methods. |
| pylabrobot/capabilities/temperature_controlling/temperature_controller.py | Adds capability-ready guard decorators to public methods. |
| pylabrobot/capabilities/shaking/shaking.py | Adds capability-ready guard decorators to public methods. |
| pylabrobot/capabilities/sealing/sealing.py | Adds capability-ready guard decorators to public methods. |
| pylabrobot/capabilities/peeling/peeling.py | Adds capability-ready guard decorators to public methods. |
| pylabrobot/capabilities/humidity_controlling/humidity_controller.py | Adds capability-ready guard decorators to public methods. |
| pylabrobot/capabilities/fan_control/fan_control.py | Adds capability-ready guard decorators to public methods. |
| pylabrobot/capabilities/centrifuging/centrifuging.py | Adds capability-ready guard decorators to public methods. |
| pylabrobot/capabilities/barcode_scanning/barcode_scanning.py | Adds capability-ready guard decorators to public methods. |
| pylabrobot/capabilities/automated_retrieval/automated_retrieval.py | Adds capability-ready guard decorators to public methods. |
| pylabrobot/capabilities/arms/standard.py | Adds canonical arms “standard types” (directions, locations, move/drop structs). |
| pylabrobot/capabilities/arms/orientable_arm.py | New orientable arm capability frontend. |
| pylabrobot/capabilities/arms/backend.py | New arms backend interfaces/mixins for capability architecture. |
| pylabrobot/capabilities/arms/articulated_arm.py | New articulated arm capability frontend. |
| pylabrobot/capabilities/arms/arm.py | New canonical arm capability frontend (base + gripper arm). |
| pylabrobot/capabilities/arms/arm_tests.py | New unit tests validating arm coordinate computations. |
| pylabrobot/capabilities/arms/architecture.md | New internal architecture documentation for arms capability stack. |
| pylabrobot/capabilities/arms/init.py | Public export surface for new arms capability module. |
| pylabrobot/brooks/precise_flex.py | Updates imports to new arms capability modules. |
| pylabrobot/barcode_scanners/init.py | Deprecation shim re-exporting legacy barcode scanners. |
| pylabrobot/arms/standard.py | Deprecation shim re-exporting new arms standard types. |
| pylabrobot/arms/orientable_arm.py | Deprecation shim re-exporting new orientable arm. |
| pylabrobot/arms/backend.py | Deprecation shim re-exporting new arms backend interfaces. |
| pylabrobot/arms/articulated_arm.py | Deprecation shim re-exporting new articulated arm. |
| pylabrobot/arms/arm.py | Deprecation shim re-exporting new arm frontend implementation. |
| pylabrobot/arms/init.py | Deprecation shim re-exporting new pylabrobot.capabilities.arms. |
| docs/user_guide/machine-agnostic-features/writing-robot-agnostic-protocols.md | Updates strictness docs to legacy module paths. |
| docs/user_guide/hamilton/star/iswap.ipynb | Updates iSWAP user guide references to new arms module paths. |
| docs/user_guide/hamilton/star/core-grippers.ipynb | Updates core grippers user guide references to new arms module paths. |
| docs/user_guide/capabilities/dispensing/index.md | Fixes relative links in dispensing docs. |
| docs/user_guide/capabilities/arms.md | Updates arms docs to new capability module paths. |
| docs/user_guide/brooks/precise_flex/hello-world.ipynb | Updates PreciseFlex docs to new arms module paths. |
| docs/user_guide/01_material-handling/temperature-controllers/temperature-controllers.rst | Updates temperature controller docs to legacy module paths. |
| docs/resources/introduction.md | Updates docs to legacy machine paths for examples. |
| docs/resources/index.md | Fixes/updates resource documentation links. |
| docs/resources/container/container.rst | Updates LiquidHandler reference to legacy module path. |
| docs/contributor_guide/visualizer.md | Updates ChatterboxBackend reference to legacy module path. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| core96_initialized = await self.core96_request_initialization_status() | ||
| if not core96_initialized and not skip_core96: | ||
| self.head96 = VantageHead96Backend(self) | ||
| await self.core96_initialize( | ||
| x_position=7347, | ||
| y_position=2684, | ||
| minimal_traverse_height_at_begin_of_command=int(self._traversal_height * 10), | ||
| minimal_height_at_command_end=int(self._traversal_height * 10), | ||
| end_z_deposit_position=2420, | ||
| ) | ||
| else: | ||
| # Even if already initialized, create the backend. | ||
| self.head96 = VantageHead96Backend(self) if not skip_core96 else None |
There was a problem hiding this comment.
setup(skip_core96=True) still calls core96_request_initialization_status() unconditionally. If the 96-head module is not installed (a common reason to pass skip_core96=True), this status query may itself raise a firmware error, defeating the purpose of the skip flag. Consider guarding the status query behind if not skip_core96: (and leaving self.head96 = None), or catching the specific "module not available" firmware error and treating it as not installed when skip_core96=True.
| core96_initialized = await self.core96_request_initialization_status() | |
| if not core96_initialized and not skip_core96: | |
| self.head96 = VantageHead96Backend(self) | |
| await self.core96_initialize( | |
| x_position=7347, | |
| y_position=2684, | |
| minimal_traverse_height_at_begin_of_command=int(self._traversal_height * 10), | |
| minimal_height_at_command_end=int(self._traversal_height * 10), | |
| end_z_deposit_position=2420, | |
| ) | |
| else: | |
| # Even if already initialized, create the backend. | |
| self.head96 = VantageHead96Backend(self) if not skip_core96 else None | |
| if skip_core96: | |
| self.head96 = None | |
| else: | |
| core96_initialized = await self.core96_request_initialization_status() | |
| if not core96_initialized: | |
| self.head96 = VantageHead96Backend(self) | |
| await self.core96_initialize( | |
| x_position=7347, | |
| y_position=2684, | |
| minimal_traverse_height_at_begin_of_command=int(self._traversal_height * 10), | |
| minimal_height_at_command_end=int(self._traversal_height * 10), | |
| end_z_deposit_position=2420, | |
| ) | |
| else: | |
| # Even if already initialized, create the backend. | |
| self.head96 = VantageHead96Backend(self) |
| pip_channels_initialized = await self.pip_request_initialization_status() | ||
| if not pip_channels_initialized or any(tip_presences): | ||
| await self.pip_initialize( | ||
| x_position=[7095] * self.num_channels, | ||
| y_position=[3891, 3623, 3355, 3087, 2819, 2551, 2283, 2016], | ||
| begin_z_deposit_position=[int(self._traversal_height * 10)] * self.num_channels, |
There was a problem hiding this comment.
pip_initialize() is passed y_position as a hard-coded 8-element list while other arguments are sized to self.num_channels. If query_tip_presence() ever returns a channel count != 8, this will produce inconsistent parameter lengths and likely a malformed firmware command. Either assert/validate that self.num_channels == 8 for Vantage PIP, or generate y_position dynamically based on the detected channel count.
| async def open_gripper( | ||
| self, gripper_width: float, backend_params: Optional[BackendParams] = None | ||
| ) -> None: | ||
| await self.driver.send_command(module="A1RM", command="DO") | ||
|
|
There was a problem hiding this comment.
open_gripper() ignores the gripper_width argument and always sends a fixed A1RM:DO command. If IPG only supports fully-open, this should be documented and the parameter should be validated/removed at the interface layer (or clamped) to avoid giving callers a false sense of control.
| pass | ||
|
|
||
| async def is_gripper_closed(self, backend_params: Optional[BackendParams] = None) -> bool: | ||
| return not self._parked |
There was a problem hiding this comment.
is_gripper_closed() returns not self._parked, but _parked is tracking parking state (set in park() / get_parking_status()), not actual gripper closure or whether a plate is held. This makes the method report incorrect values (e.g., open_gripper() doesn't update _parked, so the result won't change). Consider implementing a firmware query for hold/closed state (similar to STAR iSWAP C0:QP), or raising NotImplementedError until a reliable signal is available.
| return not self._parked | |
| raise NotImplementedError( | |
| "is_gripper_closed is not implemented for the Vantage IPG because the current " | |
| "backend does not have a reliable firmware query for gripper closed/held state. " | |
| "Parking status cannot be used as a proxy for gripper closure." | |
| ) |
| >>> parse_vantage_fw_string("id0xs30 -100 +1 1000", {"id": "int", "x": "[int]"}) | ||
| {"id": 0, "x": [30, -100, 1, 1000]} |
There was a problem hiding this comment.
The docstring example uses mismatched keys ({"id": "int", "x": "[int]"}) but the example string contains xs.... As written, the example would not work and may confuse users. Update the example to use consistent parameter names (e.g. {"xs": "[int]"}) and expected output keys.
| >>> parse_vantage_fw_string("id0xs30 -100 +1 1000", {"id": "int", "x": "[int]"}) | |
| {"id": 0, "x": [30, -100, 1, 1000]} | |
| >>> parse_vantage_fw_string("id0xs30 -100 +1 1000", {"id": "int", "xs": "[int]"}) | |
| {"id": 0, "xs": [30, -100, 1, 1000]} |
Follows the same pattern as STARBackend: the legacy class creates a VantageDriver in __init__, delegates send_command/setup/stop to it, and exposes typed property accessors for the new subsystems. All 21 legacy tests pass unchanged. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace ~275 lines of inline definitions (error dicts, VantageFirmwareError, parse_vantage_fw_string, vantage_response_string_to_error) with re-exports from the new vantage modules. Existing imports continue to work. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Summary
VantageBackend(~5,334 lines) into the same layered Device → Driver → CapabilityBackend architecture established by the STAR portpylabrobot/hamilton/liquid_handlers/vantage/totaling ~2,900 linesNew files
driver.pyVantageDriver(HamiltonLiquidHandler)— USB I/O, firmware protocol, setup/teardownpip_backend.pyVantagePIPBackend(PIPBackend)— independent channel tip/aspirate/dispensehead96_backend.pyVantageHead96Backend(Head96Backend)— 96-head operationsipg.pyIPGBackend(OrientableGripperArmBackend)— integrated plate grippervantage.pyVantage(Device)— user-facing device wiring capabilities to backendschatterbox.pyVantageChatterboxDriver— mock driver for testing without hardwarefw_parsing.pyparse_vantage_fw_string()— dict-based firmware response parsererrors.pyVantageFirmwareError, PLR error conversionx_arm.pyVantageXArm— X-arm positioning (A1XM)loading_cover.pyVantageLoadingCover— loading cover control (I1AM)Test plan
Vantage(deck, chatterbox=True)→ setup → stop lifecycle works_setup_finished)🤖 Generated with Claude Code