Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 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
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
36 changes: 36 additions & 0 deletions docs/api/pylabrobot.capabilities.rst
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,42 @@ 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:

RackReaderState
RackReaderError
RackReaderTimeoutError
RackScanEntry
RackScanResult
LayoutInfo


Microscopy
----------

Expand Down
46 changes: 46 additions & 0 deletions docs/api/pylabrobot.micronic.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
.. 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:

MicronicDriver
MicronicError


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

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

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

MicronicRackReadingBackend
MicronicRackReaderError
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
55 changes: 55 additions & 0 deletions docs/user_guide/capabilities/rack-reading.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Rack Reading

The `rack_reading` capability standardizes rack-scale code readers that trigger a rack scan,
report normalized state while scanning, and return structured per-position scan results.

Unlike one-at-a-time code reads, rack reading is job-oriented and returns the full decoded rack map.

## Public API

```python
from pylabrobot.capabilities.rack_reading import RackReader

result = await reader.scan_rack(timeout=60.0, poll_interval=1.0)
```

`scan_rack()` is the main public operation. It triggers the scan, waits internally until the
reader reaches `dataready`, and then returns a `RackScanResult`.

If the hardware supports reading just the rack barcode without decoding all tube positions,
`scan_rack_id()` exposes that as a rack-reading operation and returns the rack identifier only.

Lower-level methods are also available:

- `get_state()`
- `wait_for_data_ready()`
- `trigger_rack_scan()`
- `scan_rack_id()`
- `get_scan_result()`
- `get_rack_id()`
- `get_layouts()`
- `get_current_layout()`
- `set_current_layout(layout)`

## Example With Micronic

```python
from pylabrobot.micronic import MicronicCodeReader

reader = MicronicCodeReader(
scanner_backend="sane",
sane_device="avision:libusb:001:004",
serial_port="/dev/ttyUSB0",
)
await reader.setup()

try:
result = await reader.rack_reading.scan_rack(timeout=90.0, poll_interval=1.0)
print(result.rack_id)
print(result.entries[0].position, result.entries[0].tube_id)

rack_id = await reader.rack_reading.scan_rack_id(timeout=60.0, poll_interval=1.0)
print(rack_id)
finally:
await reader.stop()
```
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
85 changes: 85 additions & 0 deletions docs/user_guide/micronic/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# Micronic

PyLabRobot includes `v1b1` Micronic integrations built on the generic
`rack_reading` capability.

`MicronicCodeReader` controls the local hardware directly. It acquires the rack
image through a configured scanner command, a Windows TWAIN helper available on
PATH, or Ubuntu/Linux SANE `scanimage`; reads the side rack barcode through the
serial reader; decodes tube DataMatrix codes locally; and returns a standard
`RackScanResult`. It does not call Micronic Code Reader or IO Monitor, and
PyLabRobot does not package any scanner helper executable.

## Supported operations

Rack reading (large scanner that decodes 96 tubes plus the side rack barcode):

- `rack_reading.scan_rack()` to trigger image acquisition, decode all 96 tube
positions, read the side rack barcode, and return a `RackScanResult`
- `rack_reading.scan_rack_id()` for a rack-barcode-only read on the side reader
- `rack_reading.get_layouts()`, `get_current_layout()`, and
`set_current_layout()` for the fixed 8x12 rack layout

## Hardware example

The operator is responsible for installing any OS-level scanner bridge
(`twain_scan`, `scanimage`, or a custom command), the PLR serial extra
(`pylabrobot[serial]`), and the local Python decode dependencies in the runtime
environment.

```python
from pylabrobot.micronic import MicronicCodeReader

reader = MicronicCodeReader(
scanner_backend="twain",
twain_scanner_path=r"C:\Tools\twain_scan.exe",
twain_source="AVA6PlusG",
image_dir=r"C:\ProgramData\PyLabRobot\micronic-images",
serial_port="COM4",
keep_images=True,
)
await reader.setup()

try:
rack_result = await reader.rack_reading.scan_rack(timeout=90.0, poll_interval=1.0)
print(rack_result.rack_id)
print(len([entry for entry in rack_result.entries if entry.tube_id]))

rack_id = await reader.rack_reading.scan_rack_id(timeout=5.0, poll_interval=0.5)
print(rack_id)
finally:
await reader.stop()
```

On Ubuntu/Linux, use SANE if the scanner is exposed by a SANE backend:

```python
reader = MicronicCodeReader(
scanner_backend="sane",
sane_device="avision:libusb:001:004",
serial_port="/dev/ttyUSB0",
image_extension="tiff",
)
```

