Skip to content

STAR: v1 driver, first wave - #1269

Open
BioCam wants to merge 1 commit into
PyLabRobot:mainfrom
BioCam:v1-star-basic-integration
Open

BioCam wants to merge 1 commit into
PyLabRobot:mainfrom
BioCam:v1-star-basic-integration

Conversation

@BioCam

@BioCam BioCam commented Sep 14, 2026

Copy link
Copy Markdown
Collaborator

Hi everyone,

We need a new Hamilton STAR integration that is...

  • smarter
  • more scalable
  • more intuitive, and
  • more powerful.

Here I built the first (purposefully limited) version of the new v1 STAR integration with this aim.
(Note: when using the term "STAR" here, I refer to the STAR, STARlet, and STARPlus liquid handling workstations in all their possible configurations.)

The Problem

The v0 integration of the Hamilton STAR is the strongest of all liquid handling workstations in PyLabRobot. It was the first in PLR's history, and has continuously grown ever since.
However, as we have learned over the years what makes the STAR system so powerful, we have also learned the limitations of our existing monolithic architecture.
The main ones include...

  • maintenance and consistency of the over 14,000 line backend STAR_backend.py
  • extendability of existing and creation of new features, e.g.:
    • smart pipetting,
    • anti-crash features,
    • granularisation of complex commands,
    • fixing firmware bugs we found via software circumvention strategies,
    • integrating the head384 and, more generally, any new feature with ease,
    • ...
  • creation of a true device tracking system which updates in real time,
  • the constant need to navigate between the STAR backend and the liquid handler frontend for different steps inside the same function (e.g. aspirate, dispense),
  • advanced simulation,
  • and more.

v1 STAR

Here, I introduce a new STAR driver with a range of new functionalities and a series of new concepts.

The aim of this PR is not to produce a fully functional v1 STAR driver.
That requires careful step by step building and reviews by the community along the way.
The aim of this PR is to provide the first wave of new functionalities, validate them, actively discuss and build modifications ("identifying the unknown unknowns"), and get ready for the next wave (specifically, tip and liquid handling).

from pylabrobot.hamilton import STAR

