Skip to content

AccelerationConsortium/opentrons-flex

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

28 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Opentrons Flex

A SiLA2 connector for the Opentrons Flex liquid-handling robot that also replaces the standard Opentrons robot-server HTTP API. Both servers share a single hardware API instance so they cannot conflict over the CAN bus.

Architecture

This project runs two servers in the same process when deployed to a real Flex:

Server Protocol Port Purpose
SiLA2 connector gRPC 50051 Lab automation clients (SiLA Browser, UniteLabs platform)
Opentrons robot-server HTTP REST 31950 Opentrons App, any REST client

Both servers are backed by one shared HardwareControlAPI (the Flex OT3API) wrapped in HardwareProxy — an asyncio.Lock around every hardware call. This serialises the SiLA gRPC server and the in-process HTTP robot-server so their CAN commands cannot interleave, and avoids the "hardware already initialised" error two separate processes would otherwise hit.

The standard opentrons-robot-server systemd service is disabled on deployment. Our sila2-connector service owns the hardware and starts the HTTP API in-process via uvicorn on a Unix domain socket (/run/aiohttp.sock). nginx on the Flex proxies external TCP port 31950 to that socket — so the HTTP API is reachable at http://<robot-ip>:31950 exactly as it would be with the stock opentrons-robot-server service.

Motion is exposed per mount (LEFT, RIGHT, GRIPPER) in deck coordinates (x, y, z mm), matching how the Flex hardware API models the robot.

SiLA features:

Feature Commands / properties
MotionControlFeature Home, HomeMount, MoveTo, MoveRelative, GetPosition, Aspirate, Dispense, BlowOut, PrepareForAspirate, EmergencyStop, Pause, Resume, SetLights; Lights, IsSimulating, MachineStatus
LiquidHandlingController Mix, TouchTip, ProbeLiquidLevel, AspirateWhileTracking, DispenseWhileTracking, atomic Transfer, and TransferWithVerifiedLiquidClass
LabwareMovementController MoveLabware / MoveLid execute locally allowlisted plans with server-owned occupancy, module-state, waypoint, pickup-width, and cancellation checks
PipetteFeature GetAttachedPipettes; full, single-nozzle, and rectangular partial-tip layouts for 1/8/96-channel heads
TipController Atomic move + PickUpTip / DropTip, GetTipPresence; sensor verification and defined recovery errors (use MotionControlFeature.EmergencyStop for a global halt)
GripperFeature Grip, Ungrip, HomeJaw; Status, JawWidth
CalibrationFeature CalibratePipette, CalibrateGripperJaw, CalibrateDeck (automatic probe-based routines)
Module features HeaterShaker, Thermocycler, Temperature, AbsorbanceReader, FlexStacker (registered when attached)

The Heater-Shaker controller exposes observable temperature, shaking, and latch operations with intermediate execution updates and defined module errors: SetTemperature, WaitForTemperature, DeactivateHeater, SetRpm, StopShaking, OpenLatch, CloseLatch, GetTemperature, GetRpm, GetLatchStatus, GetStatus, and GetDeviceInfo. Temperature inputs carry a degrees Celsius unit constraint and a 0–95 °C range; active shaking carries a revolutions-per-minute unit constraint and a 200–3000 rpm range. Use StopShaking instead of sending an implicit zero-speed sentinel.

Advanced liquid commands retain the low-level primitives for compatibility but run multi-step operations atomically under the same hardware lock. Verified liquid-class transfers read the version-2 definitions shipped with the installed Opentrons package and match them against the attached pipette plus the full tip-rack URI. Physical pipette installation remains an operator action; the connector detects the installed head, configures its active nozzles, and verifies tip state.

Labware movement never accepts grip coordinates, sensor tolerances, or deck occupancy from a remote SiLA call. Set labware_movement_config to a local JSON file containing plans, a durable state_file, and initial_occupancy; clients can then select only a provisioned plan_identifier. The connector updates its deck state after each completed move, always keeps pickup geometry verification enabled, and serializes module latch/lid operations with gripper motion through the shared hardware lock. Leave the setting null to expose discovery and defined errors without authorizing physical labware movement.

The local file has this shape; coordinates and grip values must come from the installed labware definitions and the calibrated deck model:

