Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
34de0ee
Add v1b1 Micronic rack reading support
alexjamesgodfrey Apr 3, 2026
ef165aa
Add rack ID scan helper to rack reader capability
alexjamesgodfrey Apr 24, 2026
0614013
Test rack ID scan helper
alexjamesgodfrey Apr 24, 2026
0b77266
Test Micronic rack ID only scan surface
alexjamesgodfrey Apr 24, 2026
5360da6
Document Micronic rack reading capability usage
alexjamesgodfrey Apr 24, 2026
f245c95
Document rack ID only scan capability
alexjamesgodfrey Apr 24, 2026
ca6e0f6
Update Micronic driver setup for current v1b1
alexjamesgodfrey Apr 24, 2026
4d82772
Split Micronic single-tube scanning into barcode_scanning
alexjamesgodfrey Apr 24, 2026
8c4435e
Adapt Micronic split to updated PR branch
alexjamesgodfrey Apr 24, 2026
0296c42
Expose Micronic rack barcode-only scans
alexjamesgodfrey Apr 24, 2026
03f14d9
fix: consistency with micronic io and v1b1
alexjamesgodfrey Apr 24, 2026
320e599
Refactor rack_id scan into one-shot backend method
alexjamesgodfrey Apr 24, 2026
a41f59b
Format Micronic+rack-reading files and tighten driver type
alexjamesgodfrey Apr 24, 2026
62d1abf
Add direct Micronic rack reader driver
alexjamesgodfrey May 6, 2026
fb855a0
Remove bundled Micronic scanner helper
alexjamesgodfrey May 6, 2026
f3a8335
Tighten Micronic direct driver review findings
alexjamesgodfrey May 6, 2026
87312f9
Guard Micronic direct scan state transitions
alexjamesgodfrey May 6, 2026
119f3cf
Simplify Micronic direct rack reader
alexjamesgodfrey May 6, 2026
5e70699
Use PLR Serial for Micronic rack IDs
alexjamesgodfrey May 7, 2026
74bf9f2
Collapse Micronic driver abstraction
alexjamesgodfrey May 7, 2026
7e78900
Simplify Micronic public names
alexjamesgodfrey May 7, 2026
84d456a
Update rack-reading docs for Micronic names
alexjamesgodfrey May 7, 2026
65bb7ec
Remove Alakascan defaults from Micronic docs
alexjamesgodfrey May 7, 2026
08d91bb
Mock rack-reader backend in tests instead of hand-rolled fakes
rickwierenga May 15, 2026
332e759
Remove unused imports flagged by ruff
rickwierenga May 15, 2026
e969473
Make rack-reader state Micronic-specific
rickwierenga May 15, 2026
51f63fb
Drop min_wells from Micronic driver in favor of rack.num_items
rickwierenga May 15, 2026
a2662dd
Collapse rack-reading error hierarchy to MicronicError
rickwierenga May 15, 2026
cd8d7ef
Drop date and time fields from RackScanResult
rickwierenga May 15, 2026
66fd384
Type RackScanEntry.status as Literal["OK", "NOREAD"]
rickwierenga May 15, 2026
9b5b637
Log decode method instead of carrying it on RackScanEntry
rickwierenga May 15, 2026
ecc7bbb
Use PLR position convention (A1) instead of zero-padded A01
rickwierenga May 15, 2026
45e18aa
Drop MicronicDriver.get_rack_id
rickwierenga May 15, 2026
8316517
Convert rack-reading docs from md to notebook
rickwierenga May 15, 2026
a85283e
Use chatterbox in rack-reading docs and convert Micronic index to not…
rickwierenga May 15, 2026
2c32b74
Refactor Micronic scanner acquisition into Scanner classes
rickwierenga May 15, 2026
3c02bce
Remove rack_id_command subprocess escape hatch from Micronic driver
rickwierenga May 15, 2026
0d282cf
Remove rack_id_override from Micronic driver
rickwierenga May 15, 2026
b70756f
MicronicDriver owns Serial and reads the rack ID on the main loop
rickwierenga May 15, 2026
baf689b
Drop stale scan_command and image_input references from Micronic doc
rickwierenga May 15, 2026
4237796
Revert "Drop stale scan_command and image_input references from Micro…
rickwierenga May 15, 2026
63b9ef3
Remove the Micronic state machine
rickwierenga May 15, 2026
61be902
Move Micronic notebook under code_reader/ and use index.md vendor pat…
rickwierenga May 15, 2026
76df825
Reject concurrent Micronic rack scans
rickwierenga May 15, 2026
662b0b3
Drop unused default_timeout / default_poll_interval on MicronicCodeRe…
rickwierenga May 15, 2026
2162f62
Address Micronic rack reader review notes
alexjamesgodfrey May 20, 2026
765abe3
Merge remote-tracking branch 'upstream/v1b1' into micronic-rack-reade…
alexjamesgodfrey May 20, 2026
fa028d5
Align Micronic reader with v1b1 layering
alexjamesgodfrey May 20, 2026
fb24c17
Keep Micronic scan locked through timeout cleanup
alexjamesgodfrey May 21, 2026
d6298d4
Merge remote-tracking branch 'upstream/v1b1' into micronic-rack-reade…
alexjamesgodfrey May 21, 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
32 changes: 32 additions & 0 deletions docs/api/pylabrobot.capabilities.rst
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,38 @@ Barcode Scanning
BarcodeScannerBackend


