Port Vantage to new Device/Driver/CapabilityBackend architecture#976
Port Vantage to new Device/Driver/CapabilityBackend architecture#976rickwierenga wants to merge 4 commits intov1b1from
Conversation
Split the monolithic VantageBackend (5334 lines) into the new PLR architecture, mirroring the STAR port: - VantageDriver: HamiltonLiquidHandler subclass with USB I/O, fw parsing, error handling, and device-level commands - VantagePIPBackend: PIPBackend impl with all ~35 PIP firmware commands - VantageHead96Backend: Head96Backend impl with all Core96 commands - VantageIPG: OrientableGripperArmBackend impl for the plate gripper - VantageXArm, VantageLoadingCover: plain subsystems - Vantage: Device frontend wiring capabilities to driver backends - VantageChatterboxDriver: test driver that prints commands Also adds a generic LED control capability (LEDBackend/LEDControlCapability) with VantageLEDBackend encoding C0AM/LI commands. UV intensity and blink interval are exposed as VantageLEDParams (BackendParams). The legacy VantageBackend now delegates pick_up_tips, drop_tips, aspirate, and dispense to the new VantagePIPBackend. All 21 legacy tests pass through the forwarding chain. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Ports the Hamilton Vantage implementation to the newer Device/Driver/CapabilityBackend architecture (mirroring the STAR port), while keeping legacy VantageBackend functional via forwarding to the new PIP backend.
Changes:
- Introduces a new
pylabrobot/hamilton/liquid_handlers/vantage/module withVantageDriver,Vantagedevice wiring, and new PIP/Head96/IPG/LED backends. - Adds a generic
LEDControlCapabilityplus a Vantage-specific LED backend/params to encode C0AM/LI firmware commands. - Updates legacy Vantage backend/tests to instantiate new-architecture backends and forward core pipetting ops through
VantagePIPBackend.
Reviewed changes
Copilot reviewed 14 out of 16 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| pylabrobot/legacy/liquid_handling/backends/hamilton/vantage_tests.py | Updates test harness setup to create new-architecture backends so legacy forwarding works. |
| pylabrobot/legacy/liquid_handling/backends/hamilton/vantage_backend.py | Adds new-backend fields + setup initialization and forwards pick/drop/aspirate/dispense to the new PIP backend. |
| pylabrobot/hamilton/liquid_handlers/vantage/driver.py | New Vantage driver with firmware parsing + error conversion and backend/subsystem instantiation. |
| pylabrobot/hamilton/liquid_handlers/vantage/pip_backend.py | New PIP backend translating PIP ops into Vantage firmware commands. |
| pylabrobot/hamilton/liquid_handlers/vantage/head96_backend.py | New Head96 backend translating 96-head ops into firmware commands. |
| pylabrobot/hamilton/liquid_handlers/vantage/ipg.py | New IPG (iswap) backend implementing OrientableGripperArmBackend over A1RM commands. |
| pylabrobot/hamilton/liquid_handlers/vantage/led_backend.py | New LED backend + Vantage-specific params for C0AM/LI control. |
| pylabrobot/hamilton/liquid_handlers/vantage/loading_cover.py | New helper wrapper for loading-cover firmware commands. |
| pylabrobot/hamilton/liquid_handlers/vantage/x_arm.py | New helper wrapper for X-arm firmware commands. |
| pylabrobot/hamilton/liquid_handlers/vantage/chatterbox.py | Adds a chatterbox driver that prints commands rather than using USB I/O. |
| pylabrobot/hamilton/liquid_handlers/vantage/vantage.py | New Vantage device wiring driver backends to capabilities. |
| pylabrobot/hamilton/liquid_handlers/vantage/init.py | Exposes new Vantage public API symbols. |
| pylabrobot/hamilton/liquid_handlers/vantage/tests/init.py | New test package placeholder. |
| pylabrobot/capabilities/led_control/backend.py | Introduces the abstract LED backend interface. |
| pylabrobot/capabilities/led_control/led_control.py | Introduces LEDControlCapability convenience API. |
| pylabrobot/capabilities/led_control/init.py | Exports LED capability/backend symbols. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| elif not len(tadm_channel_pattern) < 24: | ||
| raise ValueError( | ||
| f"tadm_channel_pattern must be of length 24, but is '{len(tadm_channel_pattern)}'" |
There was a problem hiding this comment.
tadm_channel_pattern validation is incorrect: the condition elif not len(tadm_channel_pattern) < 24 will reject any pattern length >= 24 (including the default 96) and the error message says length 24, while _tadm_channel_pattern_to_hex asserts length 96. This makes it impossible to pass a custom 96-channel pattern. Update the check to require exactly 96 booleans (and align the error message accordingly).
| elif not len(tadm_channel_pattern) < 24: | |
| raise ValueError( | |
| f"tadm_channel_pattern must be of length 24, but is '{len(tadm_channel_pattern)}'" | |
| elif len(tadm_channel_pattern) != 96: | |
| raise ValueError( | |
| f"tadm_channel_pattern must be of length 96, but is '{len(tadm_channel_pattern)}'" |
| elif not len(tadm_channel_pattern) < 24: | ||
| raise ValueError( | ||
| f"tadm_channel_pattern must be of length 24, but is '{len(tadm_channel_pattern)}'" |
There was a problem hiding this comment.
Same issue as above: tadm_channel_pattern validation uses not len(...) < 24 and an error message claiming length 24, but the encoder expects 96. This will incorrectly raise for user-supplied 96-length patterns. Validate against length 96 instead.
| elif not len(tadm_channel_pattern) < 24: | |
| raise ValueError( | |
| f"tadm_channel_pattern must be of length 24, but is '{len(tadm_channel_pattern)}'" | |
| elif len(tadm_channel_pattern) != 96: | |
| raise ValueError( | |
| f"tadm_channel_pattern must be of length 96, but is '{len(tadm_channel_pattern)}'" |
| elif not len(tadm_channel_pattern) < 24: | ||
| raise ValueError( | ||
| f"tadm_channel_pattern must be of length 24, but is '{len(tadm_channel_pattern)}'" |
There was a problem hiding this comment.
Same tadm_channel_pattern length check bug here: the code rejects any pattern length >= 24 and the error message says 24, but _tadm_channel_pattern_to_hex requires 96. Require exactly 96 booleans to avoid runtime assertion failures / unusable API.
| elif not len(tadm_channel_pattern) < 24: | |
| raise ValueError( | |
| f"tadm_channel_pattern must be of length 24, but is '{len(tadm_channel_pattern)}'" | |
| elif len(tadm_channel_pattern) != 96: | |
| raise ValueError( | |
| f"tadm_channel_pattern must be of length 96, but is '{len(tadm_channel_pattern)}'" |
| backend_params.mix_position_in_z_direction_from_liquid_surface * 100 | ||
| ), | ||
| surface_following_distance_during_mixing=round( | ||
| backend_params.surface_following_distance_during_mixing * 100 |
There was a problem hiding this comment.
Unit conversion looks inconsistent here: mix_position_in_z_direction_from_liquid_surface and surface_following_distance_during_mixing are documented as [0.1mm] in the raw firmware method, but you multiply the mm values by 100 (10x too large). This likely should be * 10 (matching the other mm→0.1mm conversions in this call).
| backend_params.mix_position_in_z_direction_from_liquid_surface * 100 | |
| ), | |
| surface_following_distance_during_mixing=round( | |
| backend_params.surface_following_distance_during_mixing * 100 | |
| backend_params.mix_position_in_z_direction_from_liquid_surface * 10 | |
| ), | |
| surface_following_distance_during_mixing=round( | |
| backend_params.surface_following_distance_during_mixing * 10 |
| await self._driver.send_command( | ||
| module="C0AM", | ||
| command="LI", | ||
| li={"on": 1, "off": 0, "blink": 2}[mode], |
There was a problem hiding this comment.
mode is used as a dict key here, so an invalid value will raise a KeyError rather than a clear ValueError. Consider validating mode explicitly ("on"/"off"/"blink") and raising a helpful exception before indexing.
| await self._driver.send_command( | |
| module="C0AM", | |
| command="LI", | |
| li={"on": 1, "off": 0, "blink": 2}[mode], | |
| mode_to_li = {"on": 1, "off": 0, "blink": 2} | |
| try: | |
| li_value = mode_to_li[mode] | |
| except KeyError: | |
| raise ValueError("Invalid mode {!r}. Expected 'on', 'off', or 'blink'.".format(mode)) from None | |
| await self._driver.send_command( | |
| module="C0AM", | |
| command="LI", | |
| li=li_value, |
| """Dangerous easter egg.""" | ||
| sure = input( | ||
| "Are you sure you want to play Russian Roulette? This will turn on the uv-light " | ||
| "with a probability of 1/6. (yes/no) " | ||
| ) | ||
| if sure.lower() != "yes": | ||
| print("boring") | ||
| return | ||
|
|
||
| if random.randint(1, 6) == 6: | ||
| await self.led.set_color( | ||
| "on", intensity=100, white=100, red=100, green=0, blue=0, | ||
| backend_params=VantageLEDParams(uv=100), | ||
| ) | ||
| print("You lost.") | ||
| else: | ||
| await self.led.set_color("on", intensity=100, white=100, red=0, green=100, blue=0) | ||
| print("You won.") | ||
|
|
||
| await asyncio.sleep(5) | ||
| await self.led.set_color("on", intensity=100, white=100, red=100, green=100, blue=100) |
There was a problem hiding this comment.
This async method uses blocking input() and print() calls and is an intentionally hazardous “easter egg” that can enable UV output. This is problematic for a library API (blocks event loop, unsafe side effects, and unprofessional public surface). Suggest moving it to an example/script outside the package or gating it behind an explicit, opt-in debug/test module.
| """Dangerous easter egg.""" | |
| sure = input( | |
| "Are you sure you want to play Russian Roulette? This will turn on the uv-light " | |
| "with a probability of 1/6. (yes/no) " | |
| ) | |
| if sure.lower() != "yes": | |
| print("boring") | |
| return | |
| if random.randint(1, 6) == 6: | |
| await self.led.set_color( | |
| "on", intensity=100, white=100, red=100, green=0, blue=0, | |
| backend_params=VantageLEDParams(uv=100), | |
| ) | |
| print("You lost.") | |
| else: | |
| await self.led.set_color("on", intensity=100, white=100, red=0, green=100, blue=0) | |
| print("You won.") | |
| await asyncio.sleep(5) | |
| await self.led.set_color("on", intensity=100, white=100, red=100, green=100, blue=100) | |
| """Deprecated hazardous easter egg. | |
| This method previously performed blocking console I/O and could enable UV output | |
| on the device. It has been disabled to avoid unsafe side effects and blocking the | |
| asyncio event loop. If you need similar functionality for testing or demonstration | |
| purposes, implement it in an external script that explicitly opts in to such | |
| behavior. | |
| """ | |
| raise RuntimeError( | |
| "Vantage.russian_roulette has been disabled because it was hazardous and " | |
| "performed blocking I/O. Use an external, opt-in script if you need this " | |
| "behavior for testing or demos." | |
| ) |
| x_position: X Position [0.1mm]. | ||
| x_speed: X speed [0.1mm/s]. | ||
| TODO_XA_1: (0). | ||
| """ | ||
| if not -50000 <= x_position <= 50000: | ||
| raise ValueError("x_position must be in range -50000 to 50000") | ||
| if not 1 <= x_speed <= 25000: | ||
| raise ValueError("x_speed must be in range 1 to 25000") | ||
| if not 1 <= TODO_XA_1 <= 25000: | ||
| raise ValueError("TODO_XA_1 must be in range 1 to 25000") |
There was a problem hiding this comment.
The docstring indicates TODO_XA_1 is (0), but the default is 1 and validation forbids 0 (1 <= TODO_XA_1). Either allow 0 in the validation/default, or update the docstring so callers don't get surprising ValueErrors for the documented value.
…mports - pip_backend.py dispense() mix_speed: * 100 -> * 10 (matching aspirate and legacy) - pip_backend.py pick_up_tips/drop_tips/aspirate: fix list duplication when user provides explicit traversal heights (] * len was outside comprehension) - chatterbox.py: remove unused List, Optional imports Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… validation - Fix tadm_channel_pattern validation: check len != 96 (was broken `not len < 24`) - Fix aspirate96 mix_position and surface_following_distance scaling: * 100 -> * 10 (matching dispense96 and firmware docs [0.1mm]) - Add explicit mode validation in VantageLEDBackend before dict lookup Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Prevents TypeError crash when send_command returns None (e.g. chatterbox mode or communication timeout). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
475dbab to
588abbb
Compare
Summary
VantageBackendinto the new PLR architecture, mirroring the STAR portpylabrobot/hamilton/liquid_handlers/vantage/withVantageDriver,VantagePIPBackend,VantageHead96Backend,VantageIPG,VantageXArm,VantageLoadingCover,Vantagedevice, andVantageChatterboxDriverLEDControlCapability(pylabrobot/capabilities/led_control/) withVantageLEDBackendencoding C0AM/LI firmware commands; UV and blink interval exposed asVantageLEDParamsVantageBackendnow forwardspick_up_tips,drop_tips,aspirate,dispensethrough the newVantagePIPBackendTest plan
set_color,turn_off,disco_mode,russian_roulettewithVantageLEDParams(uv=...)🤖 Generated with Claude Code