Micronic rack reader v1b1 - #1009
Conversation
rickwierenga
left a comment
There was a problem hiding this comment.
direct machine support is awesome
|
can we use the PLR barcode class for tube or rack id? |
|
I think considering using |
…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>
…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>
6af085c to
1ae9dc6
Compare
- 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
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>
…nic doc" This reverts commit baf689b.
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>
d6298d4 to
a83dcc3
Compare
a83dcc3 to
8ac1882
Compare
There was a problem hiding this comment.
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
MicronicCodeReaderwithscan_rack/scan_rack_id, plusRackScanResult/RackScanEntry, local grid-fitting and DataMatrix decoding, and cancellation-safe scan sequencing. Scanner/TwainScanner/SaneScannerabstractions built onCommandLineTransport, and aMicronicErrortype.- 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.
Micronic provides liquid storage hardware built around barcoded vial racks. This PR adds a generic
rack_readingcapability 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:
TwainScannerwith a local TWAIN helper path or helper onPATHSaneScannerthrough SANEscanimageScannersubclassio.SerialRackScanResultwith PLRBarcodeobjects for the rack and tube barcodesWhat's in this PR
RackReadercapability (rack_reading) withscan_rackandscan_rack_idRackReaderBackend,RackScanResult, andRackScanEntryRackReaderChatterboxBackendfor docs/tests without physical hardwareMicronicCodeReaderdevice exposing therack_readingcapabilityMicronicCodeReaderDriverfor direct local image acquisition and serial barcode transportMicronicCodeReaderRackReadingBackendfor Micronic rack-reading sequencing and result constructionScanner,TwainScanner, andSaneScannerWhy 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
reader.rack_reading.scan_rack(rack)RackScanResultreader.rack_reading.scan_rack_id()io.SerialExample
Hardware validation
Live-tested with an 8x12 Micronic rack on the scanner:
io.Serial9500017722Latest direct-mode Alakascan dogfood run on May 21, 2026 succeeded through the local funnel service with scan ID
fabda2bd-c217-4d57-a68a-ccc5a05926f9, rack9500017722, 96 wells, one attempt.