Rack Reading
------------

.. currentmodule:: pylabrobot.capabilities.rack_reading.rack_reader

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

RackReader

.. currentmodule:: pylabrobot.capabilities.rack_reading.backend

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

RackReaderBackend

.. currentmodule:: pylabrobot.capabilities.rack_reading.standard

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

RackScanEntry
RackScanResult


Microscopy
----------

Expand Down
68 changes: 68 additions & 0 deletions docs/api/pylabrobot.micronic.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
.. currentmodule:: pylabrobot.micronic

pylabrobot.micronic package
===========================

Micronic integrations built on the rack-reading capability.

Device
------

.. currentmodule:: pylabrobot.micronic.code_reader.code_reader

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

MicronicCodeReader


Driver
------

.. currentmodule:: pylabrobot.micronic.code_reader.driver

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

MicronicCodeReaderDriver

.. currentmodule:: pylabrobot.micronic.code_reader.errors

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

MicronicError


Scanners
--------

.. currentmodule:: pylabrobot.micronic.code_reader.scanner

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

Scanner
TwainScanner
SaneScanner


Capabilities
------------

.. currentmodule:: pylabrobot.micronic.code_reader.rack_reading_backend

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

MicronicCodeReaderRackReadingBackend
1 change: 1 addition & 0 deletions docs/api/pylabrobot.rst
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ Manufacturers
pylabrobot.inheco
pylabrobot.liconic
pylabrobot.mettler_toledo
pylabrobot.micronic
pylabrobot.molecular_devices
pylabrobot.opentrons
pylabrobot.qinstruments
Expand Down
1 change: 1 addition & 0 deletions docs/user_guide/capabilities/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ loading-tray
pumping
weighing
barcode-scanning
rack-reading
microscopy
automated-retrieval
absorbance
Expand Down
107 changes: 107 additions & 0 deletions docs/user_guide/capabilities/rack-reading.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Rack Reading\n",
"\n",
"The `rack_reading` capability standardizes rack-scale code readers that decode a tube rack and\n",
"return structured per-position scan results.\n",
"\n",
"Unlike one-at-a-time code reads, rack reading is job-oriented and returns the full decoded rack map.\n",
"\n",
"## Walkthrough"
]
},
{
"cell_type": "code",
"metadata": {},
"source": [
"from pylabrobot.capabilities.rack_reading import RackReader\n",
"from pylabrobot.capabilities.rack_reading.chatterbox import RackReaderChatterboxBackend\n",
"from pylabrobot.resources import ResourceHolder, TubeRack, create_ordered_items_2d\n",
"\n",
"reader = RackReader(backend=RackReaderChatterboxBackend())\n",
"await reader._on_setup()"
],
"execution_count": null,
"outputs": []
},
{
"cell_type": "code",
"metadata": {},
"source": [
"rack = TubeRack(\n",
" name=\"rack\",\n",
" size_x=85.0,\n",
" size_y=127.0,\n",
" size_z=20.0,\n",
" ordered_items=create_ordered_items_2d(\n",
" ResourceHolder,\n",
" num_items_x=12,\n",
" num_items_y=8,\n",
" dx=0,\n",
" dy=0,\n",
" dz=0,\n",
" item_dx=9.0,\n",
" item_dy=9.0,\n",
" size_x=9.0,\n",
" size_y=9.0,\n",
" size_z=20.0,\n",
" ),\n",
")\n",
"\n",
"result = await reader.scan_rack(rack=rack, timeout=60.0, poll_interval=1.0)\n",
"print(result.rack_id)\n",
"print(result.entries[0].position, result.entries[0].tube_id)"
],
"execution_count": null,
"outputs": []
},
{
"cell_type": "code",
"metadata": {},
"source": [
"rack_id = await reader.scan_rack_id(timeout=60.0, poll_interval=1.0)\n",
"print(rack_id)"
],
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Public API\n",
"\n",
"- `scan_rack(rack, timeout, poll_interval)` — scan a `TubeRack` and return a `RackScanResult`. The\n",
" backend validates that the rack shape matches what the hardware supports.\n",
"- `scan_rack_id(timeout, poll_interval)` — read just the rack barcode (no per-position decoding)\n",
" and return the rack identifier.\n",
"\n",
"## Supported hardware\n",
"\n",
"```{supported-devices} rack reading\n",
"```\n",
"\n",
"## API reference\n",
"\n",
"See {class}`~pylabrobot.capabilities.rack_reading.rack_reader.RackReader` and {class}`~pylabrobot.capabilities.rack_reading.backend.RackReaderBackend`."
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"name": "python",
"version": "3.12.0"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
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 @@ inheco/index
liconic/index
mettler_toledo/index
molecular_devices/index
micronic/index
opentrons/index
qinstruments/index
thermo_fisher/index
Expand Down
6 changes: 6 additions & 0 deletions docs/user_guide/machines.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,12 @@ tr > td:nth-child(5) { width: 15%; }
|--------------|---------|-------------|--------|
| Mettler Toledo | WXS205SDU | Full | [PLR](02_analytical/scales/mettler-toledo-WXS205SDU.ipynb) / [OEM](https://www.mt.com/us/en/home/products/Industrial_Weighing_Solutions/high-precision-weigh-sensors/weigh-module-wxs205sdu-15-11121008.html) |

### Rack Readers

| Manufacturer | Machine | Features | PLR-Support | Links |
|--------------|---------|----------|-------------|--------|
| Micronic | Direct local scanner + serial control | <span class="badge badge-reading">rack reading</span> | Basics | [PLR](https://docs.pylabrobot.org/user_guide/micronic/index.html) / [OEM](https://www.micronic.com/products/code-reader/) |

---

## Understanding the Tables
Expand Down
46 changes: 46 additions & 0 deletions docs/user_guide/micronic/code_reader/hello-world.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": "# Micronic\n\nPyLabRobot includes `v1b1` Micronic integrations built on the generic\n`rack_reading` capability.\n\n`MicronicCodeReader` controls the local hardware directly. The caller picks a\n`Scanner` to acquire the rack image (`TwainScanner` on Windows, `SaneScanner` on\nLinux, or a custom `Scanner` subclass for any other acquisition stack). The driver\nhandles image acquisition and side-rack serial barcode reads; the rack-reading\nbackend decodes tube DataMatrix codes locally and returns a standard\n`RackScanResult`. It does not call Micronic Code Reader or IO Monitor, and\nPyLabRobot does not package any scanner helper executable.\n\n## Supported operations\n\nRack reading (large scanner that decodes 96 tubes plus the side rack barcode):\n\n- `rack_reading.scan_rack(rack)` to trigger image acquisition, decode all 96 tube\n positions, read the side rack barcode, and return a `RackScanResult`. The device\n currently supports 8x12 racks; passing a different shape raises `MicronicError`.\n- `rack_reading.scan_rack_id()` for a rack-barcode-only read on the side reader\n\n## Hardware example\n\nThe operator is responsible for installing any OS-level scanner bridge\n(`twain_scan`, `scanimage`, or custom scanner integration), the PLR serial extra\n(`pylabrobot[serial]`), and the local Python decode dependencies in the runtime\nenvironment."
},
{
"cell_type": "code",
"metadata": {},
"source": "from pylabrobot.micronic import MicronicCodeReader, TwainScanner\nfrom pylabrobot.resources import ResourceHolder, TubeRack, create_ordered_items_2d\n\nrack = TubeRack(\n name=\"micronic_96_well_rack\",\n size_x=85.0,\n size_y=127.0,\n size_z=20.0,\n ordered_items=create_ordered_items_2d(\n ResourceHolder,\n num_items_x=12,\n num_items_y=8,\n dx=0,\n dy=0,\n dz=0,\n item_dx=9.0,\n item_dy=9.0,\n size_x=9.0,\n size_y=9.0,\n size_z=20.0,\n ),\n)\n\nreader = MicronicCodeReader(\n scanner=TwainScanner(\n twain_scanner_path=r\"C:\\Tools\\twain_scan.exe\",\n twain_source=\"AVA6PlusG\",\n ),\n serial_port=\"COM4\",\n image_dir=r\"C:\\ProgramData\\PyLabRobot\\micronic-images\",\n keep_images=True,\n)\nawait reader.setup()\n\ntry:\n rack_result = await reader.rack_reading.scan_rack(\n rack=rack, timeout=90.0, poll_interval=1.0\n )\n print(rack_result.rack_id)\n print(len([entry for entry in rack_result.entries if entry.tube_id]))\n\n rack_id = await reader.rack_reading.scan_rack_id(timeout=5.0, poll_interval=0.5)\n print(rack_id)\nfinally:\n await reader.stop()",
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"metadata": {},
"source": "On Ubuntu/Linux, use `SaneScanner` if the scanner is exposed by a SANE backend:"
},
{
"cell_type": "code",
"metadata": {},
"source": "from pylabrobot.micronic import MicronicCodeReader, SaneScanner\n\nreader = MicronicCodeReader(\n scanner=SaneScanner(sane_device=\"avision:libusb:001:004\"),\n serial_port=\"/dev/ttyUSB0\",\n)",
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"metadata": {},
"source": "For any other acquisition stack, subclass `Scanner` and implement `acquire` to\nwrite a rack image to the given path.\n\n## Notes\n\n- `scan_rack` reads the rack ID and every tube barcode, so it typically takes\n tens of seconds. `scan_rack_id` only reads the rack barcode and completes in a\n few seconds.\n- TWAIN is a Windows scanner-driver API. PyLabRobot does not ship a TWAIN\n bridge binary and does not install one for you; pass `twain_scanner_path` to\n `TwainScanner`, set `MICRONIC_TWAIN_SCANNER_PATH`, or put a local helper\n named `twain_scan` / `twain_scan.exe` on `PATH`.\n- Ubuntu/Linux scanner control uses SANE `scanimage` (via `SaneScanner`).\n PyLabRobot does not install SANE or vendor scanner drivers. Rack-ID reads use\n `pylabrobot.io.Serial`, which is installed through the `pylabrobot[serial]`\n extra.\n- Image decoding imports `pillow`, `opencv-python-headless`, `numpy`, and\n `zxing-cpp` at runtime. Install them in the environment that runs PyLabRobot."
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"name": "python",
"version": "3.12.0"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
7 changes: 7 additions & 0 deletions docs/user_guide/micronic/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Micronic

```{toctree}
:maxdepth: 1

code_reader/hello-world
```
6 changes: 6 additions & 0 deletions pylabrobot/capabilities/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,7 @@
from .capability import Capability, CapabilityBackend, need_capability_ready
from .rack_reading import (
RackReader,
RackReaderBackend,
RackScanEntry,
RackScanResult,
)
4 changes: 4 additions & 0 deletions pylabrobot/capabilities/rack_reading/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from .backend import RackReaderBackend
from .chatterbox import RackReaderChatterboxBackend
from .rack_reader import RackReader
from .standard import RackScanEntry, RackScanResult
20 changes: 20 additions & 0 deletions pylabrobot/capabilities/rack_reading/backend.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from __future__ import annotations

from abc import ABCMeta, abstractmethod

from pylabrobot.capabilities.capability import CapabilityBackend
from pylabrobot.resources.tube_rack import TubeRack

from .standard import RackScanResult


class RackReaderBackend(CapabilityBackend, metaclass=ABCMeta):
"""Abstract backend for rack readers that decode position-indexed rack contents."""

@abstractmethod
async def scan_rack(self, rack: TubeRack, timeout: float, poll_interval: float) -> RackScanResult:
"""Scan ``rack`` and return its decoded contents."""

@abstractmethod
async def scan_rack_id(self, timeout: float, poll_interval: float) -> str:
"""Read the rack barcode only and return the rack identifier."""
32 changes: 32 additions & 0 deletions pylabrobot/capabilities/rack_reading/chatterbox.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
from __future__ import annotations

from pylabrobot.resources.barcode import Barcode
from pylabrobot.resources.tube_rack import TubeRack

from .backend import RackReaderBackend
from .standard import RackScanEntry, RackScanResult


class RackReaderChatterboxBackend(RackReaderBackend):
"""Device-free rack-reading backend for tests and examples."""

async def scan_rack(self, rack: TubeRack, timeout: float, poll_interval: float) -> RackScanResult:
del rack, timeout, poll_interval
return RackScanResult(
rack_id="CHATTERBOX",
entries=[
RackScanEntry(
position="A1",
tube_id="SIMULATED",
status="OK",
barcode=Barcode(data="SIMULATED", symbology="DataMatrix", position_on_resource="bottom"),
),
],
rack_barcode=Barcode(
data="CHATTERBOX", symbology="Code 128 (Subset B and C)", position_on_resource="right"
),
)

async def scan_rack_id(self, timeout: float, poll_interval: float) -> str:
del timeout, poll_interval
return "CHATTERBOX"
Loading