feat(highres): add TundraStore/SteriStore plate store driver - #1090
Open
rickwierenga wants to merge 130 commits into
Open
feat(highres): add TundraStore/SteriStore plate store driver#1090rickwierenga wants to merge 130 commits into
rickwierenga wants to merge 130 commits into
Conversation
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
y: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Notebook autoreload creates new class objects, breaking isinstance checks on backend params (silently falling back to defaults). BackendParams uses a metaclass with __instancecheck__ that falls back to qualname+module comparison, which stays stable across reloads. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- DeviceBackend -> Driver (with backward-compat alias) - Device._backend -> Device._driver, param backend -> driver - New CapabilityBackend ABC for capability-specific backend interfaces - All 15 abstract capability backends now extend CapabilityBackend - Concrete backends extend both their capability backend and Driver - Serialization key "backend" -> "driver" (deserialize accepts both) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Every monolithic backend that extended both a CapabilityBackend and Driver is now split into: - Driver: owns I/O, connection lifecycle, device-level ops - CapabilityBackend: protocol translation, encodes capability calls into driver commands Devices split: HepaFan, BioShake, Pico, Opentrons TempModule, Hamilton HeaterShaker, Hamilton TiltModule, Keyence BarcodeScanner, XPeel, SCILA, MettlerToledo, A4S, VSpin/Access2, CLARIOstar, SpectraMax 384+/M5. Also: CapabilityBackend gains _on_setup/_on_stop hooks, Capability._on_setup calls backend._on_setup, updated creating-capabilities.md, updated all legacy wrappers. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Recording backends and chatterbox backends are now pure CapabilityBackends. Test devices use a _NullDriver for the Device lifecycle. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Test capabilities directly via cap._on_setup() instead of wrapping in a fake Device. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Fix import sorting across 10+ files (ruff format --fix) - Fix MolecularDevices legacy backend: reference renamed class, update test mocks to patch at correct level (Driver/Protocol instead of legacy wrapper) - Fix Pico legacy tests: split Driver/MicroscopyBackend usage to match new architecture - Fix Opentrons temp module: add base-type annotations for if/else branches - Fix Liconic: use _on_setup/_on_stop (CapabilityBackend API) - Fix Azenta A4S: type: ignore[safe-super] for abstract Driver methods - Fix Pico backend: self._snap_images() instead of self._driver._snap_images() Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Move shaker/tc capabilities into base class with has_shaking, has_temperature, supports_active_cooling flags. Add resource definitions for BioShake3000, BioShake3000Elm, BioShake3000ElmDWP, and BioShakeQ1 from spec sheets. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…nds (#957) Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Extract autoload firmware protocol into a standalone class that takes a driver reference and operates on track numbers instead of Carrier objects. The legacy STARBackend and new STAR device can both wire into this class. Includes 36 tests covering all command types. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
PlateReader now delegates reads through AbsorbanceCapability, LuminescenceCapability, and FluorescenceCapability via adapter backends that wrap the legacy PlateReaderBackend. Extracted _DictBackendParams into pylabrobot/legacy/_backend_params.py for reuse across legacy adapters. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Move autoload, cover, x-arm, wash station, and ~44 generic driver infrastructure methods (firmware queries, EEPROM, area reservation, configuration) into the new STARDriver architecture. Legacy backend methods now delegate to new classes or have deprecation docstrings. - STARAutoload: autoload module control (carrier loading, barcode, LEDs) - STARCover: front cover lock/unlock/enable/disable - STARXArm: left/right X-arm positioning (parameterized by side) - STARWashStation: dual-chamber wash station drain/fill/init - STARDriver: generic instrument operations directly on driver - STARChatterboxDriver: updated with all subsystems - STAR device only exposes capabilities (PIP, Head96, iSWAP) - Subsystems live on the driver, accessed via star._driver - 114 tests across all subsystems - Right X-arm and wash station are conditional on hardware config - X-arm methods use mm (PLR standard), not 0.1mm firmware units - Fixed pre-existing assertion bugs in release_occupied_area and set_instrument_configuration Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
These methods send a command to hardware and wait for a response, so request_ better reflects the I/O semantics. Covers PreciseFlex, capability interfaces (temperature, humidity), and all vendor backends (Azenta, Agilent, BMG, Byonoy, Hamilton, INHECO, Liconic, Molecular Devices, Opentrons, Qinstruments, Thermo Fisher). Legacy public APIs keep get_ names unchanged; only internal delegations are updated. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Move 14 multi-channel PIP operations to STARPIPBackend: channel positioning (Y/Z), initialization, spread, z-safety, foil piercing. Parameters use mm (PLR standard) with internal 0.1mm conversion. Key changes: - pierce_foil and step_off_foil on STARPIPBackend with explicit deck param - iSWAP-parked checks on Y-movement methods - Channel min Y spacing queried from firmware in driver setup() - Right X-arm conditional on right_x_drive_large - Wash station conditional on wash_station_*_installed - Legacy backend aliases (left_x_arm, iswap) for PIPBackend compat - Fixed pierce_foil one_by_one bug (z vs z+distance_from_bottom) - Fixed _ensure_can_reach_position dead fallback (is None vs not) - Architecture doc updated Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Two new dispensing capabilities under bulk_dispensers/:
- SyringeDispensing: dispense(plate, volumes={col: vol}), prime(plate, volume)
- PeristalticDispensing: dispense(plate, volumes={col: vol}), prime(), purge()
Both use BackendParams for device-specific parameters.
Also adds BackendParams to PlateWashingCapability.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- EL406Driver: FTDI I/O, batch management, device-level ops, queries - EL406PlateWashingBackend: manifold ops (wash, aspirate, dispense, prime) - EL406ShakingBackend: shake/soak - EL406SyringeDispensingBackend: syringe dispense/prime - EL406PeristalticDispensingBackend: peristaltic dispense/prime/purge Legacy code is thin wrappers delegating to new backends. All 385 tests pass. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…tecture Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Move plate washer docs from 00_liquid-handling/plate-washing/ to agilent/biotek/el406/ and heater-shaker docs from 01_material-handling/heating_shaking/ to qinstruments/bioshake/. Add Manufacturers toctree section with manufacturer-level indexes. Include migration guide at repo root for future device migrations. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Move device docs from legacy category dirs (00_liquid-handling/, 01_material-handling/, 02_analytical/) to manufacturer-based layout mirroring the codebase. Add Manufacturers toctree section and API reference RST files for all manufacturers with autosummary directives and autoclass for nested BackendParams. Add Sphinx cross-references for BackendParams in notebook markdown cells. Devices migrated: EL406, BioShake, Mettler Toledo WXS205SDU, Azenta a4S, Azenta XPeel, Liconic STX, Inheco ThermoShake, Inheco CPAC, Inheco SCILA, Inheco Incubator Shaker, Inheco ODTC, Thermo Fisher Multidrop Combi. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
These single-field dataclasses just wrapped a dict. request_door_status now returns Dict[str, DoorState] and request_nest_status returns Dict[int, NestState] directly. The only behavior on them, DoorStatus. all_closed, was test-only and is inlined at its sole call site. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replaces the raw-dict-plus-property settings view with a frozen dataclass
exposing all 545 device settings as explicit typed attributes (device key
lower-cased), with types inferred from the device's values. machine_type
is a Literal of known models and warns ("please contribute") on an unknown
model. Settings are loaded whole from the device; no shipped defaults.
request_settings() is simplified accordingly: drops the unused search
filter and the separate serial/firmware plumbing (SERIAL_NUMBER is now a
field; firmware stays on request_version).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The device's settings command is the only source, returning NAME = value text, so keep just from_lines (used by request_settings) and fold the field mapping into it. Drops the unused from_dict/from_json/to_json; direct construction is already available via the dataclass constructor. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
#1125) Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…river # Conflicts: # pylabrobot/capabilities/automated_retrieval/automated_retrieval.py
…trays #1125 moved storage bookkeeping into AutomatedRetrieval (single loading tray). Extend that standard to multiple loading trays addressed by a 0-based tray_index (defaulting to the first), so multi-nest devices get the shared bookkeeping too: - AutomatedRetrieval takes loading_trays: List[PlateHolder]; fetch_plate_ to_loading_tray / take_in_plate take tray_index and pass it to the backend (which already accepts it). - TundraStore composes the capability with its nests as loading_trays and drops its duplicated bookkeeping (sites, fetch/store, NoFreeSiteError, default_tray); users go through store.retrieval.* with tray_index. - Liconic and Cytomat pass loading_trays=[self.loading_tray]. - Updated the automated-retrieval notebook to loading_trays=[...]. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
After #1125 moved storage operations onto AutomatedRetrieval, the device frontends no longer expose take_in_plate / fetch_plate_to_loading_tray / summary. Route those calls through device.retrieval in both hello-world notebooks, and repoint the Cytomat {meth} cross-references to the AutomatedRetrieval capability. (.loading_tray and .racks stay on the device.) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Coercion mostly returned the raw string unchanged; only int/float fields need conversion. Inline that single expression in from_lines instead of a dedicated helper. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
DoorState and NestState are now Literal aliases with lowercase values plus DOOR_STATES/NEST_STATES valid-value tuples, matching the MachineType pattern. The backend lower-cases the device's status output and validates against the tuples (falling back to "unknown") instead of constructing enums. Rename standard.py to types.py. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ckends
The single TundraStoreBackend mixed transport, retrieval, temperature and
humidity. Split it so a HighResSampleStorageDriver owns the transport
(send_command + shared version/environment queries) and the per-capability
backends it constructs:
- HighResSampleStorageAutomatedRetrievalBackend (storage/motion)
- HighResSampleStorageTemperatureControllerBackend
- HighResSampleStorageHumidityControllerBackend
Capability backends build commands on driver.send_command (no duplication on
the driver). The device pulls them off driver.{automated_retrieval,temperature,
humidity}; the chatterbox is now a driver that fakes only the transport and
owns the real capability backends.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…lasses The TundraStore, SteriStore and AmbiStore are the same machine family behind a shared port-1000 API. Rename the module to highres.sample_storage and put all implementation on a private base device _HighResSampleStorage; the concrete devices are thin subclasses: - TundraStore / SteriStore: refrigerated, full retrieval + temperature/humidity. - AmbiStore: WORK IN PROGRESS (warns on init); ambient, so it wires only the retrieval capability and no environment control (unverified against hardware). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…r file backend.py held the driver and all three capability backends. Split it into a driver/ subpackage: - driver/protocol.py - wire tokens + parse_kv (shared helpers) - driver/automated_retrieval.py - driver/temperature.py - driver/humidity.py - driver/driver.py - HighResSampleStorageDriver (owns the three backends) Capability backends reference the driver via a TYPE_CHECKING import to avoid a runtime import cycle; the driver/ __init__ re-exports the four classes so the public import paths are unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Relocate backend_tests.py into tests/, matching the repo convention of a tests/ package with *_tests.py modules, and split the two suites (which share no fixtures) into self-contained files: - tests/driver_tests.py - transport, queries, temperature/humidity (FakeSocket) - tests/recovery_tests.py - pick/place fault classification + recovery (ScriptedSocket) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Use a consistent request_ prefix for every method that reads state from the device, matching request_version/request_environment/etc: - get_stacker_dimensions -> request_stacker_dimensions - scan_stacker_barcodes -> request_stacker_barcodes - is_homed -> request_is_homed - is_parked -> request_is_parked - spatula_request_is_holding -> request_spatula_is_holding - nest_request_is_holding -> request_nest_is_holding Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tterbox Mirror STARChatterboxDriver: call the real driver __init__ (which constructs an inert, unconnected socket and owns the real per-capability backends) and only override setup/send_command/request_* to fake the transport. Drops the `self.io = None # type: ignore` placeholder and the duplicate backend construction that came from bypassing the parent constructor. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Remove the device-free chatterbox driver and its export; it can be reintroduced later if needed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
#1129) Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…edRetrieval ` with `RandomAccessRetrieval `) (#1142) Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: Rick Wierenga <rick@pylabrobot.org>
Delegate host/port/timeouts to Socket.serialize() under an "io" key (matching vspin/EVO backends) rather than reaching into io._host/_port. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reconcile the sample-store multi-tray work with upstream's split of AutomatedRetrieval into a base + RandomAccessRetrieval / StackerRetrieval. Fold in finding #2 correctly: multi-tray support now lives in the shared AutomatedRetrieval base (loading_trays + tray_index resolution), so both RandomAccessRetrieval and StackerRetrieval are multi-tray. tray_index=None resolves to the backend's default_tray_index on BOTH the capability's resource bookkeeping and the backend's motion, so they always target the same physical tray (previously the capability hardcoded tray 0, defeating a non-zero default). - capability base owns loading_tray(s) + _loading_tray/_plate_on_loading_tray - AutomatedRetrievalBackend/StackerBackend gain default_tray_index (default 0) - Random-access + stacker ops thread tray_index through to the backend - sample store overrides default_tray_index = loading_tray_nest - 1 - Liconic/Cytomat compose RandomAccessRetrieval (single loading_tray) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The public knob was loading_tray_nest (1-based device nest); rename to default_tray_index (0-based, default 0) so the capability API is uniformly 0-based. The 0-based -> 1-based device-nest conversion now lives solely in _nest_for_tray(), where the wire protocol needs it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Is this compatible with the AmbiStore? The AmbiStore is the same commands as the TundraStore without the refrigeration. |
Member
Author
|
should be! do you have one to test it with? |
|
Yes! I will test when I find time! Thanks! |
Member
Author
|
sweet, I dont have one for testing, so would appreciate the feedback there! the docs I have are actually for tundra store but work just the same on steri. look forward to hearing how it goes, please dont hesitate to reach out |
rickwierenga
force-pushed
the
v1b1
branch
2 times, most recently
from
August 1, 2026 20:32
6af085c to
1ae9dc6
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.
Summary
Adds a PyLabRobot driver for the HighRes Biosolutions TundraStore (branded "SteriStore") refrigerated automated plate store. The device exposes a line-based text protocol over TCP port 1000 (
ACK!echo → optional data →OK!/ABORTED!/ERROR!completion).New package
pylabrobot/highres/tundrastore/, modeled on the v1b1 Cytomat driver:TundraStoreBackend— transport overpylabrobot.io.socket.Socket, with command framing + error-stack parsing. Mixes in theAutomatedRetrieval,TemperatureController, and (read-only)HumidityControllercapability backends +Driver. Exposes low-levelhome/pick(stacker, slot, nest)/place/ door / barcode commands and status queries (version, doors, nests, spatula, environment, stacker dimensions).TundraStoreChatterboxBackend— device-free backend for offline testing.TundraStore—Resource + Devicefrontend (racks → stackers, capability registration), mirroring the Cytomat frontend.backend_tests.py— 13 tests driven by real responses captured from hardware (firmware 3.0.0.119, serial HRB-2209-35148), including the realERROR! home … Unable to close all doorsstack.Validation
ruff format/ruff checkandmypyclean.Open question / follow-up
The TundraStore has two transfer nests, but the
AutomatedRetrievalcapability is single-loading-tray. For now the capability uses a configurableloading_tray_nest(default 1);pick/placecan address either nest directly. A proper two-nest mapping is left as a follow-up.The temperature/humidity setter paths (
environmentset TEMP …,environment TEMP off) are constructed from the manual's command grammar but not yet hardware-verified (couldn't safely run state changes during testing). Read paths are verified.🤖 Generated with Claude Code