For any other acquisition stack, pass `scan_command`. Each command argument is
formatted with `{output_path}`, `{timeout_ms}`, `{twain_source}`, and
`{sane_device}` before execution. The command must write the rack image to
`{output_path}`.

## Notes

- `scan_rack` reads every tube barcode and finishes by reading the rack ID, so
it typically takes tens of seconds. `scan_rack_id` only reads the rack
barcode and completes in a few seconds.
- TWAIN is a Windows scanner-driver API. PyLabRobot does not ship a TWAIN
bridge binary and does not install one for you; configure
`twain_scanner_path`, set `MICRONIC_TWAIN_SCANNER_PATH`, or put a local helper
named `twain_scan`/`twain_scan.exe` on PATH when using the `twain` backend.
- Ubuntu/Linux scanner control should use SANE `scanimage` or a custom
`scan_command`. PyLabRobot does not install SANE or vendor scanner drivers.
Rack-ID reads use `pylabrobot.io.Serial`, which is installed through the
`pylabrobot[serial]` extra.
- Image decoding imports `pillow`, `opencv-python-headless`, `numpy`, and
`zxing-cpp` at runtime. Install them in the environment that runs PyLabRobot.
- Use `image_input` for offline decode checks without touching scanner hardware.
10 changes: 10 additions & 0 deletions pylabrobot/capabilities/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,11 @@
from .capability import Capability, CapabilityBackend, need_capability_ready
from .rack_reading import (
LayoutInfo,
RackReader,
RackReaderBackend,
RackReaderError,
RackReaderState,
RackReaderTimeoutError,
RackScanEntry,
RackScanResult,
)
11 changes: 11 additions & 0 deletions pylabrobot/capabilities/rack_reading/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from .backend import RackReaderBackend
from .chatterbox import RackReaderChatterboxBackend
from .rack_reader import RackReader
from .standard import (
LayoutInfo,
RackReaderError,
RackReaderState,
RackReaderTimeoutError,
RackScanEntry,
RackScanResult,
)
48 changes: 48 additions & 0 deletions pylabrobot/capabilities/rack_reading/backend.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
from __future__ import annotations

from abc import ABCMeta, abstractmethod

from pylabrobot.capabilities.capability import CapabilityBackend

from .standard import LayoutInfo, RackReaderState, RackScanResult


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

@abstractmethod
async def get_state(self) -> RackReaderState:
"""Return the current rack reader state."""

@abstractmethod
async def trigger_rack_scan(self) -> None:
"""Initiate a rack-wide scan."""

@abstractmethod
async def scan_rack_id(self, timeout: float, poll_interval: float) -> str:
"""Perform a rack-barcode-only scan and return the rack identifier.

Backends whose hardware exposes a one-shot rack-id read may ignore
``timeout`` and ``poll_interval``; backends that need a trigger/poll cycle
should respect them.
"""

@abstractmethod
async def get_scan_result(self) -> RackScanResult:
"""Return the most recent rack scan result."""

@abstractmethod
async def get_rack_id(self) -> str:
"""Return the rack identifier reported by the scanner."""

@abstractmethod
async def get_layouts(self) -> list[LayoutInfo]:
"""Return supported layouts."""

@abstractmethod
async def get_current_layout(self) -> str:
"""Return the active layout."""

@abstractmethod
async def set_current_layout(self, layout: str) -> None:
"""Set the active layout."""
44 changes: 44 additions & 0 deletions pylabrobot/capabilities/rack_reading/chatterbox.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
from __future__ import annotations

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


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

def __init__(self):
self._state = RackReaderState.IDLE
self._layout = "96"

async def get_state(self) -> RackReaderState:
return self._state

async def trigger_rack_scan(self) -> None:
self._state = RackReaderState.DATAREADY

async def scan_rack_id(self, timeout: float, poll_interval: float) -> str:
self._state = RackReaderState.DATAREADY
return "CHATTERBOX"

async def get_scan_result(self) -> RackScanResult:
return RackScanResult(
rack_id="CHATTERBOX",
date="19700101",
time="000000",
entries=[
RackScanEntry(position="A01", tube_id="SIMULATED", status="Code OK"),
],
)

async def get_rack_id(self) -> str:
return "CHATTERBOX"

async def get_layouts(self) -> list[LayoutInfo]:
return [LayoutInfo(name="96"), LayoutInfo(name="48")]

async def get_current_layout(self) -> str:
return self._layout

async def set_current_layout(self, layout: str) -> None:
self._layout = layout
Loading