Skip to content

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 p36 is set to auto_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