Skip to content

feat(highres): add TundraStore/SteriStore plate store driver#1090

Open
rickwierenga wants to merge 6 commits into
v1b1from
highres-tundrastore-driver
Open

feat(highres): add TundraStore/SteriStore plate store driver#1090
rickwierenga wants to merge 6 commits into
v1b1from
highres-tundrastore-driver

Conversation

@rickwierenga

Copy link
Copy Markdown
Member

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 over pylabrobot.io.socket.Socket, with command framing + error-stack parsing. Mixes in the AutomatedRetrieval, TemperatureController, and (read-only) HumidityController capability backends + Driver. Exposes low-level home / 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.
  • TundraStoreResource + Device frontend (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 real ERROR! home … Unable to close all doors stack.

Validation

  • 13 unit tests pass; ruff format/ruff check and mypy clean.
  • Verified live against the device (read-only queries): version, homed state, nests, doors, temperature (21.9 °C), humidity, and 14-stacker layout all read back correctly through the backend.

Open question / follow-up

The TundraStore has two transfer nests, but the AutomatedRetrieval capability is single-loading-tray. For now the capability uses a configurable loading_tray_nest (default 1); pick/place can 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

rick and others added 2 commits June 12, 2026 17:57
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>
@rickwierenga rickwierenga changed the title feat(highres): add TundraStore plate store driver feat(highres): add TundraStore/SteriStore plate store driver Jun 15, 2026
rickwierenga added 4 commits June 15, 2026 22:55
- 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).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant