Skip to content

Micronic rack reader v1b1 - #1009

Merged
rickwierenga merged 55 commits into
PyLabRobot:mainfrom
alexjamesgodfrey:micronic-rack-reader-v1b1
Aug 20, 2026
Merged

Micronic rack reader v1b1#1009
rickwierenga merged 55 commits into
PyLabRobot:mainfrom
alexjamesgodfrey:micronic-rack-reader-v1b1

Conversation

@alexjamesgodfrey

@alexjamesgodfrey alexjamesgodfrey commented Apr 25, 2026

Copy link
Copy Markdown
Contributor

Micronic provides liquid storage hardware built around barcoded vial racks. This PR adds a generic rack_reading capability and a Micronic v1b1 integration that controls the reader hardware directly through PyLabRobot, without depending on Micronic Code Reader, IO Monitor, or the IO Monitor HTTP server.

The direct path:

  • acquires the rack image through an operator-installed scanner bridge:
    • Windows: TwainScanner with a local TWAIN helper path or helper on PATH
    • Linux/Ubuntu: SaneScanner through SANE scanimage
    • custom: any user-provided Scanner subclass
  • decodes tube Data Matrix codes locally with Python image/decode dependencies
  • reads the side rack barcode through PLR io.Serial
  • returns a standard RackScanResult with PLR Barcode objects for the rack and tube barcodes

What's in this PR

  • RackReader capability (rack_reading) with scan_rack and scan_rack_id
  • RackReaderBackend, RackScanResult, and RackScanEntry
  • RackReaderChatterboxBackend for docs/tests without physical hardware
  • MicronicCodeReader device exposing the rack_reading capability
  • MicronicCodeReaderDriver for direct local image acquisition and serial barcode transport
  • MicronicCodeReaderRackReadingBackend for Micronic rack-reading sequencing and result construction
  • Local Data Matrix image decoding for the 8x12 / 96-position Micronic rack layout
  • Scanner abstractions: Scanner, TwainScanner, and SaneScanner
  • Docs: generic rack-reading guide plus Micronic code-reader guide
  • Unit tests for rack-reading behavior, chatterbox behavior, scanner command selection, PLR serial rack-ID reads, and device wiring
  • Live smoke-tested on Micronic hardware with an 8x12 rack: rack ID read plus full 96-vial scan

Why not IO Monitor?

Micronic's IO Monitor HTTP server can trigger rack scans and return results, but relying on that server means PLR is not actually controlling the hardware. This PR keeps the v1b1 shape and moves hardware ownership into PLR itself.

PLR does not ship a TWAIN helper executable, SANE, scanner drivers, or the image-decoding packages as mandatory dependencies. The operator installs the OS-level scanner bridge, the PLR serial extra (pylabrobot[serial]), and the Python decode dependencies in the runtime environment.

Supported operations

Operation PLR API Implementation
Full rack scan reader.rack_reading.scan_rack(rack) read side rack barcode, acquire scanner image, decode tube Data Matrix codes, return RackScanResult
Rack barcode only reader.rack_reading.scan_rack_id() read side barcode reader over PLR io.Serial

Example

from pylabrobot.micronic import MicronicCodeReader, TwainScanner
from pylabrobot.resources import ResourceHolder, TubeRack, create_ordered_items_2d

rack = TubeRack(
  name="micronic_96_well_rack",
  size_x=85.0,
  size_y=127.0,
  size_z=20.0,
  ordered_items=create_ordered_items_2d(
    ResourceHolder,
    num_items_x=12,
    num_items_y=8,
    dx=0,
    dy=0,
    dz=0,
    item_dx=9.0,
    item_dy=9.0,
    size_x=9.0,
    size_y=9.0,
    size_z=20.0,
  ),
)

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

