Skip to content

Sync quickstart

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

Single controller

from watlowlib import ProtocolKind
from watlowlib.sync import Watlow

with Watlow.open(
    "/dev/ttyUSB0",
    protocol=ProtocolKind.STDBUS,
    address=1,
) as ctl:
    pv = ctl.read_pv()
    print(pv.value, pv.unit)
    ctl.set_setpoint(75.0, confirm=True)

Watlow.open is a context manager that returns a SyncController wrapping the async Controller. Same parameters as open_deviceport, protocol, address, serial_settings — plus an optional portal= for sharing event loops.

Multi-controller acquisition

from watlowlib.sync import (
    SyncCsvSink,
    SyncWatlowManager,
    pipe,
    record,
)

with SyncWatlowManager() as mgr:
    mgr.add("oven_top", "/dev/ttyUSB0", address=1)
    mgr.add("oven_bot", "/dev/ttyUSB0", address=2)
    mgr.add("retort", "/dev/ttyUSB1", address=1)
    with (
        record(
            mgr,
            parameters=["process_value", "setpoint"],
            rate_hz=2.0,
            duration=300.0,
        ) as stream,
        SyncCsvSink("run.csv") as sink,
    ):
        summary = pipe(stream, sink, batch_size=64, flush_interval=1.0)
    print(summary.samples_emitted, "samples written")

SyncWatlowManager is a plain context manager that owns the shared portal and the wrapped async WatlowManager. Port canonicalisation, per-port locking, and ref-counted client 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 Sample batches. Drift-free absolute-target scheduling works the same way as the async recorder — see Logging and acquisition.

Discovery

from watlowlib import sweep_stdbus
from watlowlib.sync import SyncPortal

with SyncPortal() as portal:
    results = portal.call(sweep_stdbus, "/dev/ttyUSB0")
    for row in results:
        if row.protocol is not None:
            print(row.address, row.info.part_number.raw)

sweep_stdbus walks Standard Bus addresses 1–16; sweep_modbus walks a configurable Modbus slave range. Both return one DiscoveryResult per address regardless of outcome — the protocol and info 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.

Persistent writes need confirm=True

ctl.set_setpoint(75.0)               # WatlowConfirmationRequiredError
ctl.set_setpoint(75.0, confirm=True)  # writes; returns Reading echo

SafetyTier.PERSISTENT parameters require an explicit confirm=True at the facade. The session raises WatlowConfirmationRequiredError before any I/O if the gate is missing. See Safety.

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 watlowlib.sync import (
    SyncPortal,
    SyncSqliteSink,
    SyncWatlowManager,
    pipe,
    record,
)

with SyncPortal() as portal:
    with SyncWatlowManager(portal=portal) as mgr:
        mgr.add("oven_top", "/dev/ttyUSB0", address=1)
        with (
            record(mgr, parameters=["process_value"], rate_hz=2.0, duration=60.0, portal=portal) as stream,
            SyncSqliteSink("run.sqlite", 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