Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
17 changes: 17 additions & 0 deletions docs/api/pylabrobot.opentrons.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
.. currentmodule:: pylabrobot.opentrons

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

Flex
----

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

OpentronsRobot
OpentronsFlex
OpentronsError
PipetteInfo
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
}
7 changes: 7 additions & 0 deletions docs/user_guide/opentrons/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Opentrons

```{toctree}
:maxdepth: 1

flex/hello-world
```
17 changes: 17 additions & 0 deletions pylabrobot/opentrons/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from pylabrobot.opentrons.flex import OpentronsFlex
from pylabrobot.opentrons.flex_head import FlexHead1, FlexHead8, FlexHead96
from pylabrobot.opentrons.robot import OpentronsError, OpentronsRobot, PipetteInfo
from pylabrobot.opentrons.transport import ChatterboxTransport, HttpxTransport, OpentronsTransport

__all__ = [
"ChatterboxTransport",
"FlexHead1",
"FlexHead8",
"FlexHead96",
"HttpxTransport",
"OpentronsError",
"OpentronsFlex",
"OpentronsRobot",
"OpentronsTransport",
"PipetteInfo",
]
Loading
Loading