Streaming¶
Sartorius balances support two acquisition shapes:
- Request/response polling — works on either xBPI or SBI. The session emits one read frame per tick at an absolute cadence.
- Device-driven autoprint — SBI-only. The balance owns the wire and
emits weight lines continuously when parameter
p36is set toauto_wo/auto_w.
Both paths produce the same Reading shape. The differences
live entirely in Balance.stream(...)'s mode= argument. See Design §10.
Cadenced poll (default)¶
async with await open_device("/dev/ttyUSB0") as bal:
async with bal.stream(rate_hz=10) as stream:
async for reading in stream:
print(reading.value, reading.unit)
Balance.stream(rate_hz=...)
returns a StreamingSession
— an async context manager and async iterator. Ticks fire on absolute
targets (no drift accumulation) and the session never mutates device-side
state. Works on both protocols.
rate_hz is required for mode="poll". The session raises
ValueError if it's missing or non-positive.
Three SBI modes¶
xBPI does not autoprint in the decoded protocol; the xBPI stream(...)
convenience always polls. SBI is where stream modes matter, because
autoprint and command/reply share line framing but have incompatible
semantics — once the balance owns the wire, query tokens don't reliably
produce distinguishable replies.
Balance.stream(...) exposes three explicit modes so the caller's intent
is always visible:
# 1. Poll — request/response cadence, no device-side mutation. Default.
async with bal.stream(mode="poll", rate_hz=10) as stream: ...
# 2. Consume already-enabled autoprint — no device-side mutation.
# Fails on entry if autoprint is not already active.
async with bal.stream(mode="autoprint") as stream: ...
# 3. Configure autoprint for the lifetime of the stream, restore on exit.
# Mutates parameter p36 — requires confirm=True.
async with bal.stream(
mode="autoprint",
temporary_autoprint=True,
confirm=True,
) as stream: ...
Mode 3 is not implemented yet
mode="autoprint", temporary_autoprint=True raises
NotImplementedError in the current release. The temporary-autoprint
path needs a verified SBI parameter-write command. Until that lands,
callers who want autoprint must enable p36 from the front panel
(or via sarto-configure) and use mode="autoprint" against the
already-enabled stream.
mode="poll" while SBI autoprint is active¶
If the SBI session detects autoprint already running and the caller asks
for mode="poll", the stream entry raises SartoriusAutoprintActiveError
with guidance to use mode="autoprint" instead. The library refuses to
buffer continuous output behind a cadenced poll loop because stale
backlog reads silently introduce timing skew.
Front-panel autoprint toggle mid-session¶
If the user enables autoprint on the front panel during an open session,
the next SBI command/reply call that sees unsolicited output marks the
session active, preserves the observed line for later consumption, and
raises SartoriusAutoprintActiveError. Conversely, if autoprint is
disabled mid-session, calls to Balance.refresh_sbi_autoprint_state()
let the session re-discover the state quietly.
Multi-device acquisition with record(...)¶
For multiple balances or when you need a sample-stream with explicit timing metadata, use the recorder:
import anyio
from sartoriuslib import SartoriusManager
from sartoriuslib.streaming import record
from sartoriuslib.sinks import CsvSink, pipe
async def main() -> None:
async with SartoriusManager() as mgr:
await mgr.add("bal1", "/dev/ttyUSB0")
await mgr.add("bal2", "/dev/ttyUSB1")
async with (
record(mgr, rate_hz=5, duration=30) as stream,
CsvSink("run.csv") as sink,
):
summary = await pipe(stream, sink)
print(f"{summary.samples_emitted} samples, {summary.samples_late} late")
anyio.run(main)
record(...) yields
Mapping[device_name, Sample] per tick. Each Sample
carries the wrapped Reading, requested timestamp, received timestamp,
elapsed seconds, protocol, and any per-device error caught during the
tick. The recorder logs its producer AcquisitionSummary on context exit;
pipe(...) returns an AcquisitionSummary for the samples it wrote.
See Logging and acquisition for the full sink surface and Design §10 for scheduling and overflow policy.
Error handling inside a stream¶
The async iterator surfaces transport / protocol errors inline — the
stream stops on the first uncaught exception. For long-running runs
that should tolerate a flaky device, catch SartoriusError inside the
loop body. The record() recorder takes a different approach: per-tick
errors are recorded on Sample.error instead of stopping the stream.
See also¶
- Logging and acquisition —
record(), sinks,pipe(). - Commands — what
poll()actually sends. - Readings —
Readingshape. - Safety —
confirm=Trueand persistent-state mutation rules. - Design §10 — scheduling, overflow, autoprint state machine.