Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
5f3869c
feat(opentrons): Opentrons Flex liquid handler (plain-class, mount-ad…
vcjdeboer Aug 5, 2026
891238c
feat(opentrons): gripper capability + fine pipetting on Flex heads
miikee Aug 12, 2026
daa7be4
feat(opentrons): container/reservoir ops + motion surface on Flex hea…
miikee Aug 12, 2026
49aa753
feat(opentrons): custom labware definition upload for non-Opentrons l…
miikee Aug 12, 2026
3f3b89c
fix(opentrons): post-review fix pass on the Flex capability stack
miikee Aug 12, 2026
e9538ea
fix(opentrons): well-position clearance, rotated-cavity guard, and st…
miikee Aug 12, 2026
f58d4a8
fix(opentrons): anchor uploaded well geometry at the real cavity floor
miikee Aug 13, 2026
8b5a59f
fix(opentrons): clear a cherry-picked tip, and anchor it where the ro…
miikee Aug 13, 2026
de0f726
build: declare httpx in the opentrons extra
miikee Aug 13, 2026
947c5d3
docs: drop a stranded copy of the channel-spreading comment in dispense
miikee Aug 13, 2026
6ca4e6a
Expose the lifecycle steps behind setup() and stop()
miikee Aug 12, 2026
b04653d
Take flow-rate defaults from the robot's own pipette data
miikee Aug 13, 2026
a62435a
Resolve labware by declared load name or model, and drop tips where t…
miikee Aug 13, 2026
8f58e58
`CaptureReader.done()`: end validation instead of re-arming it
miikee Aug 13, 2026
53fc90b
`io.http`: record and replay a request/response device API
miikee Aug 13, 2026
054d851
`OpentronsRobot`: route the wire through `io.http`, add replay and `s…
miikee Aug 13, 2026
6854668
`OpentronsFlex`: name-based motion, in-place pipetting, and recovery …
miikee Aug 13, 2026
252056b
Read tip presence as a run command, not over GET /instruments
miikee Aug 13, 2026
dd25757
Let the robot assign labware ids, so a capture can be replayed
miikee Aug 13, 2026
cbee07f
Fix two engine-contract bugs the simulator sweep found
miikee Aug 13, 2026
8ad7ef6
Address the driver review: capture recipe, optional extra, lifecycle …
miikee Aug 13, 2026
f93c9de
Carry the slot-deck reads and OT-2 channel moves onto the plain-class…
miikee Aug 13, 2026
a600d30
move_to_well: accept a tip spot, not just a well
miikee Aug 13, 2026
d6f23ce
Let a deck attach to a live robot, and stop create_run stranding the …
miikee Aug 13, 2026
cc14c72
A Flex head carries its own pipette capacity
miikee Aug 13, 2026
74e1baa
Fold the repeated aspirate/dispense plumbing into one helper
miikee Aug 17, 2026
e4f312f
Say which head was verified on hardware, per op
miikee Aug 17, 2026
1401754
Let the robot own plunger priming
miikee Aug 17, 2026
8e0a02e
Record the in-place ops as hardware verified, and say where a tip bel…
miikee Aug 17, 2026
c93a306
Take the legacy backend changes out of this PR
miikee Aug 18, 2026
52704e7
Drop the opentrons-shared-data dependency
miikee Aug 18, 2026
519215d
Let the robot say what labware names it knows, instead of guessing here
miikee Aug 18, 2026
cbe884e
Let the resource say which definition revision to load
miikee Aug 18, 2026
2f4f704
Merge pull request #1 from Cheshire-Labs/feat/opentrons-plainclass-ca…
vcjdeboer Aug 18, 2026
b34c881
Unify FlexHead8 around per-call use_channels, and always travel above…
vcjdeboer Aug 18, 2026
abba480
fix(opentrons): record FlexHead8 hardware-verified ops accurately
vcjdeboer Aug 20, 2026
b5e841d
chore(opentrons): green the Flex-stack CI checks
vcjdeboer Aug 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions _typos.toml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ LOK = "LOK"
ouput = "ouput"
hegiht = "hegiht"

# "unparseable" is an accepted English variant used by the Opentrons version parser.
unparseable = "unparseable"

[files]
extend-exclude = [
"*.ipynb"
Expand Down
44 changes: 44 additions & 0 deletions docs/api/pylabrobot.opentrons.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
.. currentmodule:: pylabrobot.opentrons

pylabrobot.opentrons package
============================

Flex
----

.. autosummary::
:toctree: _autosummary
:nosignatures:
:recursive:

OpentronsRobot
OpentronsFlex
OpentronsError
OpentronsCommandError
PipetteInfo

Heads
-----

.. currentmodule:: pylabrobot.opentrons.flex_head

.. autosummary::
:toctree: _autosummary
:nosignatures:
:recursive:

FlexHead1
FlexHead8
FlexHead96

Gripper
-------

.. currentmodule:: pylabrobot.opentrons.flex_gripper

.. autosummary::
:toctree: _autosummary
:nosignatures:
:recursive:

FlexGripper
1 change: 1 addition & 0 deletions docs/api/pylabrobot.rst
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ Manufacturers
pylabrobot.kbiosystems
pylabrobot.mettler_toledo
pylabrobot.molecular_devices
pylabrobot.opentrons
pylabrobot.qinstruments
pylabrobot.sartorius
pylabrobot.thermo_fisher
1 change: 1 addition & 0 deletions docs/user_guide/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ kbioscience/index
kbiosystems/index
mettler_toledo/index
molecular_devices/index
opentrons/index
qinstruments/index
sartorius/index
thermo_fisher/index
Expand Down
328 changes: 328 additions & 0 deletions docs/user_guide/opentrons/flex/hello-world.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,328 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "flex-intro",
"metadata": {},
"source": [
"# Opentrons Flex — hello world (real hardware)\n",
"\n",
"This notebook drives a **real Opentrons Flex** over its robot-server HTTP API\n",
"using the mount-addressed head model:\n",
"\n",
"- `OpentronsFlex` is the device. It owns the deck, the HTTP connection, and\n",
" discovers whichever pipette(s) are actually mounted at `setup()` time,\n",
" composing a head sub-object onto `flex.left`, `flex.right`, and/or\n",
" `flex.head96` — there is no `flex.pick_up_tips(...)`; you always go\n",
" through the head that matches the mounted pipette (e.g. `FlexHead8` for\n",
" an 8-channel head).\n",
"- **The robot owns labware geometry, not PLR.** A tip rack or plate built\n",
" here carries only a *nominal* SBS grid (named `TipSpot`/`Well` objects for\n",
" tip/volume tracking) — when it's loaded, PLR sends the robot its\n",
" Opentrons load name (`ot_load_name`, e.g.\n",
" `\"opentrons_flex_96_tiprack_50ul\"`) and the robot resolves the real,\n",
" authoritative definition. We just *name* what we loaded.\n",
"- **The Flex hardware tip sensor is authority for tip presence.** Every\n",
" `pick_up_tips()` is verified against the real per-pipette `tipDetected`\n",
" sensor (`GET /instruments`) after the wire command succeeds — PLR's tip\n",
" trackers only commit if the sensor confirms a tip actually seated, and\n",
" roll back otherwise.\n",
"\n",
"```{warning}\n",
"**Safety note before running:**\n",
"\n",
"- Clear the deck of anything you don't want the gantry to hit.\n",
"- Load a **real Flex 50 uL tip rack** in slot **C1** and a **real 96-well\n",
" plate** in slot **D1** (matching the labware constructed in the cells\n",
" below).\n",
"- Confirm the robot-server is reachable on port `31950` (the Opentrons App\n",
" can already talk to it — that's the same server).\n",
"- **Close the Flex's front door before running.** The gantry moves more\n",
" safely with the enclosure shut, and the Flex expects the door closed\n",
" during motion.\n",
"- Running this notebook **homes all axes and moves the gantry**. Keep hands\n",
" and obstructions clear of the deck while cells are executing.\n",
"```\n",
"\n",
"```{note}\n",
"`FlexHead8` is verified on real Opentrons Flex hardware, so it no longer\n",
"emits an untested-hardware warning. `FlexHead1` and `FlexHead96` remain\n",
"unverified (they need 1-channel / 96-channel pipettes) and still log a\n",
"one-time warning on first use.\n",
"```"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "flex-imports-code",
"metadata": {},
"outputs": [],
"source": [
"FLEX_HOST = \"169.254.1.1\" # <-- SET to your Flex's IP / USB address\n",
"\n",
"from pylabrobot.opentrons import FlexHead8, OpentronsFlex\n",
"from pylabrobot.resources.opentrons import (\n",
" FlexDeck,\n",
" corning_96_wellplate_360ul_flat,\n",
" flex_96_tiprack_50ul,\n",
")"
]
},
{
"cell_type": "markdown",
"id": "flex-deck-md",
"metadata": {},
"source": [
"## Build the deck and labware\n",
"\n",
"Construct a `FlexDeck` (12 standard slots + trash, auto-placed at `A3`),\n",
"then create a Flex 50 uL tip rack and a Corning 96-well plate and place them\n",
"on real deck slots with `deck.assign_child_at_slot(...)`. These must match\n",
"the physical labware you loaded onto the robot in the safety step above.\n",
"\n",
"Both factories build a *nominal* PLR grid (for tracking/addressing) and set\n",
"`ot_load_name` to the Opentrons Labware Library name — that name is how the\n",
"labware is identified to the robot; the robot looks up its own authoritative\n",
"geometry from it."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "flex-deck-code",
"metadata": {},
"outputs": [],
"source": [
"deck = FlexDeck()\n",
"\n",
"tip_rack = flex_96_tiprack_50ul(name=\"tips_01\")\n",
"plate = corning_96_wellplate_360ul_flat(name=\"plate_01\")\n",
"\n",
"deck.assign_child_at_slot(tip_rack, \"C1\")\n",
"deck.assign_child_at_slot(plate, \"D1\")"
]
},
{
"cell_type": "markdown",
"id": "flex-connect-md",
"metadata": {},
"source": [
"## Connect\n",
"\n",
"`OpentronsFlex(deck, host=FLEX_HOST)` builds the device; `await flex.setup()`\n",
"opens the HTTP connection, checks `/health`, creates an empty run, and\n",
"discovers + loads the mounted pipette(s) — composing a head (`FlexHead1`,\n",
"`FlexHead8`, or `FlexHead96`) onto `flex.left`/`flex.right`/`flex.head96`\n",
"depending on what's actually mounted."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "flex-connect-code",
"metadata": {},
"outputs": [],
"source": [
"flex = OpentronsFlex(deck, host=FLEX_HOST)\n",
"await flex.setup()\n",
"\n",
"print(\"api_version:\", flex.api_version)\n",
"print(\"robot_model:\", flex.robot_model)\n",
"print(\"left mount: \", flex.left)\n",
"print(\"right mount:\", flex.right)\n",
"print(\"96-head: \", flex.head96)"
]
},
{
"cell_type": "markdown",
"id": "flex-head-md",
"metadata": {},
"source": [
"## Pick the active 8-channel head\n",
"\n",
"Grab whichever mount discovery populated (`flex.left` or `flex.right`) and\n",
"confirm it's the `FlexHead8` this notebook is written for.\n",
"`get_mounted_tips()` reports per-channel tip state — PLR-side bookkeeping,\n",
"`None` per channel until a pickup happens."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "flex-head-code",
"metadata": {},
"outputs": [],
"source": [
"head = flex.left or flex.right\n",
"assert isinstance(head, FlexHead8), f\"expected FlexHead8, got {type(head)}\"\n",
"\n",
"print(\"mounted tips:\", head.get_mounted_tips())"
]
},
{
"cell_type": "markdown",
"id": "flex-home-md",
"metadata": {},
"source": [
"## Home\n",
"\n",
"Homes all axes — the gantry moves to the rear-left-top reference position."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "flex-home-code",
"metadata": {},
"outputs": [],
"source": [
"await flex.home()"
]
},
{
"cell_type": "markdown",
"id": "flex-pickup-md",
"metadata": {},
"source": [
"## Pick up a column of tips\n",
"\n",
"One `pickUpTip` command anchored at column 0's A-row well (`A1`); the\n",
"hardware fans it out to all 8 physical nozzles, picking up the whole column\n",
"at once."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "flex-pickup-code",
"metadata": {},
"outputs": [],
"source": [
"await head.pick_up_tips(tip_rack, column=0)\n",
"\n",
"print(\"mounted tips:\", head.get_mounted_tips())"
]
},
{
"cell_type": "markdown",
"id": "flex-tip-presence-md",
"metadata": {},
"source": [
"## Verify tip presence against the hardware sensor\n",
"\n",
"`pick_up_tips()` already checked this internally — it verifies the pickup\n",
"against the Flex's real per-pipette `tipDetected` sensor\n",
"(`GET /instruments`) before committing PLR's tip trackers, and rolls the\n",
"pickup back (raising) if the sensor never reports a seated tip. This cell\n",
"just re-queries that same sensor explicitly (`has_tip_on_hardware()`) so you\n",
"can see the hardware ground truth next to PLR's own per-channel bookkeeping\n",
"(`get_mounted_tips()`)."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "flex-tip-presence-code",
"metadata": {},
"outputs": [],
"source": [
"print(\"hardware tipDetected:\", await head.has_tip_on_hardware())\n",
"print(\"mounted tips:\", head.get_mounted_tips())"
]
},
{
"cell_type": "markdown",
"id": "flex-liquid-md",
"metadata": {},
"source": [
"## Aspirate and dispense\n",
"\n",
"Aspirate 50 uL from column 0 of the plate, then dispense it back — each is a\n",
"single command anchored at the column's A-row well (`A1`), fanned to all 8\n",
"channels. The first aspirate since the last tip pickup automatically fires a\n",
"`prepareToAspirate` command before the `aspirate` itself — the Flex requires\n",
"this explicit plunger-priming step (unlike the STAR, where it's implicit)."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "flex-liquid-code",
"metadata": {},
"outputs": [],
"source": [
"await head.aspirate(plate, column=0, volume=50)\n",
"await head.dispense(plate, column=0, volume=50)"
]
},
{
"cell_type": "markdown",
"id": "flex-discard-md",
"metadata": {},
"source": [
"## Discard the tips\n",
"\n",
"Drop the mounted column of tips into the deck's trash (auto-placed at slot\n",
"`A3` by `FlexDeck`), then re-query the hardware tip-presence sensor — it\n",
"should now report no tip seated."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "flex-discard-code",
"metadata": {},
"outputs": [],
"source": [
"trash = flex.deck.get_trash_area()\n",
"await head.discard_tips(trash)\n",
"\n",
"print(\"after drop, tipDetected:\", await head.has_tip_on_hardware())"
]
},
{
"cell_type": "markdown",
"id": "flex-teardown-md",
"metadata": {},
"source": [
"## Teardown\n",
"\n",
"`flex.stop()` drops any mounted tips into the trash (distributed across the\n",
"bin via `alternateDropLocation`), homes the gantry, then cancels the run and\n",
"closes the HTTP connection — so the robot is left parked and empty-handed."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "flex-teardown-code",
"metadata": {},
"outputs": [],
"source": [
"await flex.stop()"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.10.0"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
Loading
Loading