await reader.setup()
try:
  result = await reader.rack_reading.scan_rack(rack=rack, timeout=90.0, poll_interval=1.0)
  print(result.rack_id)
  print(len([entry for entry in 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()

Hardware validation

Live-tested with an 8x12 Micronic rack on the scanner:

  • rack barcode-only read succeeded over PLR io.Serial
  • full rack scan succeeded through PLR direct hardware control
  • decoded all 96 tube positions
  • returned physical rack ID 9500017722
  • no Micronic Code Reader / IO Monitor HTTP server dependency

Latest direct-mode Alakascan dogfood run on May 21, 2026 succeeded through the local funnel service with scan ID fabda2bd-c217-4d57-a68a-ccc5a05926f9, rack 9500017722, 96 wells, one attempt.

@rickwierenga rickwierenga left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

direct machine support is awesome

Comment thread pylabrobot/micronic/code_reader/driver.py
@rickwierenga

Copy link
Copy Markdown
Member

can we use the PLR barcode class for tube or rack id?

@rickwierenga

Copy link
Copy Markdown
Member

I think considering using Barcode and please testing if I didn't break anything by running the hello world notebook are the last things needed before merging :)

alexjamesgodfrey added a commit to alexjamesgodfrey/pylabrobot that referenced this pull request Jul 1, 2026
…cted transport

Follows Rick Wierenga's v1b1 transport guidance (PRs PyLabRobot#1040/PyLabRobot#1009): a driver that
spans two wires holds an injected transport object of the right io class rather
than subclassing the driver per wire (the Micronic `Scanner`/`TwainScanner`/
`SaneScanner` shape). Replaces the previous `MultidropCombiDriverBase` +
per-wire driver subclasses with:

- ONE `MultidropCombiDriver(Driver)` — wire-agnostic; owns the shared ASCII
  protocol (command framing, END/status parsing, error table, queries, device
  ops) and an injected `MultidropCombiTransport`.
- `MultidropCombiTransport` ABC + `MultidropCombiSerialTransport` (io.Serial) and
  `MultidropCombiHidTransport` (io.HID, length-prefixed 64-byte reports). Each
  wraps a concrete pylabrobot.io transport (no speculative io-class injection,
  per Rick's PyLabRobot#1040 review).

`MultidropCombi` (serial) and `MultidropCombiNl` (HID) now build the same driver
with the matching transport. `PeristalticDispensing8` + the backend are unchanged
and fully transport-agnostic. Driver serialize/deserialize round-trips the
transport.

Volume calibration: verified against the Combi nL manual spec (0.5-2500 uL) and
the live REP dump that the nL shares the Combi's 1/10-uL command units, so no
unit recalibration is warranted (per-cassette pump curves are firmware-internal,
REP section 9). Introducing a per-device volume spec would be speculative
parameterization (Rick PyLabRobot#1040), so `_ul_to_tenths` is left as-is.

Tests: existing serial Combi tests updated for transport construction; added
`nl_transport_tests.py` (HID length-prefix write, multi-report reassembly,
error status, long-command chunking). 84 pass (1 pre-existing pytest /dev/null
conftest env error, unrelated). Re-verified read-only on real Combi nL hardware
(VER/REP/LOG) through the agnostic driver.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
alexjamesgodfrey added a commit to alexjamesgodfrey/pylabrobot that referenced this pull request Jul 2, 2026
…cted transport

Follows Rick Wierenga's v1b1 transport guidance (PRs PyLabRobot#1040/PyLabRobot#1009): a driver that
spans two wires holds an injected transport object of the right io class rather
than subclassing the driver per wire (the Micronic `Scanner`/`TwainScanner`/
`SaneScanner` shape). Replaces the previous `MultidropCombiDriverBase` +
per-wire driver subclasses with:

- ONE `MultidropCombiDriver(Driver)` — wire-agnostic; owns the shared ASCII
  protocol (command framing, END/status parsing, error table, queries, device
  ops) and an injected `MultidropCombiTransport`.
- `MultidropCombiTransport` ABC + `MultidropCombiSerialTransport` (io.Serial) and
  `MultidropCombiHidTransport` (io.HID, length-prefixed 64-byte reports). Each
  wraps a concrete pylabrobot.io transport (no speculative io-class injection,
  per Rick's PyLabRobot#1040 review).

`MultidropCombi` (serial) and `MultidropCombiNl` (HID) now build the same driver
with the matching transport. `PeristalticDispensing8` + the backend are unchanged
and fully transport-agnostic. Driver serialize/deserialize round-trips the
transport.

Volume calibration: verified against the Combi nL manual spec (0.5-2500 uL) and
the live REP dump that the nL shares the Combi's 1/10-uL command units, so no
unit recalibration is warranted (per-cassette pump curves are firmware-internal,
REP section 9). Introducing a per-device volume spec would be speculative
parameterization (Rick PyLabRobot#1040), so `_ul_to_tenths` is left as-is.

Tests: existing serial Combi tests updated for transport construction; added
`nl_transport_tests.py` (HID length-prefix write, multi-report reassembly,
error status, long-command chunking). 84 pass (1 pre-existing pytest /dev/null
conftest env error, unrelated). Re-verified read-only on real Combi nL hardware
(VER/REP/LOG) through the agnostic driver.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@rickwierenga
rickwierenga force-pushed the v1b1 branch 2 times, most recently from 6af085c to 1ae9dc6 Compare August 1, 2026 20:32
alexjamesgodfrey and others added 20 commits August 17, 2026 14:49
- Replace abstract trigger_rack_id_scan() with composite scan_rack_id(timeout, poll_interval) -> str
- Capability layer delegates directly; backend chooses one-shot vs trigger+poll
- Micronic backend now uses GET /rackid (one-shot) instead of POST /scantube
  (which is the single-tube scanner, not the rack-barcode reader)
- Update chatterbox, tests, and docs to match
- ruff format on rack_reader.py, driver.py, micronic_tests.py
- annotate response.read() result so mypy --check-untyped-defs is clean
rickwierenga and others added 14 commits August 17, 2026 14:51
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ebook

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace MicronicDriver's flat twain_scanner_path/sane_device/scan_command/
scanner_backend/image_extension/image_input params with a single scanner: Scanner
argument. Concrete subclasses TwainScanner and SaneScanner own their own
resolution and extension. For other acquisition stacks, users subclass Scanner
directly.

- New: pylabrobot/micronic/code_reader/scanner.py (Scanner ABC + TwainScanner +
  SaneScanner) and errors.py (MicronicError moved here to break the import cycle).
- MicronicDriver.__init__ takes scanner + required serial_port (no more "COM4"
  default) plus a smaller flat-param surface.
- MicronicCodeReader matches: scanner + serial_port required, no driver injection.
- Module-level run_scan / choose_image_extension / normalize_* / resolve_* helpers
  in driver.py are deleted; what remains is owned by the scanner classes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the standalone read_rack_id_plr_serial / read_rack_id / extract_rack_id
trio with a single scan_rack_id method on the driver. The driver holds the
Serial instance, opens it in setup(), closes it in stop(), and reads the side
barcode through self.io.

trigger_rack_scan now awaits scan_rack_id() before kicking off the executor
thread, so the sync _scan_rack_blocking receives the rack ID as an argument
and no longer bridges sync/async via asyncio.run.

serial_port is no longer stored on the driver (the Serial instance owns the
connection info).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
trigger_rack_scan / get_rack_reader_state / get_scan_result and the
MicronicRackReaderState enum existed only to expose the executor Future as a
poll-and-finalize state machine. Replace them with a single async scan_rack
method on the driver that awaits the executor directly. The backend collapses
to asyncio.wait_for, the stale-DATAREADY workaround disappears, and the driver
loses _state / _scan_task / _scan_error / _reported_scanning_since_trigger /
_expected_well_count.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…tern

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The state-machine removal dropped the "already in progress" guard. Replace it
with an asyncio.Lock + locked() check that refuses overlapping scan_rack calls
rather than queuing them, since the underlying hardware can only run one scan
at a time.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ader

poll_interval became dead weight when the state machine went away; nothing
polls now. default_timeout was misleading: it only fed the scanner subprocess
timeout, never any rack-reading default. Rename the ctor arg to
scanner_timeout (its real meaning), drop both stored attributes, and drop the
serialize override since Device.serialize already emits the driver state.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@rickwierenga
rickwierenga force-pushed the micronic-rack-reader-v1b1 branch from d6298d4 to a83dcc3 Compare August 17, 2026 21:52
@rickwierenga
rickwierenga force-pushed the micronic-rack-reader-v1b1 branch from a83dcc3 to 8ac1882 Compare August 17, 2026 22:11
@rickwierenga
rickwierenga changed the base branch from v1b1 to main August 17, 2026 22:12

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds a new pylabrobot.micronic manufacturer package with a direct integration for the Micronic rack code reader that does not depend on Micronic's OEM software (Code Reader / IO Monitor). A single MicronicCodeReader class reads the side rack barcode over PLR io.Serial, acquires a rack image through a pluggable Scanner (TWAIN on Windows, SANE on Linux, or custom) via CommandLineTransport, and decodes the 96 tube DataMatrix codes locally (OpenCV/NumPy/Pillow/zxing-cpp, imported lazily). It also wires the device into the docs registry and adds a hello-world notebook plus unit tests.

Changes:

  • New MicronicCodeReader with scan_rack / scan_rack_id, plus RackScanResult/RackScanEntry, local grid-fitting and DataMatrix decoding, and cancellation-safe scan sequencing.
  • Scanner/TwainScanner/SaneScanner abstractions built on CommandLineTransport, and a MicronicError type.
  • Docs: registry entry, API rst, user-guide index, and a hello-world notebook; unit tests for scanners, serial rack-ID reads, decode helpers, and device wiring.

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
pylabrobot/micronic/code_reader/driver.py Core MicronicCodeReader, result types, serial rack-ID read, and image decode/grid-fit logic.
pylabrobot/micronic/code_reader/scanner.py Scanner/TwainScanner/SaneScanner image-acquisition helpers over CommandLineTransport.
pylabrobot/micronic/code_reader/errors.py MicronicError exception.
pylabrobot/micronic/code_reader/__init__.py, pylabrobot/micronic/__init__.py Public re-exports for the package.
pylabrobot/micronic/code_reader/micronic_tests.py Unit tests for scanners, serial reads, decode helpers, and scan flow.
docs/_static/devices.json Registry entry for the Micronic Code Reader.
docs/api/pylabrobot.micronic.rst, docs/api/pylabrobot.rst API reference pages/toctree.
docs/user_guide/micronic/index.md, docs/user_guide/index.md, docs/user_guide/micronic/code_reader/hello-world.ipynb User guide index and hello-world notebook.

Key review notes: the PR description's rack_reading capability/backend/chatterbox architecture and poll_interval API do not match the implemented monolithic MicronicCodeReader (comment 001); requiring all rack.num_items (96) positions to decode contradicts the documented NOREAD partial-read behavior (comment 002); and cluster_axis duplicates fitted_axis (comment 003, optional). The decode logic and cancellation handling are internally consistent and well-tested; the heavy image-processing helpers are untested but require real image/decode libraries not available in the test environment.


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread pylabrobot/micronic/code_reader/driver.py
Comment thread pylabrobot/micronic/code_reader/driver.py Outdated
Comment thread pylabrobot/micronic/code_reader/driver.py Outdated
@rickwierenga
rickwierenga merged commit 9d3f2ae into PyLabRobot:main Aug 20, 2026
63 of 71 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants