Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
68cf51b
feat(opentrons): Opentrons Flex liquid handler (plain-class, mount-ad…
vcjdeboer Aug 5, 2026
d827453
feat(opentrons): gripper capability + fine pipetting on Flex heads
miikee Aug 12, 2026
9737372
feat(opentrons): container/reservoir ops + motion surface on Flex hea…
miikee Aug 12, 2026
8529045
feat(opentrons): custom labware definition upload for non-Opentrons l…
miikee Aug 12, 2026
d763294
fix(opentrons): post-review fix pass on the Flex capability stack
miikee Aug 12, 2026
848fdc7
fix(opentrons): well-position clearance, rotated-cavity guard, and st…
miikee Aug 12, 2026
966f80e
fix(opentrons): anchor uploaded well geometry at the real cavity floor
miikee Aug 13, 2026
0eb8c94
fix(opentrons): clear a cherry-picked tip, and anchor it where the ro…
miikee Aug 13, 2026
88a8fd5
build: declare httpx in the opentrons extra
miikee Aug 13, 2026
1289640
docs: drop a stranded copy of the channel-spreading comment in dispense
miikee Aug 13, 2026
2356427
Expose the lifecycle steps behind setup() and stop()
miikee Aug 12, 2026
6513104
Take flow-rate defaults from the robot's own pipette data
miikee Aug 13, 2026
c90a8f4
Resolve labware by declared load name or model, and drop tips where t…
miikee Aug 13, 2026
6950116
`CaptureReader.done()`: end validation instead of re-arming it
miikee Aug 13, 2026
c60df86
`io.http`: record and replay a request/response device API
miikee Aug 13, 2026
5a004a9
`OpentronsRobot`: route the wire through `io.http`, add replay and `s…
miikee Aug 13, 2026
3196c2b
`OpentronsFlex`: name-based motion, in-place pipetting, and recovery …
miikee Aug 13, 2026
df85e18
Read tip presence as a run command, not over GET /instruments
miikee Aug 13, 2026
6dfea16
Let the robot assign labware ids, so a capture can be replayed
miikee Aug 13, 2026
1c82224
Fix two engine-contract bugs the simulator sweep found
miikee Aug 13, 2026
6d207b0
Address the driver review: capture recipe, optional extra, lifecycle …
miikee Aug 13, 2026
85bbe8f
Carry the slot-deck reads and OT-2 channel moves onto the plain-class…
miikee Aug 13, 2026
8fefdc5
move_to_well: accept a tip spot, not just a well
miikee Aug 13, 2026
c1f42dd
Let a deck attach to a live robot, and stop create_run stranding the …
miikee Aug 13, 2026
5274073
A Flex head carries its own pipette capacity
miikee Aug 13, 2026
059dde3
Fold the repeated aspirate/dispense plumbing into one helper
miikee Aug 17, 2026
32c69bb
Say which head was verified on hardware, per op
miikee Aug 17, 2026
f5ec681
Let the robot own plunger priming
miikee Aug 17, 2026
f8bef12
Record the in-place ops as hardware verified, and say where a tip bel…
miikee Aug 17, 2026
489caab
Take the legacy backend changes out of this PR
miikee Aug 18, 2026
ed797e1
Drop the opentrons-shared-data dependency
miikee Aug 18, 2026
2296e78
Let the robot say what labware names it knows, instead of guessing here
miikee Aug 18, 2026
a8277b2
Let the resource say which definition revision to load
miikee Aug 18, 2026
fb9fc1b
Unify FlexHead8 around per-call use_channels, and always travel above…
vcjdeboer Aug 18, 2026
113c49b
fix(opentrons): record FlexHead8 hardware-verified ops accurately
vcjdeboer Aug 20, 2026
2e59b12
chore(opentrons): green the Flex-stack CI checks
vcjdeboer Aug 20, 2026
9fc254e
Choose a single-tip anchor by what the idle nozzles sit over, not by …
miikee Aug 18, 2026
642a173
Make the single-tip anchor a declared argument, not a derived one
miikee Aug 18, 2026
c3357af
Record what the 8-channel head and gripper have now run on hardware
miikee Aug 18, 2026
aff3cff
Credit the 8-head with the in-place and recovery ops it ran on hardware
miikee Aug 18, 2026
1dd5d61
Record what the 8-head and the gripper have run on hardware
miikee Aug 18, 2026
c90aba1
feat(opentrons): default the single-tip anchor to H1, derive the well…
vcjdeboer Sep 4, 2026
fd31c66
fix(opentrons): name the plate, not the well, when material_z_thickne…
vcjdeboer Sep 4, 2026
67c5bea
chore(opentrons): drop a stray editor workspace file
vcjdeboer Sep 4, 2026
ac6bc0d
fix(opentrons): restore the opentrons_shared_data mypy override
vcjdeboer Sep 4, 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 @@ -45,6 +45,9 @@ Accleration = "Accleration"
Tranformation = "Tranformation"
tranformation = "tranformation"

# "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 @@ -35,6 +35,7 @@ Manufacturers
pylabrobot.mettler_toledo
pylabrobot.micronic
pylabrobot.molecular_devices
pylabrobot.opentrons
pylabrobot.qinstruments
pylabrobot.revvity
pylabrobot.sartorius
Expand Down
1 change: 1 addition & 0 deletions docs/user_guide/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ kbiosystems/index
mettler_toledo/index
micronic/index
molecular_devices/index
opentrons/index
qinstruments/index
revvity/index
sartorius/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