Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
73 commits
Select commit Hold shift + click to select a range
46c0125
Capability composition architecture (#931)
rickwierenga Mar 23, 2026
94b1d0c
Replace backend_kwargs with backend_params: SerializableMixin (#954)
rickwierenga Mar 23, 2026
fff620c
Move VSpin/Access2 to capability composition architecture (#956)
rickwierenga Mar 23, 2026
da085e9
Add Opentrons temperature module to new architecture (#955)
rickwierenga Mar 23, 2026
4beca0d
add P-DW-10ML-24-C part number
rickwierenga Mar 25, 2026
67ec8b4
Add BackendParams base class with autoreload-safe isinstance
rickwierenga Mar 25, 2026
a051237
Rename DeviceBackend to Driver, introduce CapabilityBackend
rickwierenga Mar 26, 2026
2e8dc3d
Split all backends into Driver + CapabilityBackend adapters
rickwierenga Mar 26, 2026
eb80265
Remove Driver from Recording and Chatterbox test backends
rickwierenga Mar 26, 2026
1a7b3ec
Remove _NullDriver/_TestDevice from capability tests
rickwierenga Mar 26, 2026
f2a241e
Fix lint, format, and mypy errors for CI compliance
rickwierenga Mar 27, 2026
0c91c9d
Add v1b1 branch to CI workflow triggers
rickwierenga Mar 27, 2026
e25431a
Add pumping capability and migrate vendor backends (#958)
rickwierenga Mar 27, 2026
ab4af3d
Refactor BioShake models from subclasses to factory functions
rickwierenga Mar 27, 2026
8afc75d
Add liquid handling capabilities (#969)
rickwierenga Mar 27, 2026
ed710c6
Add arms module with Arm/GripperArm hierarchy, Brooks and iSWAP backe…
rickwierenga Mar 27, 2026
e061346
productive work
rickwierenga Mar 27, 2026
1117f2c
Add STARAutoload class for Hamilton STAR autoload module control
rickwierenga Mar 28, 2026
619c7d5
Migrate legacy PlateReader to use capabilities
rickwierenga Mar 28, 2026
be81047
Extract STAR subsystems and generic driver methods from legacy backend
rickwierenga Mar 29, 2026
d364172
Rename get_ to request_ for all device-reading methods across codebase
rickwierenga Mar 29, 2026
fe3e577
Add multi-channel PIP methods, foil piercing, and review fixes
rickwierenga Mar 29, 2026
6758349
RFC 7386 JSON Merge Patch serialization with compact format (#885)
rickwierenga Mar 30, 2026
6f7f7b5
Add PlateWashingCapability for manifold-based plate washers (#980)
rickwierenga Mar 31, 2026
31998a7
Add bulk dispenser module with Multidrop Combi backend support (#964)
Robert-Keyser-Calico Mar 31, 2026
31933a6
Add SyringeDispensing and PeristalticDispensing capabilities
rickwierenga Apr 1, 2026
97dccdd
Migrate EL406 to Device/Driver/CapabilityBackend architecture
rickwierenga Apr 1, 2026
3854785
WIP: Migrate Multidrop Combi to Device/Driver/CapabilityBackend archi…
rickwierenga Apr 1, 2026
797451e
Migrate EL406 and BioShake docs to manufacturer-based layout
rickwierenga Apr 1, 2026
27fdbd7
Migrate EL406, BioShake, and 10 more devices to manufacturer-based docs
rickwierenga Apr 1, 2026
fe39ca8
Migrate analytical device docs to manufacturer-based layout
rickwierenga Apr 1, 2026
d307355
Migrate all material-handling devices to manufacturer-based docs
rickwierenga Apr 1, 2026
6574e39
Refine Multidrop Combi dispense workflow and document DispensingOrder
rickwierenga Apr 1, 2026
fe3acd9
Add PIPChannel class, extract STAR errors/parsing, legacy delegates t…
rickwierenga Apr 1, 2026
5563420
Migrate Hamilton STAR docs to manufacturer-based layout with new API
rickwierenga Apr 1, 2026
b1ed92c
Merge branch 'main' into v1b1
rickwierenga Apr 2, 2026
819d0e3
Fix CI, type safety, and safety-critical issues across v1b1
rickwierenga Apr 2, 2026
588abbb
Safety, correctness, and architecture improvements across v1b1
rickwierenga Apr 2, 2026
705bdd6
Port probe methods to PIPChannel, add firmware lock, fix notebooks an…
rickwierenga Apr 4, 2026
fedaea6
Add scientific traceability logging across all non-legacy backends an…
rickwierenga Apr 4, 2026
bd7a25b
Add backend_params support to all setup methods
rickwierenga Apr 4, 2026
70d55e1
Move single-channel queries to PIPChannel, fix move_y spacing safety
rickwierenga Apr 4, 2026
c18e9c4
Refactor shaker capability: duration required, HasContinuousShaking m…
rickwierenga Apr 4, 2026
eff943d
Add plate tracking to bulk dispensing capabilities
rickwierenga Apr 4, 2026
9a796e4
EL406: plate caching on driver, parameterless batch, ShakeParams
rickwierenga Apr 4, 2026
cd67533
Fix lint, type, and correctness issues across v1b1
rickwierenga Apr 6, 2026
cd628e2
Fix Liconic test model: use STX44_DC2 for humidity tests
rickwierenga Apr 6, 2026
d96e516
WIP: pickup_distance_from_bottom, arm delegation, STAR/STARLet classe…
rickwierenga Apr 7, 2026
5c20b33
WIP: Rename dispensing/washing classes, inline forwarding methods, Ba…
rickwierenga Apr 9, 2026
9d06b8d
WIP: LoadingTray capability, Cytation integration, docs
rickwierenga Apr 9, 2026
8639d64
Fix iSWAP close_gripper, improve iswap.ipynb docs
rickwierenga Apr 7, 2026
1bd8941
EL406: fix multi-volume batch lifecycle, add advanced usage docs
rickwierenga Apr 9, 2026
ef1764f
Add HasLoadingTray mixin, SpectraMax LoadingTray, direct send_command…
rickwierenga Apr 9, 2026
a4162e5
Remove aravis files (moved to separate PR #985)
rickwierenga Apr 10, 2026
af69da8
Use Aravis camera driver instead of PySpin for Cytation (#985)
vcjdeboer Apr 11, 2026
72ef784
basic implementation of uFactory xArm (#965)
Robert-Keyser-Calico Apr 13, 2026
c5e31a4
SCILA: expose drawers as LoadingTray capabilities
rickwierenga Apr 15, 2026
65c9ba4
xArm6: rename joint enum values to J1-J6
rickwierenga Apr 16, 2026
97aa2f9
Migrate Tecan Infinite 200 PRO to Device/Driver/Capability architectu…
rickwierenga Apr 17, 2026
aacf8b7
Port Hamilton Nimbus to v1b1 architecture (#986)
rickwierenga Apr 18, 2026
d3c6d0a
Require deck at driver/backend/capability construction for STAR and N…
rickwierenga Apr 18, 2026
0fa5e4e
pf kinematics
rickwierenga Apr 21, 2026
162dd2c
Expose gripper_length and gripper_z_offset on PreciseFlex400 Device
rickwierenga Apr 23, 2026
2af885b
Add Pioreactor (#893)
har1eyk Apr 25, 2026
dfacc60
STARPIPChannel.request_x_pos: delegate to left_x_arm (C0 RX), was sen…
BioCam Apr 27, 2026
28d8540
Return STAR firmware versions as datetime.date instead of strings
rickwierenga Apr 28, 2026
0909bfb
Add LI-COR Odyssey Classic to v1b1 architecture
vcjdeboer May 3, 2026
f66983d
Add DeviceCard for instrument identity / provenance metadata
vcjdeboer May 3, 2026
9d5319d
Refactor Odyssey driver to transport-only (P-06)
vcjdeboer May 3, 2026
034d87a
Tests + cross-capability orchestration cleanup
vcjdeboer May 3, 2026
7b46a17
Pre-PR readiness: lint, format, mypy, changelog, optional dep
vcjdeboer May 3, 2026
c8885f8
Remove inherited architecture changes and keep Odyssey files
rickwierenga Sep 7, 2026
8bb87af
Port Odyssey Classic to direct device API and shared HTTP transport
rickwierenga Sep 8, 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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

### Added

- LI-COR Odyssey Classic (model 9120) infrared imaging system at `pylabrobot.li_cor.odyssey`
- `StackerRetrieval` capability (`pylabrobot.capabilities.automated_retrieval.StackerRetrieval`) for sequential ("stacking access") plate storage: one or more single-ended LIFO `ResourceStack` stacks plus a loading tray, with `downstack`/`upstack` operations and a `StackerBackend` interface (plus `StackerChatterboxBackend`). Intended for devices like the Agilent BenchCel and HighRes MicroServe (#1113).
- `AutomatedRetrieval` base capability (`pylabrobot.capabilities.automated_retrieval.AutomatedRetrieval`) that owns the loading tray and the plate-movement plumbing shared by the random-access `RandomAccessRetrieval` and the sequential `StackerRetrieval`. The former random-access `AutomatedRetrieval` is now `RandomAccessRetrieval` and extends this base.
- HighRes Biosolutions MicroSpin centrifuge backend (`pylabrobot.centrifuge.highres.MicroSpinBackend`) speaking the device's ASCII command/response protocol over TCP/1000, plus a `MicroSpin(...)` factory.
Expand Down
1 change: 1 addition & 0 deletions docs/_exts/plr_devices/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ class DeviceRegistryError(ValueError):
"fan",
"flow cytometer",
"heater shaker",
"imager",
"liquid handler",
"microscope",
"peeler",
Expand Down
21 changes: 21 additions & 0 deletions docs/_static/devices.json
Original file line number Diff line number Diff line change
Expand Up @@ -2036,5 +2036,26 @@
"doc_slug": "ufactory/xarm6/hello-world",
"manager": "https://discuss.pylabrobot.org/u/rickwierenga",
"oem": "https://www.ufactory.cc/xarm-collaborative-robot/"
},
{
"id": "li-cor-odyssey-classic",
"vendor": "LI-COR",
"name": "Odyssey Classic",
"models": [
{
"name": "9120",
"status": "wip"
}
],
"kind": "imager",
"capabilities": [
"fluorescence"
],
"status": "wip",
"api": "pylabrobot.li_cor.OdysseyClassic",
"api_version": "v1",
"code_slug": "li_cor/odyssey",
"doc_slug": "li_cor/odyssey/hello-world",
"notes": "HTTP scan control and TIFF retrieval; port pending hardware verification."
}
]
23 changes: 23 additions & 0 deletions docs/api/pylabrobot.li_cor.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
.. currentmodule:: pylabrobot.li_cor.odyssey

pylabrobot.li_cor package
============================

Odyssey Classic
---------------

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

OdysseyClassic
OdysseyStatus
OdysseyChatterbox
StopResult
OdysseyError
OdysseyScanError
OdysseyImageError
OdysseyStatusError
build_identity_description
tag_tiff_with_identity
1 change: 1 addition & 0 deletions docs/api/pylabrobot.rst
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ Manufacturers
pylabrobot.inheco
pylabrobot.kbioscience
pylabrobot.kbiosystems
pylabrobot.li_cor
pylabrobot.mettler_toledo
pylabrobot.micronic
pylabrobot.molecular_devices
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 @@ -40,6 +40,7 @@ high_res/index
inheco/index
kbioscience/index
kbiosystems/index
li_cor/index
mettler_toledo/index
micronic/index
molecular_devices/index
Expand Down
7 changes: 7 additions & 0 deletions docs/user_guide/li_cor/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# LI-COR

```{toctree}
:maxdepth: 1

odyssey/hello-world
```
250 changes: 250 additions & 0 deletions docs/user_guide/li_cor/odyssey/hello-world.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,250 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "07ed52b8",
"metadata": {},
"source": [
"# LI-COR Odyssey Classic\n",
"\n",
"```{device-card} li-cor-odyssey-classic\n",
"```\n",
"\n",
"| Property | Value |\n",
"|---|---|\n",
"| Model | 9120 |\n",
"| Connection | Ethernet, HTTP Basic Auth |\n",
"| Acquisition | 700 and 800 nm fluorescence |\n",
"| Length units | millimeters, including resolution |\n",
"\n",
"**This port has not been verified on hardware.** Review the protocol and scan settings before using an instrument. These cells are not executed by the documentation build."
]
},
{
"cell_type": "markdown",
"id": "42811c1a",
"metadata": {},
"source": [
"## Connection and sample preparation\n",
"\n",
"Connect the scanner and computer to the same network and use the instrument's configured IP address. The driver uses the embedded web interface; no vendor desktop application is required. Set `ODYSSEY_HOST`, `ODYSSEY_USER`, and `ODYSSEY_PASS` in your environment. Load the sample on the scan bed and close the lid before starting acquisition.\n",
"\n",
"The optional `pylabrobot[odyssey]` extra installs Pillow for TIFF identity tagging. Instrument communication uses the standard-library HTTP transport."
]
},
{
"cell_type": "markdown",
"id": "cddf8a7b",
"metadata": {},
"source": [
"## Setup\n",
"\n",
"Open the transport and read the authenticated status page."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "4ddbaf39",
"metadata": {},
"outputs": [],
"source": [
"from pylabrobot.li_cor import OdysseyClassic\n",
"\n",
"odyssey = OdysseyClassic() # Host and credentials come from the environment.\n",
"await odyssey.setup()"
]
},
{
"cell_type": "markdown",
"id": "097e5896",
"metadata": {},
"source": [
"## Read status\n",
"\n",
"Status includes scanner state, progress in percent, remaining-time text, and the lid state when reported by the firmware."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "a6a643f3",
"metadata": {},
"outputs": [],
"source": [
"await odyssey.request_status()"
]
},
{
"cell_type": "markdown",
"id": "d035ce02",
"metadata": {},
"source": [
"## Configure the scan\n",
"\n",
"The rectangle and resolution are in millimeters. This example scans a 100 × 100 mm region at 0.169 mm resolution with both channels enabled. Configuration includes the firmware's seven-step initialization sequence."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "3ed2ca59",
"metadata": {},
"outputs": [],
"source": [
"await odyssey.configure_scan(\n",
" \"membrane_001\",\n",
" group=\"odyssey\",\n",
" width=100,\n",
" height=100,\n",
" resolution=0.169,\n",
" focus=0,\n",
" channel_700=True,\n",
" channel_800=True,\n",
")"
]
},
{
"cell_type": "markdown",
"id": "93049201",
"metadata": {},
"source": [
"## Acquire\n",
"\n",
"Start the configured scan and wait up to one hour for completion. The instrument returns to `Idle` on completion. Unknown states and firmware errors raise exceptions."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "b50d1e04",
"metadata": {},
"outputs": [],
"source": [
"final_status = await odyssey.scan(timeout=3600)\n",
"final_status"
]
},
{
"cell_type": "markdown",
"id": "4bc334ca",
"metadata": {},
"source": [
"## Download the channel TIFFs\n",
"\n",
"Each channel remains a separate image. `download_channel(group, name, 700)` retrieves only one channel; `download` retrieves both."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "b2a28342",
"metadata": {},
"outputs": [],
"source": [
"from pathlib import Path\n",
"\n",
"images = await odyssey.download(\"odyssey\", \"membrane_001\")\n",
"for channel, tiff in images.items():\n",
" Path(f\"membrane_001-{channel}.tif\").write_bytes(tiff)"
]
},
{
"cell_type": "markdown",
"id": "49f4b412",
"metadata": {},
"source": [
"## Optional instrument identity tags\n",
"\n",
"Supply the identifier for your instrument as a plain dictionary. No runtime DeviceCard object is required. The helper returns the original bytes if Pillow is unavailable or tagging fails."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "d6f18af7",
"metadata": {},
"outputs": [],
"source": [
"from pylabrobot.li_cor.odyssey import tag_tiff_with_identity\n",
"\n",
"identity = {\"name\": \"My Odyssey\"} # Add your unit's persistent identifier as \"pid\", if available.\n",
"tagged = tag_tiff_with_identity(images[700], identity, scan_name=\"membrane_001\", channel=700)\n",
"Path(\"membrane_001-700-tagged.tif\").write_bytes(tagged)"
]
},
{
"cell_type": "markdown",
"id": "aef77986",
"metadata": {},
"source": [
"## Pause or interrupt an active acquisition\n",
"\n",
"For interactive control, configure a scan and use `await odyssey.start_scan()` to start without waiting. During acquisition:\n",
"\n",
"- `await odyssey.pause_scan()` pauses; `await odyssey.start_scan()` resumes.\n",
"- `await odyssey.stop_and_save()` gracefully stops and reports available channel TIFFs.\n",
"- `await odyssey.cancel_scan()` aborts and discards partial output.\n",
"\n",
"`await odyssey.wait_until_done()` polls for completion. It requires a fresh transition out of an initial terminal state by default; use `require_fresh=False` when you know which scan you are waiting for."
]
},
{
"cell_type": "markdown",
"id": "f1c89b35",
"metadata": {},
"source": [
"## Disconnect\n",
"\n",
"Closing the transport does not stop acquisition. Use `stop_and_save()` or `cancel_scan()` first when an active scan needs to be interrupted."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "e70a0d68",
"metadata": {},
"outputs": [],
"source": [
"await odyssey.stop()"
]
},
{
"cell_type": "markdown",
"id": "7ba6783b",
"metadata": {},
"source": [
"## Chatterbox\n",
"\n",
"The simulated HTTP transport runs the same scan-control code without opening a socket. Status reads advance progress and produce synthetic one-pixel TIFFs. It does not render JPEG previews."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "f0fad940",
"metadata": {},
"outputs": [],
"source": [
"from pylabrobot.li_cor import OdysseyChatterbox\n",
"\n",
"async with OdysseyClassic(io=OdysseyChatterbox()) as simulated:\n",
" await simulated.configure_scan(\"demo\")\n",
" await simulated.scan(poll_interval=0.01)\n",
" demo_tiff = await simulated.download_channel(\"odyssey\", \"demo\", 700)"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"name": "python"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
Loading
Loading