Skip to content

Port Vantage to new Device/Driver/CapabilityBackend architecture#976

Open
rickwierenga wants to merge 4 commits intov1b1from
port-vantage-new-plr
Open

Port Vantage to new Device/Driver/CapabilityBackend architecture#976
rickwierenga wants to merge 4 commits intov1b1from
port-vantage-new-plr

Conversation

@rickwierenga
Copy link
Copy Markdown
Member

Summary

  • Splits the monolithic 5334-line VantageBackend into the new PLR architecture, mirroring the STAR port
  • New module at pylabrobot/hamilton/liquid_handlers/vantage/ with VantageDriver, VantagePIPBackend, VantageHead96Backend, VantageIPG, VantageXArm, VantageLoadingCover, Vantage device, and VantageChatterboxDriver
  • Adds generic LEDControlCapability (pylabrobot/capabilities/led_control/) with VantageLEDBackend encoding C0AM/LI firmware commands; UV and blink interval exposed as VantageLEDParams
  • Legacy VantageBackend now forwards pick_up_tips, drop_tips, aspirate, dispense through the new VantagePIPBackend

Test plan

  • All 21 legacy vantage tests pass through the forwarding chain
  • Chatterbox smoke test: setup/stop lifecycle, PIP/Head96/IPG/LED init commands
  • Import verification for all new classes and class hierarchy checks
  • LED capability end-to-end: set_color, turn_off, disco_mode, russian_roulette with VantageLEDParams(uv=...)

🤖 Generated with Claude Code

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>
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 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 with VantageDriver, Vantage device wiring, and new PIP/Head96/IPG/LED backends.
  • Adds a generic LEDControlCapability plus 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.

Comment on lines +699 to +701
elif not len(tadm_channel_pattern) < 24:
raise ValueError(
f"tadm_channel_pattern must be of length 24, but is '{len(tadm_channel_pattern)}'"
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

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

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).

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

Copilot uses AI. Check for mistakes.
Comment on lines +921 to +923
elif not len(tadm_channel_pattern) < 24:
raise ValueError(
f"tadm_channel_pattern must be of length 24, but is '{len(tadm_channel_pattern)}'"
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

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

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.

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

Copilot uses AI. Check for mistakes.
Comment on lines +1262 to +1264
elif not len(tadm_channel_pattern) < 24:
raise ValueError(
f"tadm_channel_pattern must be of length 24, but is '{len(tadm_channel_pattern)}'"
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

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

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.

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

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

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

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

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).

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

Copilot uses AI. Check for mistakes.
Comment on lines +53 to +56
await self._driver.send_command(
module="C0AM",
command="LI",
li={"on": 1, "off": 0, "blink": 2}[mode],
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

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

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.

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

Copilot uses AI. Check for mistakes.
Comment on lines +84 to +104
"""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)
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

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

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.

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

Copilot uses AI. Check for mistakes.
Comment on lines +63 to +72
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")
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
rickwierenga and others added 3 commits March 30, 2026 14:39
…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>
@rickwierenga rickwierenga force-pushed the v1b1 branch 5 times, most recently from 475dbab to 588abbb Compare April 2, 2026 21:12
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