{
  "state_file": "/var/lib/unitelabs-opentrons-flex/labware-state.json",
  "initial_occupancy": {"D1": "plate-1"},
  "plans": [
    {
      "identifier": "plate_d1_to_d2",
      "labware_identifier": "plate-1",
      "is_lid": false,
      "source": {
        "identifier": "D1",
        "kind": "DECK_SLOT",
        "grip_point": {"x": -999.0, "y": -999.0, "z": -999.0}
      },
      "destination": {
        "identifier": "D2",
        "kind": "DECK_SLOT",
        "grip_point": {"x": -999.0, "y": -999.0, "z": -999.0}
      },
      "grip": {
        "force_newtons": 15.0,
        "expected_width": 74.0,
        "uncertainty_wider": 2.0,
        "uncertainty_narrower": 2.0
      },
      "post_drop_slide_offset": {"x": 0.0, "y": 0.0, "z": 0.0}
    }
  ]
}

The negative grip coordinates above are placeholders and are intentionally not a runnable deck plan. Replace them with validated absolute grip points before enabling the file on hardware, and provision a separate reverse plan for a round trip. The state ledger is marked invalid before physical movement and committed only after completion; an interrupted move or unclean shutdown requires local deck reconciliation before replacing the ledger and restarting.

Key source files:

  • src/unitelabs/opentrons_flex/__init__.pycreate_app() entry point and _create_app_with_robot_server() (the in-process HTTP server startup)
  • src/unitelabs/opentrons_flex/io/flex_motion.pyFlexMotionController (hardware-API wrapper)
  • src/unitelabs/opentrons_flex/io/hardware_proxy.pyHardwareProxy shared-lock wrapper
  • tests/test_create_app_with_robot_server.py — unit tests for the startup wiring (mocked)
  • tests/integration/ — end-to-end gRPC + HTTP API tests against the simulator or a live robot

Getting Started

For a general introduction to connector development with the UniteLabs CDK, see the connector development documentation.

Prerequisites

Ensure that uv is installed on your system. You can install it with:

pipx install uv

Installation

Create a Virtual Environment

It is highly recommended to use a virtual environment to manage the dependencies for your connector project. This keeps the dependencies for different connectors isolated from each other. Use the following command to create a virtual environment:

uv venv

Activate the virtual environment:

  • On Windows:

    .\venv\Scripts\activate.bat
  • On macOS/Linux:

    source .venv/bin/activate

If you are on a Windows machine, you may additionally wish to set the UNITELABS_CDK_APP environment variable to the connector's entry point:

set UNITELABS_CDK_APP=unitelabs.opentrons_flex:create_app

Setting this environment variable will allow you to run various CLI commands without providing --app unitelabs.opentrons_flex:create_app every time.

Install Required Dependencies

Install the connector and its dependencies into your active virtual environment:

uv sync --all-extras

Configure the Connector

To get information about the configuration values for the connector simply run:

  • On Windows:

    config show --app unitelabs.opentrons_flex:create_app
  • On macOS/Linux:

    config show

To create a configuration file for the connector we run:

  • On Windows:

    config create --app unitelabs.opentrons_flex:create_app
  • On macOS/Linux:

    config create

Used as such this command will create a config.json in the current working directory. If you prefer yaml, or would like to save the file to a different location, add the --path argument. A ready-made template is provided in config/flex_config.json.

Key values:

  • use_simulatortrue runs the OT3 hardware simulator (no robot); false drives real Flex hardware.
  • simulated_heater_shaker — when true, explicitly attaches one simulated Heater-Shaker to the OT3 simulator. It is rejected when use_simulator=false.
  • with_robot_servertrue additionally starts the in-process opentrons HTTP robot-server.

Note: The cloud_server_endpoint values are only necessary if you want to use the connector with the UniteLabs platform.

Verify the Installation

Start the connector in simulator mode using the CLI tool included in the dependencies:

connector start --app unitelabs.opentrons_flex:create_app -vvv

If you created your configuration file at a non-default location, specify it with --config-path (or -cfg):

connector start --app unitelabs.opentrons_flex:create_app -cfg <path to config> -vvv

Testing

The test suite runs fully offline against the real opentrons OT3 simulator — no robot required.

# Everything except the live-robot HTTP tests (which skip without a target).
# --extra test makes uv install pytest/httpx instead of accidentally using a global pytest.
uv run --extra test python -m pytest

# Just the unit + simulation tests
uv run --extra test python -m pytest tests/io tests/features