star = STAR(simulation=True)  # or STAR() for your physical device
await star.setup()
  • Device-centered architecture
    Core to PLR v1: the center of an integration is now the device, a resource which owns a device driver and has a deck.

    • the driver enables complete programmatic control of the device
    • the deck and the device resource itself are the modelling system / "digital twin" of the integration
    star.deck
    # HamiltonSTARDeck(name='deck', location=Coordinate(116.215, 097.800, 080.500), size_x=1545.0, ...)
    star.driver.features
    # [Pipettes, Head96, iSWAP, Autoload] - only what setup found fitted on this device
  • Composition architecture across programmatic control & modelling system
    Robots are fundamentally composable, i.e. made up of subcomponents which can vary between configurations.
    Based on @rickwierenga's idea of the v1 architecture, this composition is now taken directly into the integration architecture:

    1. Each physical feature is programmatically enabled by a dedicated feature module (e.g. pipettes.py, iswap.py, head96.py, autoload.py, ...).
    2. Simultaneously, each moving feature is modelled as a resource -> the feature resource.

    (Importantly, "one feature, one module" is a paradigm, not a hard rule - where it is unintuitive or overcomplicating, we do not adhere to it just for the sake of ticking a checkbox. Discussion and community feedback are meant to derive useful solutions in these cases.)

    A feature's driver module and resource are in continuous communication. This enables...

    1. real time updates of physical state,
    2. anti-crash features based on model state and the next task commanded (WIP),
    3. simulation, which returns model state (in the absence of a device request), and more.
    # every feature is its own object - no more one backend for everything
    await star.pipettes.move_to_y_position(0, 400.0, make_space=True)  # moves the other channels out of the way first
    await star.iswap.rotate_to_angles(rotation_absolute_angle="front", gripper_absolute_angle="front")
    await star.autoload.move_to_track(10)
    
    # ...and the model moved with them
    await star.pipettes.request_y_positions()  # what the device says
    # [400.0, 378.3, 351.6, 324.9, 298.2, 271.5, 244.8, 218.1]
    channel = star.pipettes.resources[0]  # what the model says
    (channel.get_location_wrt(star.deck) + channel.reference_point).y
    # 400.0
    star.iswap.rotation_drive_get_angle(), star.iswap.wrist_drive_get_angle()
    # (0.0, -45.0)
  • Configuration per device feature as the source of all information
    A common problem we encountered is that each feature brings its own settings, conversion factors, subcomponent modifications, ... i.e. configuration.
    Worse, between different hardware and firmware versions these configurations can vary massively (e.g. a drive's parameters might use increments x 100 and after an update just increments, drive ranges might change as the hardware evolves, ...).
    We need to discover & capture this information during setup/initialisation of a device and record it inside the running integration, so every command of the driver works with up-to-date information.
    This is why

    1. the device itself has a configuration (STARDriver.configuration), which also declares which features are fitted, and
    2. every feature has its own configuration (e.g. XArm.configuration, Head96.configuration, ...), which provides the information that feature needs.
    star.driver.configuration.num_pip_channels
    # 8
    star.iswap.configuration.rotation_drive_y_min, star.iswap.configuration.rotation_drive_y_max
    # (178.6, 627.39) - read off this device at setup, not hard-coded

    On a physical device, this entire configuration can easily be saved as a configuration JSON (though I am not attached to JSON as the format here).

  • Simulation, with easy configuration declaration provided by JSON
    A real simulation engine must...

    1. generate real firmware commands (which is what STARChatterboxBackend already did in legacy/v0),
    2. read state from a model (which is continuously updated by the simulated driver's actions),
    3. be easily configured to represent the physical device's features and capabilities.

    To enable (3), the v1 STAR driver now takes an optional declared_configuration_json: Optional[str], the path to a saved configuration.
    When provided...
    a) to a simulated STAR, it adapts the simulator to that specific configuration (x/y/z ranges, accelerations, speeds, which feature classes are instantiated, ...),
    b) to a physical STAR, it cross-checks the declared configuration against what the device reports is fitted: channel count, deck size, autoload, iSWAP, heads, cover monitoring, and what each arm carries. Serial number, firmware and geometry are deliberately not compared, so a configuration saved on one device describes any other of the same build (very useful for automated protocols requiring specific features :)
    (A simulated STAR runs the same cross-check during setup - its declared configuration is what it answers from, so there it always matches.)

    Simple SOP: connect to your device, run setup + save the configuration -> use that JSON from then on for all simulations of that device:

    star = STAR()
    await star.setup()
    star.driver.save_configuration("my_star.json")
    
    # from then on: a digital twin of exactly that device
    twin = STAR(simulation=True, declared_configuration_json="my_star.json")
    await twin.setup()
    
    # and a protocol that needs those features refuses to run on a device without them
    star = STAR(declared_configuration_json="my_star.json")
    await star.setup()
    # ValueError: the declared configuration does not describe this device:
    #   autoload_installed: declared True, device answers False

What is in this PR, and what is not

  • In: device discovery & setup, moves of every feature (channels, head96/head384, iSWAP, autoload, X-arms), the feature resources, simulation, configuration save/load.
  • Not yet: tip and liquid handling (the next wave). The 3D visualizer and the Prep integration come as their own PRs. Changelog entry and user docs follow.
  • v0 is untouched: the legacy STARBackend works exactly as before, so nobody has to migrate yet.
  • Backwards compatible: the resources/ changes (STAR decks and CORE grippers in their own modules, channels modelled as NChannelPipette/TipMountingShaft) keep their old imports working - main's own resources and legacy tests pass against this branch.
  • Tested on a physical STAR (8 channels, 96-head, iSWAP, autoload) and in simulation on STAR, STARlet and STARPlus configurations.

How to review

It's one commit, most of it in pylabrobot/hamilton/star/driver/features/. A good reading order: device.py -> driver/master.py -> one feature (x_arm.py is the smallest) -> driver/simulator.py.

Known follow-ups: move_to_safe_z as a stop-disc move with speed & acceleration, rotate_to_angles refusing before it moves, packet_read_timeout in the reply router.

There is a lot more, but this is getting too long.
So let's discuss the rest in the review of this and provide regular smaller summaries so people can easily follow along.

🤖 Generated with Claude Code

A STAR, STARlet and STAR+ driven through a single driver in `pylabrobot.hamilton`,
with each fitted module as a feature of it: the pipetting channels, the 96- and
384-heads, the iSWAP, the autoload, the X-arms and the front cover. Setup discovers
what is fitted, initializes only what reports itself down, and models every
feature on the deck. A simulator answers from recorded device configurations, and
a device's configuration can be saved and simulated from later.

Resources: the STAR decks and CORE grippers move to their own modules with their
old imports kept, the pipetting channels are modelled as `NChannelPipette` and
`TipMountingShaft`, and `HamiltonDeck` places the parts the driver models.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@BioCam
BioCam requested a review from a team as a code owner September 14, 2026 21:22
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.

1 participant