Add Agilent BenchCel 4R storage backend#1109
Open
c-reiter wants to merge 2 commits into
Open
Conversation
c8c4eeb to
b07c7dc
Compare
e553767 to
6e203e6
Compare
Follow-up to PyLabRobot#1110. When stacking bare plates in the z direction, a plate placed directly on another bare plate now nests in by `size_z - stacking_z_height` instead of resting at the lower plate's full height, so a stack of N identical plates is `size_z + (N - 1) * stacking_z_height` tall (both `get_size_z()` and child placement). Gated on the value being set and skipped when the lower plate wears a lid, so existing behaviour is unchanged for plates without a `stacking_z_height`. This parallels the lid-nesting `ResourceStack` already did. Also includes the documentation for `stacking_z_height` (the Plate resource page, the ResourceStack notebook, and the example definition in the contributor guide) that was part of the original PyLabRobot#1110 work. These changes were force-pushed to the PyLabRobot#1110 branch shortly after it was already merged, so they never made it into main; this PR brings them in. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1c0de90 to
1094a06
Compare
Adds an integration for the Agilent BenchCel 4R microplate handler, modeled as a PLR storage machine, plus an in-process mock TCP server that emulates the device's binary wire protocol for hardware-free development. - BenchCel4RBackend speaks the reverse-engineered framed binary protocol over TCP/7612 (reverse-engineered from VWorks captures + live tests). - BenchCel4R(...) factory returning a plain Incubator (it takes a name like all PLR device factories, since the returned Resource needs a unique name for the resource tree/serialization/lookups). - Regular motion/transfer APIs (home, move, pick/place, downstack/upstack, jog, gripper moves, load/unload, save_teachpoint, fetch/take_in_plate, ...) are unprefixed. Only opening the stacker clamps is exposed as dangerously_open_stacker_grippers, because that specific diagnostic can release/drop a plate stack. - Calculation-based labware integration: derive BenchCel geometry (StackingThickness, RobotGripperOffset, StackerGripperOffset, SensorOffset) from PLR Plate dimensions instead of bundling per-catalog XML profiles. - set_labware(...) pushes labware geometry to the device using the decoded 0x7d settings frame + 0x9f commit handshake. The 0x7d payload layout was reverse-engineered from VWorks packet captures and reproduces the captured frames byte-for-byte for standard flat microplates (StackingThickness, robot/stacker/sensor gripper offsets, full plate height at offset 37, notch flags, thresholds, and PlatePresenceThreshold at offset 75). Invalid geometry is rejected by the device with a 0x02 "labware gripper positions are too close" error, which is surfaced as BenchCelDeviceError. - BenchCelMockServer + tests (stub-based and real-TCP integration), including the 0x7d/0x9f settings handshake and its rejection path. - User guide + API docs. No files are written on the host running PLR: the device cannot read teachpoints back, but PLR does not persist any local teachpoint registry/files. Keep the numeric teachpoints you write in your own protocol/config if you need them. Known limitations / not yet covered: - Lidded and sealed plate configurations are NOT fully supported. In every captured VWorks 0x7d frame for standard flat microplates, the lidded/sealed sub-fields (lidded stacking thickness / plate thickness / resting height / gripper offset / gripper position / departure height, and sealed stacking thickness / plate thickness) plus ErrorCorrectionOffset were always zero, so their exact wire offsets could not be confirmed. They are therefore encoded as zero by to_device_payload() and are not decoded by from_device_payload(). - As a result, set_labware() should not be used for lidded or sealed labware yet, and there are no tests covering lidded/sealed geometry round-trips. Pinning these down needs captures where a lidded plate (e.g. a tip box with lid) and the sealed Seahorse plate are applied in VWorks with non-zero lidded/sealed values. - The robot/stacker/sensor gripper offsets calculated from PLR Plate dimensions are heuristics validated against standard SBS microplates; they are not part of the PLR labware definition and should be verified/overridden for unusual labware before pushing to hardware.
1094a06 to
804c132
Compare
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.
Add Agilent BenchCel 4R storage backend
This PR adds an integration for the Agilent BenchCel 4R microplate handler, modeled as a PyLabRobot storage machine, plus an in-process mock TCP server that emulates the device's binary wire protocol for hardware-free development.
It follows the usual PLR driver structure: a reverse-engineered backend, a faithful mock server, hardware-free tests, and a user guide.
What's added
Backend —
pylabrobot.storage.agilent.BenchCel4RBackend[1 byte command_id][2 byte LE payload_length][payload].IncubatorBackend, since the BenchCel is open, sequential stacker storage. The firmware addresses whole stackers, so PLR racks track the expected plate order/content per stacker.0x47/0x48), stacker load/unload (0x60/0x61), pick/downstack + place/upstack (0x62/0x63), move-to-target (0x65), relative jog (0x66), pneumatic stacker clamps (0x67), full-open/close robot grippers (0x6a), save teachpoint (0x73), labware-settings push + commit (0x7d/0x9f), stacker sensors (0x7e), current position (0x85), general/arm status (0x87), axis bounds (0x99), and device-error frames (0x02).request_stacker_sensors,request_all_stacker_sensors,request_arm_status,request_general_status,request_axis_bounds,request_current_position.source_ipto bind a specific local NIC on multi-homed control hosts.Narrowed
dangerously_APIOnly the one genuinely dangerous operation is prefixed:
dangerously_open_stacker_grippers(0x67open), because opening the pneumatic stacker clamps can release/drop a plate stack if it isn't physically supported. Everything else —home,move_to_stacker,downstack_plate/upstack_plate,load_stacker/unload_stacker,jog/move_x/move_z/rotate_theta,fully_open_grippers/fully_close_grippers,close_stacker_grippers,save_teachpoint,set_labware, and theIncubatortransfer hooks — are regular methods. (An earlier revision prefixed nearly every motion method; that was narrowed to just the clamp-open command on review feedback.)home_motors(0x47) drops the TCP control session while homing; the backend reconnects and polls0x87until the device is responsive again.Factory + storage resource
BenchCel4R(name, host, ...)builds the backend plus four generic stacker racks and returns a plainIncubator(the BenchCel reuses PLR's only storage abstraction; no custom subclass). Thenameis required because every PLRResourceneeds a unique name for the resource tree, serialization, and lookups — same convention as other device factories.loading_tray_teachpoint_idtherefore defaults toNone, andfetch_plate_to_loading_tray/take_in_plateraise a clear error if no teachpoint is configured (or passed per call viateachpoint_id=). The teachpoint must already be taught on the device (VWorks orsave_teachpoint).Incubator.loading_tray/loading_tray_location(a fixedCoordinate) is cosmetic only — PLR resource-tree/visualization bookkeeping. Actual robot motion is driven entirely by the teachpoint ID.VWorks button → opcode mapping (confirmed from captures)
Dedicated packet captures of the VWorks stacker buttons confirm the reverse-engineered mapping byte-for-byte (examples for stacker 3 = zero-based target
0x02):0x62(robot pick)01 02 00 010x69 620x63(robot place)01 02 00 010x69 630x60(stacker mech)01 020x69 60 020x61(stacker mech)01 02 00 00 00 000x69 61 020x67<idx> 01/<idx> 000x69 67So
0x62/0x63are the VWorks Downstack/Upstack tasks (per-plate robot pick/place at a stacker target; with a teachpoint target they're a plain pick/place there), and0x60/0x61are the separate whole-stacker Load/Unload mechanism commands behind the Load/Unload buttons — now exposed asload_stacker/unload_stacker(renamed fromload_plate/unload_plateto reflect that they drive the stacker, not a single plate). A regression test (test_stacker_primitives_match_vworks_captures) locks in these exact wire bytes.One command observed in a capture but not yet confidently mapped is
0x71(payload00 00, ACK0x69 71), seen once just before a stacker Load — likely a per-stacker init/reset. It is intentionally not exposed until more samples pin it down.Calculation-based labware integration —
benchcel_labware.pyRather than bundling per-catalog XML profiles, BenchCel geometry is calculated from PLR
Platedimensions:calculate_benchcel_labware_settings(plate, ...)derivesStackingThickness,RobotGripperOffset,StackerGripperOffset, andSensorOffsetfrom the plate's outside dimensions, with conservative defaults and per-field overrides.apply_benchcel_labware_settings(plate, ...)setsplate.preferred_pickup_location(mappingRobotGripperOffset→ pickup-z) and validates that the PLR plate height matches.BenchCel4R(..., labware=plate_or_settings)sizes the stacker racks (site pitch = stack thickness, site height = full plate height).BenchCelLabwareSettings.from_xml_file(...)can parse user-supplied VWorks XML for comparison, but no XML profiles are bundled.BenchCelLabwareSettings.to_device_payload()encodes the labware geometry as the 77-byte0x7dpayload, andbackend.set_labware(...)pushes it to the device (followed by the0x9fcommit, exactly as VWorks does). The layout was decoded from VWorks captures of labware changes and validated byte-for-byte (e.g. Greiner 781101:StackingThickness=12.76at offset 0, full plate height14.4at offset 37). Invalid geometry is rejected by the device with a0x02error (e.g. "The labware gripper positions are too close"), surfaced as aBenchCelDeviceError.The calculation was validated against four real VWorks XML/dimension pairs (Greiner 655101/781101/782076, Agilent Seahorse) plus a Corning Costar 3961 deep-well block: the safety-critical
RobotGripperOffset/ pickup-distance-from-top calculates exactly in every case;StackerGripperOffsetand nesting overlap genuinely vary by labware family and so remain overridable.Mock server —
pylabrobot.storage.agilent.BenchCelMockServerA localhost asyncio TCP server speaking the same framed protocol, usable two ways:
0x02device-error frames for out-of-bounds moves and too-close labware gripper positions, and reproduces the split-ACK and per-stacker load/unload ACK shapes seen on the wire.Folder layout
Tests
benchcel_tests.py0x7dpayload (byte-for-byte vs capture) + validation, factory wiring, serialization, teachpoint-required transfer errors, plate→stacker mapping, and per-command wire-byte assertions (incl. the capture-confirmed downstack/upstack/load/unload primitives) via a fake socket.benchcel_mock_server_tests.pyset_labware, and labware rejection.All 33 tests pass;
pylabrobot.storage.incubator_testsstill passes.Docs
docs/user_guide/01_material-handling/storage/agilent_benchcel.rst(connection, manual safety notes from G5400-90003A, labware calculation, status/sensor reads, stacker/teachpoint moves with the VWorks-confirmed opcode mapping, the cosmetic-loading_traycaveat, the mock server, and thedangerously_rationale).docs/api/pylabrobot.storage.rstadded and wired intodocs/api/pylabrobot.rst; BenchCel added to the storage user-guide toctree and comparison tables.Resolved since first draft: plate stacking-z-height (stack pitch) is now mapped
The earlier headline open item — the BenchCel stack pitch / plate-to-plate "stacking-z-height" (VWorks
StackingThickness) not being mapped to the wire — is resolved. Captures of VWorks labware changes show it is field 0 (LE float32) of the 77-byte0x7dlabware-settings payload, pushed to the device followed by a0x9fcommit. The backend now both calculates it from PLR plate dimensions and can push it viaset_labware. Full plate height sits at offset 37 of the same payload.Still open / caveats
BenchCelLabwareSettingscarrieslidded_plate_stacking_thickness/sealed_plate_stacking_thicknessfields, but those0x7dsub-fields were always zero in the available captures (standard flat microplates only), so the lidded/sealed paths are unverified against hardware. Captures of a lidded or sealed plate being configured in VWorks would let us map and test them.Lid.nesting_z_heightandNestedTipRack.stacking_z_heightexist, neither of which is a microplate stack pitch). The PR derives stack pitch asplate.size_z − nesting_overlapwith a default overlap of 1.5 mm (matched the Greiner/Seahorse examples within ~0.2 mm, but the Corning Costar 3961 deep-well nests ~2.26 mm). It is overridable vianesting_overlap=, and PLR-side stack-height bookkeeping should be treated as advisory since the firmware tracks its own stack state.0x71command (observed once before a Load) and the stacker shelf command (support/level plates during downstack/upstack) are intentionally not exposed because their captured commands aren't yet confidently identified.0x85(current position) is preserved as raw bytes since only the0x87layout is decoded.Safety note
This integration was developed against a physical BenchCel during reverse engineering, but all wire-level behaviour in the test suite runs against the mock server. The one stack-dropping operation (
dangerously_open_stacker_grippers) is prefixed accordingly. Before any real motion: keep the robot/stacker area clear, ensure E-stop/power-off is reachable, confirm compressed air is on, and disconnect VWorks/other control clients (the BenchCel effectively allows one control client at a time). Opening stacker clamps or retracting shelves with an unsupported stack can drop plates.Contributor-guide compliance
ruff format --check(new files)ruff check+ import order (--select I)pre-commit(ruff-format, ruff-check, typos)mypy --check-untyped-defs(new modules)Backwards compatibility
No breaking changes; this is purely additive under
pylabrobot.storage.agilentwith re-exports frompylabrobot.storage.