Skip to content

Port Hamilton Vantage to new Device/Driver/Backend architecture#984

Open
rickwierenga wants to merge 3 commits intov1b1from
port-vantage-v1b1-arch
Open

Port Hamilton Vantage to new Device/Driver/Backend architecture#984
rickwierenga wants to merge 3 commits intov1b1from
port-vantage-v1b1-arch

Conversation

@rickwierenga
Copy link
Copy Markdown
Member

Summary

  • Decomposes the legacy monolithic VantageBackend (~5,334 lines) into the same layered Device → Driver → CapabilityBackend architecture established by the STAR port
  • 14 new files under pylabrobot/hamilton/liquid_handlers/vantage/ totaling ~2,900 lines
  • All firmware commands verified parameter-by-parameter against the legacy implementation (16 review agents, 2 rounds)

New files

File Purpose
driver.py VantageDriver(HamiltonLiquidHandler) — USB I/O, firmware protocol, setup/teardown
pip_backend.py VantagePIPBackend(PIPBackend) — independent channel tip/aspirate/dispense
head96_backend.py VantageHead96Backend(Head96Backend) — 96-head operations
ipg.py IPGBackend(OrientableGripperArmBackend) — integrated plate gripper
vantage.py Vantage(Device) — user-facing device wiring capabilities to backends
chatterbox.py VantageChatterboxDriver — mock driver for testing without hardware
fw_parsing.py parse_vantage_fw_string() — dict-based firmware response parser
errors.py Error dicts, VantageFirmwareError, PLR error conversion
x_arm.py VantageXArm — X-arm positioning (A1XM)
loading_cover.py VantageLoadingCover — loading cover control (I1AM)

Test plan

  • 16 unit tests pass (fw_parsing + error handling)
  • Chatterbox smoke test: Vantage(deck, chatterbox=True) → setup → stop lifecycle works
  • Double-stop is safe (guarded by _setup_finished)
  • Hardware validation on physical Vantage

🤖 Generated with Claude Code

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>
@rickwierenga rickwierenga force-pushed the port-vantage-v1b1-arch branch from c6b9278 to 7bcbdbd Compare April 3, 2026 02:21
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.arms API and deprecated the legacy pylabrobot.arms import 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.

Comment on lines +169 to +181
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
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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)

Copilot uses AI. Check for mistakes.
Comment on lines +149 to +154
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,
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +163 to +167
async def open_gripper(
self, gripper_width: float, backend_params: Optional[BackendParams] = None
) -> None:
await self.driver.send_command(module="A1RM", command="DO")

Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
pass

async def is_gripper_closed(self, backend_params: Optional[BackendParams] = None) -> bool:
return not self._parked
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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."
)

Copilot uses AI. Check for mistakes.
Comment on lines +21 to +22
>>> parse_vantage_fw_string("id0xs30 -100 +1 1000", {"id": "int", "x": "[int]"})
{"id": 0, "x": [30, -100, 1, 1000]}
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
>>> 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]}

Copilot uses AI. Check for mistakes.
rickwierenga and others added 2 commits April 2, 2026 19:25
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants