sartoriuslib.protocol¶
ProtocolKind, ProtocolClient, detect_protocol(), and the xBPI /
SBI sub-packages. See Design §4 and the
wire-protocol reference.
Public surface¶
sartoriuslib.protocol ¶
Protocol layer — framing, parsing, and protocol-client adapters.
xBPI and SBI each have a full subpackage under here; the shared
:class:ProtocolClient protocol and :class:ProtocolKind live at this
level. See design doc §2 layer map and §4.
DetectionResult
dataclass
¶
Outcome of :func:detect_protocol.
Attributes:
| Name | Type | Description |
|---|---|---|
protocol |
ProtocolKind
|
The resolved :class: |
autoprint_active |
bool
|
|
pending_lines |
tuple[bytes, ...]
|
Complete CRLF-terminated SBI lines consumed during
the sniff that the caller may want to re-queue on the live
client. Empty unless |
ProtocolClient ¶
Bases: Protocol
One request/response round-trip over a transport.
Every implementation owns an :class:anyio.Lock (exposed as
:attr:lock) that serialises calls on the shared transport. Session
code does not hold its own lock — it piggybacks on the client's, so
two sessions on the same port collapse to one serialized queue when
they (exceptionally) share a client.
Errors the device tells us about (xBPI subtype 0x01, SBI refusal
lines) are raised as typed exceptions inside :meth:execute —
callers decode only success replies. Transport-level failures
(timeout, connection) surface as the corresponding
:class:sartoriuslib.errors.SartoriusTransportError subclass with
__cause__ preserving the original.
dispose ¶
execute
async
¶
Send request and return the decoded reply.
Raises:
| Type | Description |
|---|---|
SartoriusTimeoutError
|
No response within |
SartoriusConnectionError
|
Port closed or lost mid-exchange. |
SartoriusFrameError
|
Framing / checksum invalid. |
SartoriusCapabilityError(subclass)
|
Device responded with a
recognised refusal code (xBPI |
SartoriusCommandRejectedError
|
Refusal code outside the classified set. |
Source code in src/sartoriuslib/protocol/base.py
ProtocolKind ¶
Bases: StrEnum
Which wire protocol is active on a session.
AUTO is only valid at open_device call time; by the time a
session exists, AUTO has resolved to XBPI or SBI.
detect_protocol
async
¶
detect_protocol(
transport,
*,
timeout=_DEFAULT_PROBE_TIMEOUT,
sniff_window=_DEFAULT_SNIFF_WINDOW,
src_sbn=HOST_SBN_DEFAULT,
dst_sbn=BALANCE_SBN_DEFAULT,
)
Detect xBPI vs SBI on an already-open transport.
Runs the conservative sequence from design §4.3 in order — drain → passive sniff → xBPI probe → SBI probe → fail. Each probe writes at most one frame. The transport's serial settings are never changed.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
transport
|
Transport
|
An open :class: |
required |
timeout
|
float
|
Per-probe timeout for the xBPI and SBI identity probes. |
_DEFAULT_PROBE_TIMEOUT
|
sniff_window
|
float
|
Passive listen window for SBI autoprint, in seconds. |
_DEFAULT_SNIFF_WINDOW
|
src_sbn
|
int
|
Source SBN for the xBPI probe frame ( |
HOST_SBN_DEFAULT
|
dst_sbn
|
int
|
Destination SBN for the xBPI probe frame ( |
BALANCE_SBN_DEFAULT
|
Returns:
| Name | Type | Description |
|---|---|---|
A |
DetectionResult
|
class: |
DetectionResult
|
|
|
DetectionResult
|
carries the sniffed bytes for re-queue. |
Raises:
| Type | Description |
|---|---|
SartoriusError
|
No xBPI or SBI device responded — neither the
passive sniff nor either probe produced a recognisable reply.
Hard transport faults (e.g. the port closed mid-detect)
propagate as :class: |
Source code in src/sartoriuslib/protocol/detect.py
84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 | |
make_protocol_client ¶
Build the concrete protocol client for protocol on transport.
:attr:AUTO is never valid at factory time — the detection step
resolves it first.
Source code in src/sartoriuslib/protocol/client.py
Base Protocol¶
sartoriuslib.protocol.base ¶
Shared ProtocolClient Protocol and :class:ProtocolKind enum.
ProtocolKind is named with the Kind suffix to avoid colliding with
:class:typing.Protocol at import sites. See design doc §7.
:class:ProtocolClient is the structural interface both
:class:XbpiProtocolClient and :class:SbiProtocolClient satisfy. A
session holds at most one client per protocol; both may be present
when AUTO detection resolves to one.
ProtocolClient ¶
Bases: Protocol
One request/response round-trip over a transport.
Every implementation owns an :class:anyio.Lock (exposed as
:attr:lock) that serialises calls on the shared transport. Session
code does not hold its own lock — it piggybacks on the client's, so
two sessions on the same port collapse to one serialized queue when
they (exceptionally) share a client.
Errors the device tells us about (xBPI subtype 0x01, SBI refusal
lines) are raised as typed exceptions inside :meth:execute —
callers decode only success replies. Transport-level failures
(timeout, connection) surface as the corresponding
:class:sartoriuslib.errors.SartoriusTransportError subclass with
__cause__ preserving the original.
dispose ¶
execute
async
¶
Send request and return the decoded reply.
Raises:
| Type | Description |
|---|---|
SartoriusTimeoutError
|
No response within |
SartoriusConnectionError
|
Port closed or lost mid-exchange. |
SartoriusFrameError
|
Framing / checksum invalid. |
SartoriusCapabilityError(subclass)
|
Device responded with a
recognised refusal code (xBPI |
SartoriusCommandRejectedError
|
Refusal code outside the classified set. |
Source code in src/sartoriuslib/protocol/base.py
ProtocolKind ¶
Bases: StrEnum
Which wire protocol is active on a session.
AUTO is only valid at open_device call time; by the time a
session exists, AUTO has resolved to XBPI or SBI.
Client factory¶
sartoriuslib.protocol.client ¶
Protocol-client factory — xBPI vs SBI selection.
make_protocol_client ¶
Build the concrete protocol client for protocol on transport.
:attr:AUTO is never valid at factory time — the detection step
resolves it first.
Source code in src/sartoriuslib/protocol/client.py
Auto-detection¶
sartoriuslib.protocol.detect ¶
Conservative protocol auto-detection.
Drain input → passively sniff for SBI autoprint → probe xBPI (0x02) →
probe SBI (ESC x1_ then ESC P fallback) → fail clearly. Never sweeps
opcodes, never changes baud, never changes the balance's protocol mode. See
design doc §4.3.
The SBI probe tries ESC x1_ first (which gives an identity string we
can validate later) and falls back to ESC P (a print/weight read) if
the identity token is silent. The ESC P fallback is kept as
defense-in-depth for unknown firmware revisions: an earlier hardware-day
note claimed Cubis MSE1203S silently ignored Format-2 identity tokens,
but re-testing on 2026-04-25 (MSE1203S-100-DR, BAC 00-39-21) showed
all three identity tokens reply cleanly. Both probes are READ_ONLY,
so the fallback costs nothing on devices that do reply to ESC x1_.
The result is a :class:DetectionResult carrying the resolved
:class:ProtocolKind plus, when autoprint was observed, the sniffed line so
the caller can re-queue it on the eventual SBI client and not lose the first
sample. Anything observed during the sniff that does not match an autoprint
pattern is discarded — those bytes are ambiguous and we drain before each
subsequent probe.
DetectionResult
dataclass
¶
Outcome of :func:detect_protocol.
Attributes:
| Name | Type | Description |
|---|---|---|
protocol |
ProtocolKind
|
The resolved :class: |
autoprint_active |
bool
|
|
pending_lines |
tuple[bytes, ...]
|
Complete CRLF-terminated SBI lines consumed during
the sniff that the caller may want to re-queue on the live
client. Empty unless |
detect_protocol
async
¶
detect_protocol(
transport,
*,
timeout=_DEFAULT_PROBE_TIMEOUT,
sniff_window=_DEFAULT_SNIFF_WINDOW,
src_sbn=HOST_SBN_DEFAULT,
dst_sbn=BALANCE_SBN_DEFAULT,
)
Detect xBPI vs SBI on an already-open transport.
Runs the conservative sequence from design §4.3 in order — drain → passive sniff → xBPI probe → SBI probe → fail. Each probe writes at most one frame. The transport's serial settings are never changed.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
transport
|
Transport
|
An open :class: |
required |
timeout
|
float
|
Per-probe timeout for the xBPI and SBI identity probes. |
_DEFAULT_PROBE_TIMEOUT
|
sniff_window
|
float
|
Passive listen window for SBI autoprint, in seconds. |
_DEFAULT_SNIFF_WINDOW
|
src_sbn
|
int
|
Source SBN for the xBPI probe frame ( |
HOST_SBN_DEFAULT
|
dst_sbn
|
int
|
Destination SBN for the xBPI probe frame ( |
BALANCE_SBN_DEFAULT
|
Returns:
| Name | Type | Description |
|---|---|---|
A |
DetectionResult
|
class: |
DetectionResult
|
|
|
DetectionResult
|
carries the sniffed bytes for re-queue. |
Raises:
| Type | Description |
|---|---|
SartoriusError
|
No xBPI or SBI device responded — neither the
passive sniff nor either probe produced a recognisable reply.
Hard transport faults (e.g. the port closed mid-detect)
propagate as :class: |
Source code in src/sartoriuslib/protocol/detect.py
84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 | |
xBPI — framing¶
sartoriuslib.protocol.xbpi.framing ¶
xBPI frame codec.
Two concrete operations:
- :func:
build_command— assemble a host→balance TX frame. - :func:
parse_frame— validate and decompose a balance→host RX frame into an :class:XbpiFrame.
TX frame::
[len] [src_sbn] [dst_sbn] [opcode] [args...] [chk]
RX frame::
[len] [marker=0x41] [subtype] [body...] [chk]
len counts every byte that follows the length byte (including the
checksum). chk is sum(every preceding byte) & 0xFF. Defaults:
src_sbn=0x01 (host convention) and dst_sbn=0x09 (balance factory
default). See docs/protocol.md §3.
build_command ¶
Assemble a host→balance frame.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
opcode
|
int
|
Command byte ( |
required |
args
|
bytes
|
Pre-encoded argument bytes (usually one or more TLVs — see
:mod: |
b''
|
src_sbn
|
int
|
Source SBN; defaults to the host convention |
HOST_SBN_DEFAULT
|
dst_sbn
|
int
|
Destination SBN; defaults to the balance factory
default |
BALANCE_SBN_DEFAULT
|
Returns:
| Type | Description |
|---|---|
bytes
|
The complete frame bytes, length-prefixed and checksummed, ready |
bytes
|
to hand to :meth: |
Source code in src/sartoriuslib/protocol/xbpi/framing.py
checksum ¶
Return sum(data) & 0xFF — the xBPI frame checksum.
Trivial by design. No CRC, no seed; see docs/protocol.md §12.
parse_frame ¶
Validate and decompose a balance→host frame.
Raises:
| Type | Description |
|---|---|
SartoriusFrameError
|
Frame too short, length byte inconsistent
with buffer size, marker is not |
Source code in src/sartoriuslib/protocol/xbpi/framing.py
xBPI — types¶
sartoriuslib.protocol.xbpi.types ¶
Immutable xBPI wire-level types.
:class:XbpiFrame is what :func:sartoriuslib.protocol.xbpi.framing.parse_frame
produces from a balance→host reply: parsed but not yet interpreted.
:class:SubtypeFamily groups the reply subtype byte into the families from
docs/protocol.md §4. Decoded bodies (measurement, status, typed-float,
error) live here as frozen dataclasses too.
Per-protocol types live here; the protocol-neutral public :class:Reading
that the :class:Balance facade returns is composed from these.
ErrorBody
dataclass
¶
1-byte error body (subtype 0x01).
code is the raw device error code; mapping to typed exceptions
happens at the protocol-client / session layer so the codec can stay
agnostic.
LongMeasurementBody
dataclass
¶
17-byte long streaming-measurement body (subtype 0x48).
Emitted when the caller requests 0x1E 09 30 (short + status-block
concatenation per docs/protocol.md §8.3). The delimiter byte is
always 0x48 — the same value as the subtype — and separates the
two 8-byte halves.
MeasurementBody
dataclass
¶
8-byte short measurement body (subtype 0x48, non-status-block form).
Raw fields mirror docs/protocol.md §8.1 byte-for-byte. Derived
fields (value, unit, sign, stable, decimals,
off_scale) are the decoded view callers build a :class:Reading
from.
value is None on the off-scale sentinel (bytes[0..4] ==
7f ff ff ff ff). Distinguishing overload from underload requires
the status block; the measurement alone cannot tell them apart.
StatusBlockBody
dataclass
¶
StatusBlockBody(
raw,
aux_flag,
state,
status,
sequence,
stable,
overload,
underload,
adc_trusted,
isocal_due,
)
8-byte status block (subtype 0x48, from opcode 0x30).
See docs/protocol.md §8.2. State/status encoding is family-specific;
the derived stable / overload / underload flags extract the
portable bits. adc_trusted and isocal_due are MSE-only signals
and are None when the source is not a Cubis.
SubtypeFamily ¶
Bases: IntEnum
Top-level classifier for the reply subtype byte.
The xBPI subtype byte packs a family (high nibble) and a body-length
hint (low nibble, per docs/protocol.md §4). Parsers dispatch on
family, not on the raw subtype, so a new same-family subtype does not
need a parser change.
TypedFloatBody
dataclass
¶
5-byte typed-float body (subtype 0x35).
Used by temperature, capacity, and increment reads. The aux byte
is an extra payload byte that carries unit-family or decimal-place
information depending on the opcode.
XbpiFrame
dataclass
¶
One fully-validated balance→host xBPI frame.
length is the value of byte[0] — the count of bytes that follow
the length byte (so len(raw) == length + 1). marker is
byte[1], always 0x41 for device-to-host replies;
:func:parse_frame raises :class:SartoriusFrameError otherwise.
body is everything between the subtype and the checksum; raw
is the full on-wire bytes.
xBPI — tables¶
sartoriuslib.protocol.xbpi.tables ¶
xBPI opcode, subtype, and error-code tables.
Tables in this module are lookup data — pure-Python dicts with no
behaviour. The codec treats every table as an open set: unknown values
decode to :attr:SubtypeFamily.UNKNOWN or an otherwise-empty mapping
lookup rather than raising. Forward-compatibility beats strictness here
because Sartorius firmware revisions routinely introduce new subtypes.
References: docs/protocol.md §4 (subtype families), §6 (error codes),
§7 (opcode inventory).
body_length_for_subtype ¶
Expected body length for subtype, or None if variable.
Implements the §4 formula: "high nibble = type class, low nibble =
body length for types 0..4; for type 5, length = 16 + low". Returns
None for subtypes whose body length is genuinely variable
(notably 0x48 which carries either 8 or 17 bytes, and 0x00
which usually carries 0 but occasionally a variable body — e.g.
opcode 0xBC module list).
Source code in src/sartoriuslib/protocol/xbpi/tables.py
subtype_family ¶
Return the family classifier for a reply subtype byte.
Falls back to high-nibble dispatch for subtypes not explicitly
tabulated, and to :attr:SubtypeFamily.UNKNOWN when even that fails.
Source code in src/sartoriuslib/protocol/xbpi/tables.py
xBPI — TLV¶
sartoriuslib.protocol.xbpi.tlv ¶
xBPI TLV (tag-length-value) helpers.
Per docs/protocol.md §5, Cubis MSE requires request args wrapped as
TLV records rather than plain byte arguments. The tag's low nibble encodes
the value size:
+------+-------+---------------------------------------------+ | Tag | Size | Meaning | +======+=======+=============================================+ | 0x11 | 1 B | u8 (rare in requests) | | 0x12 | 2 B | u16 BE | | 0x14 | 4 B | u32 BE | | 0x21 | 1 B | u8 — the most common request-arg wrapper | | 0x22 | 2 B | u16 BE (seen in response bodies) | | 0x24 | 4 B | u32 BE (seen in response bodies) | +------+-------+---------------------------------------------+
Response bodies may contain multiple concatenated TLVs (see §5.2), so this
module also exposes :func:parse_tlv_sequence for walking them.
decode_tlv ¶
Decode one TLV record starting at offset.
Returns (tag, value_bytes, next_offset). Raises
:class:SartoriusFrameError on unknown tags or truncated values.
Source code in src/sartoriuslib/protocol/xbpi/tlv.py
encode_tlv ¶
Encode a single TLV record.
value may be an int (encoded big-endian into the tag's size)
or raw bytes (emitted verbatim, length-checked against the tag).
Source code in src/sartoriuslib/protocol/xbpi/tlv.py
parse_tlv_sequence ¶
Walk body as a concatenation of TLV records.
Returns a list of (tag, value_bytes) tuples. Raises
:class:SartoriusFrameError if any record is truncated or any tag is
unknown.
Note: parameter-table replies (opcode 0x55) have the subtype byte
double as the first TLV tag per §5.3 — the caller must prepend the
subtype byte before passing the body here.
Source code in src/sartoriuslib/protocol/xbpi/tlv.py
xBPI — units¶
sartoriuslib.protocol.xbpi.units ¶
xBPI unit-code decoding.
xBPI measurement frames pack the unit and sign into byte [6] of the 8-byte
body and the decimal-place count into byte [5]'s high nibble. See
docs/protocol.md §8.4 for the full encoding.
decode_decimals ¶
Return the displayed decimal-place count encoded in byte [5].
High nibble = decimals (docs/protocol.md §8.4). The low nibble
has a WZA-mg quirk that is advisory and ignored here.
Source code in src/sartoriuslib/protocol/xbpi/units.py
decode_sign ¶
Decode the sign bits (top 2) of byte [6].
0x00 = exactly zero, 0x40 = positive, 0x80 = negative.
Any other combination (shouldn't occur — only one bit of the 2-bit
field is set at a time on the wire) decodes to :attr:Sign.UNKNOWN.
Source code in src/sartoriuslib/protocol/xbpi/units.py
decode_unit ¶
unit_byte_to_unit ¶
Map a 6-bit base-unit ID to a :class:Unit.
Unknown IDs decode to :attr:Unit.UNKNOWN — never raises — because
new unit codes are expected as more of the parameter-table display
enum gets mapped (see docs/protocol.md §10.1 index 7).
Source code in src/sartoriuslib/protocol/xbpi/units.py
xBPI — parser¶
sartoriuslib.protocol.xbpi.parser ¶
xBPI subtype decoders — measurement, status block, typed float, errors.
This module turns an :class:XbpiFrame's body into one of the
decoded-body dataclasses from :mod:sartoriuslib.protocol.xbpi.types.
It knows nothing about opcodes or sessions; callers at the command
layer pick the right decoder based on what they sent.
References: docs/protocol.md §8 (measurement/status decoding), §9
(temperature), §4 (subtype encoding).
decode_error_body ¶
Decode a 1-byte error body into :class:ErrorBody.
The single byte is the device error code (0x03 / 0x04 /
0x06 / 0x07 / 0x10 / 0x11 — see
docs/protocol.md §6). Mapping to typed exceptions happens at
the session layer; this decoder stays neutral.
Source code in src/sartoriuslib/protocol/xbpi/parser.py
decode_long_measurement_body ¶
Decode a 17-byte long streaming measurement (§8.3).
Layout is short measurement (8 B) + delimiter (1 B, always 0x48)
+ status block (8 B). The delimiter matches the subtype byte by
coincidence.
Source code in src/sartoriuslib/protocol/xbpi/parser.py
decode_measurement_body ¶
Decode an 8-byte short measurement body.
The value is float32 big-endian in bytes [0..3]. Bytes [5..6]
pack decimals (high nibble of [5]), sign (top 2 bits of [6]), and
the base-unit ID (low 6 bits of [6]). Byte [7]'s 0x40 bit marks
a stable reading universally across families. An off-scale reading
presents the 7f ff ff ff ff sentinel in bytes [0..4]; the
status block disambiguates overload vs underload, so this decoder
just reports off_scale and leaves value as None.
.. note::
The MSE Cubis emits the same ``7f ff ff ff ff`` sentinel for
~6 frames (~2 s) immediately after :meth:`Balance.zero` while
the cell recomputes its zero point. The wire is ambiguous —
from the body alone we can't tell "cell busy" apart from
"overload" or "underload". Callers that need that distinction
invoke :meth:`Balance.status` (xBPI ``0x32``); the status
block's ``state`` byte carries the disambiguation
(``0x82``/``0x84`` for overload/underload, plain stable/
unstable for a settling cell). Observed on hardware day —
:class:`Reading` faithfully reflects ``value=None`` /
``off_scale=True`` either way.
Source code in src/sartoriuslib/protocol/xbpi/parser.py
decode_status_block_body ¶
Decode an 8-byte status block.
See docs/protocol.md §8.2. The portable stability bit is
state & 0x08 — Cubis encodes state=0x88 stable vs 0x80
unstable (the 0x80 base marks "measurement valid"), while
WZA/BCE use state=0x08 vs 0x00. Overload (state 0x82)
and underload (0x84) have only been captured on Cubis; on
non-Cubis this decoder still reports them when the exact pattern
matches, and False otherwise.
adc_trusted and isocal_due are MSE-only; decoded as None
when the frame does not look like a Cubis-style status block (base
bit 0x80 clear in state).
Source code in src/sartoriuslib/protocol/xbpi/parser.py
decode_typed_float_body ¶
Decode a 5-byte typed-float body: float32 BE + 1-byte aux.
Source code in src/sartoriuslib/protocol/xbpi/parser.py
is_status_block_body ¶
Heuristic: does this 8-byte body look like a status block?
Short measurements and status blocks share subtype 0x48 and both
carry 8-byte bodies. Disambiguation uses the §8.2 marker pattern:
bytes [1..2] == 00 81 and bytes [5..6] == 10 00 reliably
identify a status block; short measurements only match by accident.
Source code in src/sartoriuslib/protocol/xbpi/parser.py
xBPI — client¶
sartoriuslib.protocol.xbpi.client ¶
xBPI protocol client — single-in-flight request/response over a transport.
Holds an :class:anyio.Lock shared across one full exchange. Runs the
xBPI length-prefix read sequence (read_exact(1) then
read_exact(length)), validates the frame via
:func:sartoriuslib.protocol.xbpi.framing.parse_frame, and maps
subtype-0x01 error replies to typed
:class:sartoriuslib.errors.SartoriusError subclasses.
Transport errors propagate unchanged. Framing errors and error-subtype
replies both trigger a best-effort :meth:Transport.drain_input before
re-raising, so the next call starts from a clean buffer.
Design reference: doc §4.1 (protocol-duality seam), §6.1.1 (response-to-availability mapping).
XbpiProtocolClient ¶
xBPI request/response client over a :class:Transport.
Single-in-flight via an internal :class:anyio.Lock. The lock is
also exposed so :class:sartoriuslib.devices.session.Session (and
the multi-device manager) can share serialisation with other
machinery on the same port.
Source code in src/sartoriuslib/protocol/xbpi/client.py
dispose ¶
execute
async
¶
Send request and return the decoded reply frame.
Holds :attr:lock for the full exchange. Drains the input
buffer on framing errors and on error-subtype replies so the
next call is not corrupted by a partial response.
Source code in src/sartoriuslib/protocol/xbpi/client.py
SBI — framing¶
sartoriuslib.protocol.sbi.framing ¶
SBI line codec and ESC-token helpers.
build_command ¶
Build an SBI command token, optionally appending a terminator.
is_autoprint_line ¶
Return True when line looks like an unsolicited weight line.
Source code in src/sartoriuslib/protocol/sbi/framing.py
normalize_token ¶
Normalize a user-facing SBI command into on-wire bytes.
Accepts raw bytes, strings containing the literal escape character, or
readable forms like "ESC P" and "ESC x1_". Trailing CR/LF is
stripped because Sartorius documents command terminators as optional and
the existing fake-transport fixtures use the bare token.
Source code in src/sartoriuslib/protocol/sbi/framing.py
split_lines ¶
Split a raw SBI payload into complete lines.
Every non-empty line must include \r\n (or at least \n for
lenient fixture parsing). Returns lines including their terminators.
Source code in src/sartoriuslib/protocol/sbi/framing.py
strip_line_terminator ¶
Remove one SBI line terminator if present.
Source code in src/sartoriuslib/protocol/sbi/framing.py
SBI — types¶
sartoriuslib.protocol.sbi.types ¶
Immutable SBI wire-level types.
SBI is line-oriented ASCII. The transport reads complete \r\n-terminated
records; the parser turns each record into an :class:SbiLine and collects
them into an :class:SbiReply for command variants to decode.
SbiLine
dataclass
¶
One parsed SBI line.
raw includes the on-wire line terminator if it was present.
text is ASCII-decoded with the terminator stripped. reading is
populated only for weight/autoprint lines.
SbiLineKind ¶
Bases: StrEnum
Classifier for one decoded SBI line.
SbiReply
dataclass
¶
One SBI reply.
lines holds parsed records; raw is the concatenated on-wire
payload that produced them. No-response commands such as front-panel
key emulation use an empty lines tuple and raw=b"".
SBI — tables¶
sartoriuslib.protocol.sbi.tables ¶
SBI — parser¶
sartoriuslib.protocol.sbi.parser ¶
SBI line parser — command replies and autoprint output.
parse_line ¶
Parse one SBI line.
Weight/autoprint lines get a decoded :class:Reading; other printable
lines are preserved as identity text. Truly undecodable bytes raise
:class:SartoriusParseError.
Source code in src/sartoriuslib/protocol/sbi/parser.py
parse_reply ¶
Parse one raw SBI payload into an :class:SbiReply.
parse_weight_line ¶
Decode an SBI weight/autoprint line into a protocol-neutral reading.
Source code in src/sartoriuslib/protocol/sbi/parser.py
require_identity_text ¶
Return the first text-bearing line suitable for identity commands.
SBI identity fields may be numeric-only (serial numbers, software
versions), so a line that also looks like a numeric display value is still
valid text in this command context by default. Callers that expect a
non-numeric identity field, such as the model string, can set
allow_weight_like=False to avoid mistaking an autoprint reading for a
command reply. Refusal and special status lines are excluded.
Source code in src/sartoriuslib/protocol/sbi/parser.py
require_reading ¶
Return the first reading in reply or raise a parse error.
Source code in src/sartoriuslib/protocol/sbi/parser.py
SBI — client¶
sartoriuslib.protocol.sbi.client ¶
SBI protocol client — single-in-flight line request/response.
SbiProtocolClient ¶
SBI request/response client over a :class:Transport.
SBI replies are ASCII lines. Some control commands emulate front-panel
keypresses and do not produce an acknowledgement; callers pass
expect_lines=0 for those and receive an empty :class:SbiReply.
Source code in src/sartoriuslib/protocol/sbi/client.py
autoprint_active
property
¶
Whether passive sniffing has observed unsolicited SBI output.
detect_autoprint
async
¶
Passively sniff for unsolicited SBI autoprint/status lines.
Any complete lines read during detection are queued so the first stream read still sees them. This keeps detection cheap and honest: it never writes to the balance and never loses the first sample.
Source code in src/sartoriuslib/protocol/sbi/client.py
dispose ¶
execute
async
¶
Write request and parse expect_lines SBI reply lines.
Source code in src/sartoriuslib/protocol/sbi/client.py
mark_autoprint_active ¶
Record that callers have successfully consumed autoprint output.
mark_autoprint_inactive ¶
read_line
async
¶
Read and parse one unsolicited SBI line without writing first.
Source code in src/sartoriuslib/protocol/sbi/client.py
refresh_autoprint_state
async
¶
Re-sniff autoprint state after a user-side mode change.
Unlike :meth:detect_autoprint, this is an explicit resync point:
pending unsolicited lines are discarded before sniffing. A quiet line
clears :attr:autoprint_active; observed autoprint/status output sets
it and queues the observed line for later consumption.