Micronic rack reader v1b1#1009
Open
alexjamesgodfrey wants to merge 45 commits into
Open
Conversation
- 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
reviewed
May 6, 2026
Member
rickwierenga
left a comment
There was a problem hiding this comment.
direct machine support is awesome
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The generic RackReader capability had a state machine (IDLE/SCANNING/DATAREADY) plus trigger+poll+result orchestration that only made sense for Micronic's hardware. Move that into the Micronic backend and narrow the generic capability to scan_rack(rack, ...) + scan_rack_id(...), mirroring how the plate-reader capability takes a Plate. The driver validates rack shape (8x12) and raises MicronicError on mismatch. - RackReaderState -> MicronicRackReaderState in pylabrobot/micronic - MicronicRackReadingBackend.scan_rack owns trigger+poll+stale-DATAREADY guard - Backend raises plain TimeoutError on poll timeout (no custom timeout class) - Drop LayoutInfo, get_layouts/get_current_layout/set_current_layout - Drop _on_setup probe (it only read a Python attribute) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Now that trigger_rack_scan takes a TubeRack, the expected well count comes from the rack itself instead of a separate constructor parameter. Stores rack.num_items on the driver when a scan is triggered and uses it as the decode threshold. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drop RackReaderError (generic base) and MicronicRackReaderError (multi-inherited wrapper). The Micronic backend no longer catch/re-raises driver errors at every call site; MicronicError propagates as-is. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Callers can timestamp scans themselves; carrying these as result fields adds no information beyond what wall-clock at scan time would give. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Normalize chatterbox/test fixture "Code OK" to "OK" so the type closes on the two values the Micronic driver actually emits. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Micronic decoder cascade (full-image -> crop-N -> perspective-N) is a debugging detail, not part of the rack scan result. Emit it at DEBUG and drop free_text from the dataclass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Matches ItemizedResource identifiers and the split_identifier contract so RackScanEntry.position can be passed directly to rack.children-by-identifier lookups. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The cached-or-fresh accessor only existed to back the dropped capability-level get_rack_id and has no remaining callers. Remove the method and its test. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Member
|
can we use the PLR barcode class for tube or rack id? |
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>
Member
|
I think considering using |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Micronic provides liquid storage solutions in the form of barcoded vials, racks for the vials, and rack readers that read all vial Data Matrix codes plus the rack barcode.
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 software, IO Monitor, or its HTTP server controlled by the software.The direct path:
PATHscanimagescan_commandio.SerialRackScanResultWhat's in this PR
RackReadercapability (rack_reading) withscan_rack,scan_rack_id, layout management, and state pollingMicronicCodeReaderdevice exposing therack_readingcapabilityMicronicDriverfor direct local scanner + serial rack barcode controlio.Serialrack barcode read path for the side barcode readerdocs/user_guide/capabilities/rack-reading.md,docs/user_guide/micronic/index.mdWhy 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, but moves hardware ownership into PLR itself.
PLR does not ship a TWAIN helper executable, SANE, or scanner drivers. The operator is responsible for installing the OS-level scanner bridge and Python decode dependencies in their runtime environment. For example I hd to install a driver for the COM4 barcode scanner that came with my rack reader.
Supported operations
reader.rack_reading.scan_rack()reader.rack_reading.scan_rack_id()io.Serialreader.rack_reading.get_state()reader.rack_reading.get_layouts()8x12/96get_current_layout()/set_current_layout()8x12,96(8x12),96Example
Hardware validation
Live-tested with an 8x12 Micronic rack (rd235) on the scanner: