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_device —
port, 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¶
- Installation — core install and extras.
- Async quickstart — the canonical surface.
- Balances —
Balance, families, capability flags. - Logging and acquisition — recorder, sinks,
pipe(). - Safety — destructive commands and
confirm=True.