# gRPC integration tests over the wire (in-process SiLA server + OT3 simulator)
uv run --extra test python -m pytest tests/integration -k grpc

# Full Heater-Shaker workflow over SiLA gRPC against the explicit module simulator
uv run --extra test python -m pytest tests/integration/test_grpc_heater_shaker.py -v

# Advanced liquid, partial-nozzle, and gripper labware workflows over SiLA gRPC
uv run --extra test python -m pytest \
  tests/integration/test_grpc_advanced_flex.py tests/integration/test_grpc_pipette.py -v

# Full local smoketest: SiLA gRPC + in-process robot-server HTTP API, both backed
# by the OT3 simulator. This is the CI/CD end-to-end path before real hardware.
uv run --extra test python -m pytest tests/integration --with-http-server -v

The suite covers the controllers and SiLA features driven against the OT3 simulator, SiLA feature-definition generation, and real gRPC calls through the full chain gRPC → SiLA server → feature → hardware API, including defined-execution errors propagating over the wire.

config/smoketest_config.json is the local no-hardware config. It keeps use_simulator=true, with_robot_server=true, and cloud/discovery disabled. The deployment config in config/flex_config.json remains explicitly live-hardware (use_simulator=false) so simulator and robot runs do not blur together.

Testing against a real Flex

Once the connector is deployed and running on a Flex (see Deploying below), point the read-only smoke tests at it first. --robot HOST:50051 targets the live SiLA server; --robot-http HOST:31950 targets the in-process robot-server.

Do not run the full simulator gRPC suite against a real robot. Tests that home, move, actuate lights, pause/resume, emergency-stop, grip/ungrip, or calibrate are simulator-only unless they live under tests/integration/hardware/, where each movement is intentionally small and paired with a MachineStatus check.

# Read-only gRPC smoke tests against the live Flex
uv run --extra test python -m pytest tests/integration/test_grpc_motion_control.py \
    -k "is_simulating or machine_status" --robot <robot-ip>:50051 -v

# HTTP robot-server API tests against the live Flex
uv run --extra test python -m pytest tests/integration/http_api --robot-http <robot-ip>:31950

Tests marked @pytest.mark.simulator_only are skipped automatically when --robot is set. Hardware motion is validated through the explicit HITL suite below, not through the broad simulator integration suite.

For a Flex with a physical Heater-Shaker, begin with the read-only identity and status check. It does not move the latch, heat, or shake:

uv run --extra test python -m pytest \
  tests/integration/hardware/test_hitl_heater_shaker.py \
  -k identity --robot <robot-ip>:50051 -v

The actuation test is gated separately. Run it only after installing a compatible thermal adapter and secured labware, closing the robot door, and keeping the E-stop ready. It closes the latch, shakes at the minimum 200 rpm, then stops and deactivates the heater in cleanup:

uv run --extra test python -m pytest \
  tests/integration/hardware/test_hitl_heater_shaker.py \
    --robot <robot-ip>:50051 --heater-shaker-actuation -v

Labware movement has a separate physical safety gate. First provision validated outbound and return plans in the connector's local labware_movement_config. The test selects only those allowlisted plans, moves the prepared labware out and back, and checks machine state:

uv run --extra test python -m pytest \
  tests/integration/hardware/test_hitl_labware_movement.py \
  --robot <robot-ip>:50051 --gripper-labware-actuation \
  --gripper-outbound-plan PLAN_ID \
  --gripper-return-plan RETURN_PLAN_ID -v

Every integration run prints its mode/target/device header and records mode / sila_target / http_target / device_id to each test's junit properties, so a result is never ambiguous about whether it hit the simulator or a real robot (a hardware_only test that somehow runs in smoketest mode fails loudly rather than passing against the simulator).

Hardware-in-the-loop (HITL) motion tests

tests/integration/hardware/ holds Stage-4 HITL tests that run only against a real Flex (--robot/--robot-http) and are skipped otherwise. They issue only small, reversible moves (home, a 5 mm Z jog up-and-back, lights) and — crucially — query MachineStatus after every movement to assert the robot did not silently enter a hardware error state (E-stop engaged, etc.). A move that returns is not assumed to have succeeded; the connector's post-move guard raises MachineErrorStateError on a hidden fault, and the HITL client re-checks from the outside.

uv run --extra test python -m pytest tests/integration/hardware --robot <robot-ip>:50051 -v

