Skip to content

Sync quickstart

The async core is canonical (see Async quickstart). The sync facade — sartoriuslib.sync — wraps it through a per-context BlockingPortal for scripts, notebooks, and REPL sessions. Every async method has a sync parity. See Design §9.

Single device

from sartoriuslib.sync import Sartorius

with Sartorius.open("/dev/ttyUSB0") as bal:
    reading = bal.poll()
    print(reading.value, reading.unit)
    bal.tare()

Sartorius.open is a context manager that returns a SyncBalance wrapping the async Balance. Same parameters as open_deviceport, protocol, serial_settings, timeout, src_sbn, dst_sbn, strict, identify — plus an optional portal= for sharing event loops.

Multi-device acquisition

from sartoriuslib.sync import (
    SyncSartoriusManager,
    SyncCsvSink,
    pipe,
    record,
)

with SyncSartoriusManager() as mgr:
    mgr.add("bal1", "/dev/ttyUSB0")
    mgr.add("bal2", "/dev/ttyUSB1")
    with (
        record(mgr, rate_hz=10, duration=60) as stream,
        SyncCsvSink("run.csv") as sink,
    ):
        summary = pipe(stream, sink)
    print(summary.samples_emitted, "samples written")

SyncSartoriusManager is a plain context manager that owns the shared portal and the wrapped async SartoriusManager. Port canonicalisation and ref-counted port sharing are the manager's job, not the caller's.

record() and pipe() mirror their async counterparts; the yielded stream is a blocking iterator of Mapping[device_name, Sample] batches. Drift-free absolute-target scheduling works the same way as the async recorder — see Logging and acquisition.

Streaming

with Sartorius.open("/dev/ttyUSB0") as bal:
    with bal.stream(rate_hz=10) as stream:
        for reading in stream:
            print(reading.value, reading.unit)

SyncBalance.stream(...) returns a sync streaming session — both a sync context manager and a sync iterator. Same semantics as the async variant (absolute-cadence poll on either protocol; consume-only autoprint mode on SBI when mode="autoprint" is set). See Streaming for the three SBI modes.

Discovery

from sartoriuslib.sync import SyncPortal, run_sync
from sartoriuslib import discover_port

with SyncPortal() as portal:
    result = portal.call(discover_port, "/dev/ttyUSB0")
    if result.protocol is not None:
        print(result.protocol, result.model)

discover_port probes a serial port for an answering balance and returns a DiscoveryResult regardless of outcome — the protocol and model fields are populated only when a device responded. The sync facade exposes discovery through a portal rather than a dedicated wrapper because port scanning is rarely a tight loop. See Troubleshooting.

Using a shared portal

The throwaway-portal default is right for one-off scripts. For code that holds both a manager and standalone sinks, share a portal so they run on the same event loop:

from sartoriuslib.sync import (
    SyncSartoriusManager,
    SyncPortal,
    SyncSqliteSink,
    pipe,
    record,
)

with SyncPortal() as portal:
    with SyncSartoriusManager(portal=portal) as mgr:
        mgr.add("bal1", "/dev/ttyUSB0")
        with (
            record(mgr, rate_hz=10, duration=60, portal=portal) as stream,
            SyncSqliteSink("run.db", portal=portal) as sink,
        ):
            pipe(stream, sink, portal=portal)

Mixing portals works but costs an extra event-loop hop per method call. Share when performance matters; don't bother for one-off runs.

See also