sartoriuslib.transport¶
The Transport Protocol, SerialTransport for hardware, and
FakeTransport for tests. See Design §8.1.
Public surface¶
sartoriuslib.transport ¶
Transport layer — moves bytes only. No Sartorius command semantics.
See design doc §8.
FakeTransport ¶
Scripted :class:Transport for tests.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
script
|
Mapping[bytes, ScriptedReply] | None
|
Mapping of |
None
|
label
|
str
|
Identifier used in errors. |
'fake://test'
|
latency_s
|
float
|
Per-operation artificial delay, useful for simulating a slow device. |
0.0
|
Source code in src/sartoriuslib/transport/fake.py
last_reopen_baud
property
¶
Baud rate requested by the most recent :meth:reopen, or None.
last_reopen_parity
property
¶
Parity requested by the most recent :meth:reopen, or None.
last_reopen_stopbits
property
¶
Stop bits requested by the most recent :meth:reopen, or None.
add_script ¶
feed ¶
Push unsolicited bytes into the read buffer.
Useful for simulating a device that was left in SBI autoprint mode, or garbage on the line that the session must drain on recovery.
Source code in src/sartoriuslib/transport/fake.py
force_read_timeout ¶
force_reopen_error ¶
force_write_timeout ¶
reopen
async
¶
Simulate a serial-setting change — close, record, reopen.
If force_reopen_error() has been called the reopen raises
:class:SartoriusConnectionError after the close, leaving the
transport closed. That is the "reopen wedged" path used by
:meth:Balance.configure_protocol tests for the BROKEN-state
transition.
Source code in src/sartoriuslib/transport/fake.py
SerialSettings
dataclass
¶
SerialSettings(
port,
baudrate=9600,
bytesize=ByteSize.EIGHT,
parity=Parity.ODD,
stopbits=StopBits.ONE,
rtscts=False,
xonxoff=False,
exclusive=True,
)
Serial-port configuration for :class:SerialTransport.
Mirrors :class:anyserial.SerialConfig plus a port path. Default
framing is 8-O-1 because that is universal across every Sartorius
family we have captures for (MSE, WZA, BCE) per docs/protocol.md
§2.1. Baud defaults to 9600 because it matches the BCE default and
sits in the middle of the supported range; the MSE uses 19200 and the
WZA 1200, so callers that care will supply the right value or rely on
:func:sartoriuslib.open_device auto-detection. Note: all three
families ship from the factory in SBI mode (WZA in SBI autoprint at
1200-7-O-1); xBPI is reached by a front-panel menu change.
exclusive defaults True so two processes can't scribble over
the same device — neither xBPI nor SBI is multi-master tolerant.
The bytesize / parity / stopbits fields are typed as
the enums and that is what callers should statically pass. Runtime
accepts an equivalent int/str shorthand (e.g.
stopbits=1, parity="odd") — __post_init__ normalises
the value to the enum and raises
:class:SartoriusConfigurationError on anything the coercer can't
recognise. The static types stay strict so type-checkers point bad
callers at the enum form, while the runtime stays forgiving for
ad-hoc scripts and front-panel-derived values
(os.environ["SBI_PARITY"] → "odd"). The widened
constructor types are exposed only via the # type: ignore lines
in :meth:__post_init__ — :class:SerialTransport and
:mod:anyserial below this layer can always rely on the field
being an enum member.
SerialTransport ¶
:class:Transport backed by a real serial port via anyserial.
Tests that don't need hardware can use
:class:sartoriuslib.transport.fake.FakeTransport instead; the two
conform to the same structural :class:Transport protocol.
Source code in src/sartoriuslib/transport/serial.py
reopen
async
¶
Close and reopen the port with any subset of overrides.
Any argument left as None keeps the existing setting. Used for
the WZA SBI→xBPI flip (swaps both baud and parity) and for
set_baud_rate retuning after the device has already switched.
The cached :class:SerialSettings is updated so the new settings
survive subsequent close / open round-trips.
If :meth:open fails on the new settings the transport is left
closed; the caller is responsible for surfacing that as a
BROKEN session state with recovery guidance.
Source code in src/sartoriuslib/transport/serial.py
Transport ¶
Bases: Protocol
Byte-level transport.
Every I/O boundary takes an explicit timeout. On expiry, implementations
raise :class:sartoriuslib.errors.SartoriusTimeoutError — never return an
empty or partial bytes silently. Backend exceptions normalize to
:class:sartoriuslib.errors.SartoriusTransportError (or a subclass) with
__cause__ preserving the original exception.
Lifecycle is single-shot: :meth:open once, :meth:close once.
:meth:reopen closes + reopens with any subset of serial-setting
overrides, used for the WZA SBI→xBPI protocol flip that also swaps baud
and parity.
close
async
¶
drain_input
async
¶
open
async
¶
read_available
async
¶
Read until the line goes idle for idle_timeout seconds.
Never raises on idle expiry — an idle timeout is the expected exit. Returns whatever was accumulated (possibly empty). Used for best-effort drain, passive SBI autoprint sniffing during protocol detection, and stream-stop recovery.
Source code in src/sartoriuslib/transport/base.py
read_exact
async
¶
Read exactly n bytes.
The canonical shape for xBPI framing: read one byte to discover
the length, then read that many bytes. Raises
:class:sartoriuslib.errors.SartoriusTimeoutError if fewer than
n bytes arrive before timeout. Partial buffers are retained
for the next call — implementations must not discard them.
Source code in src/sartoriuslib/transport/base.py
read_until
async
¶
Read bytes up to and including the next occurrence of separator.
Raises :class:sartoriuslib.errors.SartoriusTimeoutError if the
separator does not arrive before timeout. Bytes received
after the separator remain buffered for the next call —
implementations must not discard them.
Source code in src/sartoriuslib/transport/base.py
reopen
async
¶
Close and re-open the port, optionally changing serial framing.
Used by :meth:Balance.configure_protocol for the WZA SBI→xBPI
flip, which swaps both baud rate and parity, and by operations
like set_baud_rate that retune the port after the device has
already switched mid-sequence. Any argument left as None keeps
the existing value.
Implementations must leave the transport in a consistent state: either fully reopened with the new settings, or clearly closed so callers can recognise a failure. Silent partial states are the worst failure mode for this method.
Non-serial transports (e.g. a future TCP adapter) may raise
:class:NotImplementedError — baud/parity don't apply there.
Source code in src/sartoriuslib/transport/base.py
write
async
¶
Write every byte of data.
Raises :class:sartoriuslib.errors.SartoriusTimeoutError on
expiry. A bounded write timeout is mandatory because sends can
block on RS-485 hardware flow control or a stuck device. Callers
that block indefinitely hide real hangs.
Source code in src/sartoriuslib/transport/base.py
Base Protocol + serial settings¶
sartoriuslib.transport.base ¶
Transport layer abstraction — moves bytes, knows nothing about Sartorius.
The :class:Transport :pep:544 Protocol is the structural interface every
backend implements. :class:SerialSettings is the port-configuration
dataclass consumed by
:class:sartoriuslib.transport.serial.SerialTransport.
Sartorius balances speak two wire protocols: xBPI (binary, length-prefixed) and SBI (ASCII, line-oriented). The transport surface exposes both shapes:
- :meth:
Transport.read_exact— fixed-count read for xBPI length-prefix framing (read 1 byte forlen, then readlenmore bytes). - :meth:
Transport.read_until— delimiter read for SBI's\r\n-terminated lines. - :meth:
Transport.read_available— idle-bounded read for passive SBI autoprint sniffing during :func:sartoriuslib.open_deviceauto-detection.
Default serial framing is 8-O-1. Per docs/protocol.md §2.1 the balance's
PC-USB receive path is parity-forgiving, but 8-O-1 is universal on TX so the
default is the safe one.
Design reference: docs/design.md §8.1.
SerialSettings
dataclass
¶
SerialSettings(
port,
baudrate=9600,
bytesize=ByteSize.EIGHT,
parity=Parity.ODD,
stopbits=StopBits.ONE,
rtscts=False,
xonxoff=False,
exclusive=True,
)
Serial-port configuration for :class:SerialTransport.
Mirrors :class:anyserial.SerialConfig plus a port path. Default
framing is 8-O-1 because that is universal across every Sartorius
family we have captures for (MSE, WZA, BCE) per docs/protocol.md
§2.1. Baud defaults to 9600 because it matches the BCE default and
sits in the middle of the supported range; the MSE uses 19200 and the
WZA 1200, so callers that care will supply the right value or rely on
:func:sartoriuslib.open_device auto-detection. Note: all three
families ship from the factory in SBI mode (WZA in SBI autoprint at
1200-7-O-1); xBPI is reached by a front-panel menu change.
exclusive defaults True so two processes can't scribble over
the same device — neither xBPI nor SBI is multi-master tolerant.
The bytesize / parity / stopbits fields are typed as
the enums and that is what callers should statically pass. Runtime
accepts an equivalent int/str shorthand (e.g.
stopbits=1, parity="odd") — __post_init__ normalises
the value to the enum and raises
:class:SartoriusConfigurationError on anything the coercer can't
recognise. The static types stay strict so type-checkers point bad
callers at the enum form, while the runtime stays forgiving for
ad-hoc scripts and front-panel-derived values
(os.environ["SBI_PARITY"] → "odd"). The widened
constructor types are exposed only via the # type: ignore lines
in :meth:__post_init__ — :class:SerialTransport and
:mod:anyserial below this layer can always rely on the field
being an enum member.
Transport ¶
Bases: Protocol
Byte-level transport.
Every I/O boundary takes an explicit timeout. On expiry, implementations
raise :class:sartoriuslib.errors.SartoriusTimeoutError — never return an
empty or partial bytes silently. Backend exceptions normalize to
:class:sartoriuslib.errors.SartoriusTransportError (or a subclass) with
__cause__ preserving the original exception.
Lifecycle is single-shot: :meth:open once, :meth:close once.
:meth:reopen closes + reopens with any subset of serial-setting
overrides, used for the WZA SBI→xBPI protocol flip that also swaps baud
and parity.
close
async
¶
drain_input
async
¶
open
async
¶
read_available
async
¶
Read until the line goes idle for idle_timeout seconds.
Never raises on idle expiry — an idle timeout is the expected exit. Returns whatever was accumulated (possibly empty). Used for best-effort drain, passive SBI autoprint sniffing during protocol detection, and stream-stop recovery.
Source code in src/sartoriuslib/transport/base.py
read_exact
async
¶
Read exactly n bytes.
The canonical shape for xBPI framing: read one byte to discover
the length, then read that many bytes. Raises
:class:sartoriuslib.errors.SartoriusTimeoutError if fewer than
n bytes arrive before timeout. Partial buffers are retained
for the next call — implementations must not discard them.
Source code in src/sartoriuslib/transport/base.py
read_until
async
¶
Read bytes up to and including the next occurrence of separator.
Raises :class:sartoriuslib.errors.SartoriusTimeoutError if the
separator does not arrive before timeout. Bytes received
after the separator remain buffered for the next call —
implementations must not discard them.
Source code in src/sartoriuslib/transport/base.py
reopen
async
¶
Close and re-open the port, optionally changing serial framing.
Used by :meth:Balance.configure_protocol for the WZA SBI→xBPI
flip, which swaps both baud rate and parity, and by operations
like set_baud_rate that retune the port after the device has
already switched mid-sequence. Any argument left as None keeps
the existing value.
Implementations must leave the transport in a consistent state: either fully reopened with the new settings, or clearly closed so callers can recognise a failure. Silent partial states are the worst failure mode for this method.
Non-serial transports (e.g. a future TCP adapter) may raise
:class:NotImplementedError — baud/parity don't apply there.
Source code in src/sartoriuslib/transport/base.py
write
async
¶
Write every byte of data.
Raises :class:sartoriuslib.errors.SartoriusTimeoutError on
expiry. A bounded write timeout is mandatory because sends can
block on RS-485 hardware flow control or a stuck device. Callers
that block indefinitely hide real hangs.
Source code in src/sartoriuslib/transport/base.py
Serial transport¶
sartoriuslib.transport.serial ¶
Serial-port transport backed by :mod:anyserial.
:class:SerialTransport wraps :class:anyserial.SerialPort. Every I/O call
is bounded by :func:anyio.fail_after (reads, writes) or
:func:anyio.move_on_after (idle-timeout reads). Backend exceptions
normalize to :mod:sartoriuslib.errors types with __cause__ preserved.
Design reference: docs/design.md §8.1.
SerialTransport ¶
:class:Transport backed by a real serial port via anyserial.
Tests that don't need hardware can use
:class:sartoriuslib.transport.fake.FakeTransport instead; the two
conform to the same structural :class:Transport protocol.
Source code in src/sartoriuslib/transport/serial.py
reopen
async
¶
Close and reopen the port with any subset of overrides.
Any argument left as None keeps the existing setting. Used for
the WZA SBI→xBPI flip (swaps both baud and parity) and for
set_baud_rate retuning after the device has already switched.
The cached :class:SerialSettings is updated so the new settings
survive subsequent close / open round-trips.
If :meth:open fails on the new settings the transport is left
closed; the caller is responsible for surfacing that as a
BROKEN session state with recovery guidance.
Source code in src/sartoriuslib/transport/serial.py
Fake transport¶
sartoriuslib.transport.fake ¶
In-process fake transport for tests.
:class:FakeTransport implements the :class:Transport Protocol without
touching a serial port. Tests script the expected write→response mapping and
assert the recorded command bytes.
Re-exported from :mod:sartoriuslib.testing alongside fixture-parsing helpers.
Design reference: docs/design.md §8.2.
FakeTransport ¶
Scripted :class:Transport for tests.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
script
|
Mapping[bytes, ScriptedReply] | None
|
Mapping of |
None
|
label
|
str
|
Identifier used in errors. |
'fake://test'
|
latency_s
|
float
|
Per-operation artificial delay, useful for simulating a slow device. |
0.0
|
Source code in src/sartoriuslib/transport/fake.py
last_reopen_baud
property
¶
Baud rate requested by the most recent :meth:reopen, or None.
last_reopen_parity
property
¶
Parity requested by the most recent :meth:reopen, or None.
last_reopen_stopbits
property
¶
Stop bits requested by the most recent :meth:reopen, or None.
add_script ¶
feed ¶
Push unsolicited bytes into the read buffer.
Useful for simulating a device that was left in SBI autoprint mode, or garbage on the line that the session must drain on recovery.
Source code in src/sartoriuslib/transport/fake.py
force_read_timeout ¶
force_reopen_error ¶
force_write_timeout ¶
reopen
async
¶
Simulate a serial-setting change — close, record, reopen.
If force_reopen_error() has been called the reopen raises
:class:SartoriusConnectionError after the close, leaving the
transport closed. That is the "reopen wedged" path used by
:meth:Balance.configure_protocol tests for the BROKEN-state
transition.