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 yielded AcquisitionSummary totals samples emitted, ticks,
drops, and timing stats.
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.