feat(highres): add TundraStore/SteriStore plate store driver#1090
Open
rickwierenga wants to merge 24 commits into
Open
feat(highres): add TundraStore/SteriStore plate store driver#1090rickwierenga wants to merge 24 commits into
rickwierenga wants to merge 24 commits into
Conversation
Add a PyLabRobot driver for the HighRes Biosolutions TundraStore (branded "SteriStore") refrigerated automated plate store, which speaks a line-based text protocol over TCP port 1000. - TundraStoreBackend: pylabrobot.io.socket.Socket transport with ACK / data / completion (OK!/ABORTED!/ERROR!) framing and error-stack parsing. Implements AutomatedRetrieval, TemperatureController and (read-only) HumidityController capabilities, plus low-level home/pick/place/door/ barcode commands and status queries (version, doors, nests, spatula, environment, stacker dimensions). - TundraStoreChatterboxBackend for device-free use. - TundraStore frontend (Resource + Device) modeled on Cytomat. - Tests driven by real captured firmware-3.0.0.119 responses. Note: the device has two transfer nests; mapping the single-loading-tray AutomatedRetrieval model onto both is left as a follow-up — the capability currently uses a configurable single nest (default 1). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…'s two nests Add an optional 0-based `tray` argument to AutomatedRetrieval.fetch_plate_to_ loading_tray / store_plate (and the backend abstract methods). `tray=None` selects the device default, so all existing callers are unaffected. - Single-tray devices (Cytomat, Heraeus, Liconic) validate via the new ensure_single_tray() helper (accept None/0, reject others). - TundraStore maps tray i -> device nest i+1; the frontend now models both nests as TundraStore.nests[0|1] with per-tray plate tracking (fetch_plate_to_nest / take_in_plate take a `tray`). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- TundraStoreSettings: client-side dataclass over the device's ~545-key
settings file, with typed accessors and named properties (height-detect,
beam-break, stacker bases, nest heights/inputs, plate defaults). Loadable
from the device (backend.request_settings()) or JSON.
- pick() now classifies failures without performing hidden motion:
- PlateNotFoundError when a slot is empty ("No plate detected") and the
store retracted cleanly (the normal empty-slot outcome at any non-top slot).
- TundraStoreFault when the machine was left unsafe (spatula extended /
unhomed) — e.g. an empty TOP slot, where the firmware can't complete its
safe-travel retract. homedstatus reports homed even while stuck, so the
firmware's "unsafe for rotation" signal is used, not just is_homed().
- recover(): explicit retract-spatula + re-home, for use after a fault.
Hardware-validated: empty middle-slot pick -> graceful "No plate detected";
empty top-slot pick -> fault + recover() restores a homed state.
Replace raw status-command calls with typed methods: - spatula_request_is_holding() -> bool - nest_request_is_holding(nest) -> bool (occupied nest reports UNKNOWN) - probe_presence(stacker, slot, to_nest=1) -> bool — sense a stacker slot by attempting a pick (graceful empty at non-top slots); a found plate is moved to to_nest as a side effect.
recover() short-circuited on is_homed(), but homedstatus reports homed even while the spatula is stuck extended in a stacker after a faulted top-slot pick — so recover() returned True without retracting (left the machine stuck). - Add is_parked(): homed AND the slide (Y) axis retracted near home (a stuck spatula sits at the ~256mm slide-in depth). This is the real "safe to move" check; is_homed() alone is not. - recover() now ALWAYS issues enable -> spatulaout -> home and confirms via is_parked(), retrying a few times. Never trusts homedstatus. Hardware-validated: empty-pick fault -> is_homed()=True but is_parked()=False; recover() retracts Y 256->0 and re-homes. Also found the empty-pick fault affects the top *few* slots (23 faults too), not only slot 24.
The store re-seals its doors during every transfer (the firmware exposes no way to skip that — a 4th `place` arg errors on 3.0.0.119), so close_door controls the END state: close_door=False re-opens the doors after the move, leaving the carousel accessible for back-to-back ops. Defaults to True (sealed). One firmware-agnostic implementation (place/pick + conditional openalldoors).
Renames the new loading-tray/nest index parameter from `tray` to `tray_index` across the AutomatedRetrieval capability, its backends (TundraStore, Liconic, Cytomat, Heraeus), and the TundraStore frontend, including docstrings and user-facing error messages. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Moves DoorState/NestState into standard.py (next to the DoorStatus/ NestStatus dataclasses that use them) and the protocol reply tokens (ACK!/OK!/ABORTED!/ERROR!) into backend.py, their only consumer. Drops the unused PLATE_AVAILABLE constant and deletes the now-empty module. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Removes the ensure_single_tray free function (and its export from the automated_retrieval capability) and inlines the equivalent ``tray_index not in (None, 0)`` check directly into the fetch/store methods of the single-tray Liconic, Cytomat, and Heraeus backends. Co-Authored-By: Claude Opus 4.8 (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>
…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>
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>
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