Sample storage#

The HighRes Biosolutions AmbiStore, SteriStore, and TundraStore use the same TCP command protocol and share a PyLabRobot interface. Choose the concrete class for the model you are configuring; the product name reported by firmware is retained as version information and does not override that choice.

Models#

Model

Environment

Verification

AmbiStore

Ambient storage

Work in progress; not hardware-verified

SteriStore

Heating, active cooling, humidity, CO2, and optional O2

Hardware-verified

TundraStore

Refrigeration and temperature-dependent humidity control

Work in progress; not hardware-verified

The published temperature ranges and environmental options come from the current HighRes sample-storage page and an archived HighRes sample-storage brochure.

Network connection#

The remote-control server listens on TCP port 1000. The normal factory address is 192.168.127.60; HighRes devices also expose the service at 10.253.253.253. Give the dedicated host Ethernet interface an address on both subnets so either address remains reachable:

sudo ip address replace 192.168.127.50/24 dev <interface>
sudo ip address replace 10.253.253.250/24 dev <interface>

These ip address changes are temporary and disappear when the USB Ethernet adapter is unplugged or the host restarts. On Linux systems managed by NetworkManager, create a persistent connection profile instead:

sudo nmcli connection add \
  type ethernet \
  ifname <interface> \
  con-name highres-sample-storage \
  ipv4.method manual \
  ipv4.addresses "192.168.127.50/24,10.253.253.250/24" \
  ipv4.never-default yes \
  ipv6.method disabled
sudo nmcli connection up highres-sample-storage

Use ip -brief link to find <interface>. Verify the isolated link with ping 10.253.253.253; do not add a gateway or default route to this connection.

Setup#

Pass the storage racks as a mapping from one-based physical stacker numbers to their PLR resources, then instantiate the appropriate model. Stacker numbers may be sparse. For example:

from pylabrobot.high_res.sample_storage import SteriStore

racks = {1: rack_1, 3: rack_3}
store = SteriStore(host="192.168.127.60", name="steristore", racks=racks)
await store.setup()

Each mapping key is the rack’s one-based device stacker number. Within a rack, the zero-based carrier spot maps to the one-based physical slot: spot 0 is device slot 1, spot 1 is slot 2, and so on. Mapping insertion order does not affect this relationship.

Alternatively, omit racks to select device-discovery mode. During setup(), the driver reads the configured zero offset, slot height, and slot count with the read-only getstackerdimensions command and creates empty stacker resources. Passing racks={} explicitly represents a store with no configured racks and does not enable discovery.

During setup, the device-reported transfer nests become store.nests. Their locations relative to the store are left undefined because they depend on the surrounding robot installation. Setup does not invent plate resources for occupied nests; assign any already-present plates to the matching nest after setup.

Plate transfers#

Fetch a known plate from its stacker slot to a transfer nest:

plate = await store.fetch_plate_to_loading_tray("plate_1", tray_index=0)

Move the plate on a transfer nest back into storage, choosing either a specific PlateHolder, the smallest available site, or a random available site:

await store.take_in_plate(tray_index=0, site="smallest")

Both operations update the PLR resource tree only after successful hardware motion. See Sample-storage events for their structured execution events.

Move a plate directly between two transfer nests using their zero-based tray indices:

plate = await store.transfer_plate_between_nests(
  source_tray_index=1,
  destination_tray_index=0,
)

The driver verifies every transfer-nest endpoint with its live presence sensor before motion. The stacker itself has no non-destructive per-slot presence query: fetching detects an empty source only during the pick, while storing relies on the PLR resource tree to determine that the destination slot is free. Keep the modeled stacker inventory synchronized with the physical store; barcode EMPTY is not a plate-presence result.

Barcode scans#

Barcode scans require every transfer nest to be clear. The driver checks this before starting the scan because firmware 3.0.0.119 otherwise waits for an automation door and eventually times out.

barcodes = await store.request_stacker_barcodes(2)
barcode = await store.request_stacker_barcodes(2, slot=1)

A returned value of EMPTY means that the scanner did not read a barcode. It does not prove the physical slot is empty; use resource bookkeeping or a physical plate-presence workflow for that.

Recovery#

request_is_parked() verifies that the device is homed and that both the spatula slide and lift axes are retracted. recover() refuses to move when the spatula sensor reports a plate, because the plate’s physical support must be inspected before a safe recovery path can be chosen.

If a transfer response is interrupted or otherwise ambiguous, the driver keeps the last confirmed resource assignment in place, records store.unresolved_transfer, and blocks further plate moves, homing, and barcode scans. Inspect the machine, recover it when necessary, then reconcile the observed result:

await store.resolve_unresolved_transfer("source")

Use "destination" when the plate completed the move or "unassigned" when it is at neither modeled endpoint. If the incomplete response invalidated the TCP stream, reconciliation reconnects automatically before reading the spatula and nest sensors.