HTTP ↔ SiLA parity matrix

docs/parity_matrix.md (source: docs/parity_matrix.json) records which Opentrons HTTP API functions have a first-class SiLA2 equivalent (supported / unclear / unsupported). tests/integration/http_api/test_parity_matrix.py validates the matrix and, under --with-http-server, cross-checks its HTTP paths against the live robot-server OpenAPI so the matrix cannot silently drift.

Deploying to the Flex

The Flex host is aarch64 (ARM64) with a modern glibc, so standard PyPI manylinux_2_17_aarch64 wheels for C-extension packages (grpcio, numpy, …) install directly — no from-source build is required.

Building dist_arm/

Build the aarch64 wheel bundle with the provided Dockerfile:

docker buildx build --platform linux/arm64 -f Dockerfile.build \
    --target export --output type=local,dest=dist_arm .

Or trigger the Build Flex aarch64 Wheels GitHub Actions workflow (.github/workflows/build-flex-arm-wheels.yml) and download the flex-arm-wheels artifact into dist_arm/.

Installing on the Flex

Copy the wheels and create the venv on the robot:

./deploy.sh <robot-ip>

Then install the connector as a persistent systemd service (this disables the Opentrons robot server so the connector owns the hardware):

./scripts/install_connector_service.sh <robot-ip>

Switch between the SiLA connector and the stock opentrons robot-server at any time (the choice persists across reboot):

./scripts/switch_mode.sh <robot-ip> connector
./scripts/switch_mode.sh <robot-ip> opentrons

To deploy Python source changes to a robot that already has the service installed:

./scripts/deploy_python_changes.sh <robot-ip>

Logs:

ssh root@<robot-ip> 'journalctl -u sila2-connector -f'

Why --system-site-packages

The opentrons package (and the robot_server HTTP server) are pre-installed as system packages by the Opentrons robot software. They are intentionally excluded from dist_arm/ to avoid version conflicts; the venv inherits them via --system-site-packages.

robot_server is not distributed in the public opentrons PyPI wheel. Its source lives in the Opentrons monorepo under robot-server/robot_server. Before the first hardware run, verify the Flex image exposes the same package:

ssh root@<robot-ip> \
  '/var/sila2_flex/bin/python -c "import robot_server, robot_server.hardware, robot_server.app; print(robot_server.__file__)"'

Then start the connector and run the live HTTP integration tests:

uv run --extra test python -m pytest tests/integration/http_api --robot-http <robot-ip>:31950

Usage

To interact with the running connector, we recommend using the SiLA Browser against <robot-ip>:50051. The Opentrons App and REST clients can use the standard HTTP API shape at http://<robot-ip>:31950. When durable labware plans are enabled, raw HTTP gripper/extension and gripper-axis actuation is intentionally rejected so it cannot bypass LabwareMovementController; use the allowlisted SiLA plans or disable the local plan registry for a deliberate maintenance session.

Encryption

To secure communication between the connector and its clients, you can enable TLS encryption. Start by installing the optional cryptography package for generating TLS certificates:

uv pip install cryptography

To generate a pair of public and private keys, use:

certificate generate

Without any arguments this command uses the default config location to get the connector's UUID and host name. It will prompt you as to whether or not you want to update your config file to enable TLS. This prompt can be suppressed with --non-interactive/-y, or with --embed/-e to write the file contents into the config file directly.

If you choose not to auto-update the config, set the following values yourself under sila_server:

  • certificate_chain — the path to the cert.pem file
  • private_key — the path to the key.pem file
  • tls — a boolean that toggles TLS encryption for the SiLA server

Important: Never share the key.pem file with anyone. Only the cert.pem is required for clients to connect to encrypted servers.

Contribute

We welcome contributions to improve this connector. We use uv for Python packaging (requires uv>=0.6.8).

Clone the repository and set up the development environment:

git clone https://github.com/AccelerationConsortium/opentrons-flex.git
cd opentrons-flex
uv sync --all-extras
uv run connector start -vvv

Install pre-commit hooks to ensure code quality:

uv run pre-commit install

Run the test suite and linters:

uv run pytest
uv run ruff check src tests

To improve the development experience, run the connector in developer mode, which automatically reloads whenever source changes are saved:

uv run connector dev --app unitelabs.opentrons_flex:create_app

Contact

If you found a bug, please use the issue tracker.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors