sartoriuslib.devices¶
Balance, Session, the public dataclasses (Reading, BalanceStatus,
DeviceInfo, …), BalanceFamily, Capability, SafetyTier,
open_device, and discovery helpers. See Balances,
Readings, and Design §5–§7.
Public surface¶
sartoriuslib.devices ¶
Device-layer public surface.
Re-exports the enums, capability bitmap, and (once implemented) the
:class:Balance facade and session machinery. See design doc §5 and §6.
Availability ¶
Bases: StrEnum
Derived state the session consults when dispatching a command.
See design doc §5.1, §6.1.1.
INAPPLICABLE
class-attribute
instance-attribute
¶
Device responded with xBPI 0x06. Retryable; state-dependent.
SUPPORTED
class-attribute
instance-attribute
¶
Directly confirmed by a successful call or probe.
UNKNOWN
class-attribute
instance-attribute
¶
Never exercised; priors may exist but no device observation yet.
UNSUPPORTED
class-attribute
instance-attribute
¶
Device responded with xBPI 0x04 / equivalent SBI refusal. Sticky per session.
BalanceFamily ¶
Bases: StrEnum
Classification from the model string returned by xBPI 0x02 or SBI identify.
- :attr:
CUBIS— MSE and related Cubis strings; full xBPI plus Cubis extensions. - :attr:
OEM_WEIGH_CELL— WZ/WZA; ships from the factory in SBI autoprint (1200-7-O-1) and requires a front-panel menu change to switch to xBPI. (MSE and BCE also ship in SBI by default — switching to xBPI is a front-panel menu change on every family.) - :attr:
BASIC_LAB— BCE*; MSE opcode subset, no Cubis extensions. - :attr:
UNKNOWN— anything we have not classified; every call becomes a live probe.
Capability ¶
Bases: Flag
Feature capabilities derived from family defaults + live probing.
Flag bitmap carries capabilities currently believed SUPPORTED. Full
tri/quad-state per capability lives in DeviceInfo.probe_report.
ProbeSource ¶
Bases: StrEnum
Where an :class:Availability value came from.
FAMILY_TABLE
class-attribute
instance-attribute
¶
Seeded prior from our captures.
LIVE_CALL
class-attribute
instance-attribute
¶
Updated by the device's response to a normal command.
TARGETED_PROBE
class-attribute
instance-attribute
¶
Explicit probe during identify() / discovery.
USER_OVERRIDE
class-attribute
instance-attribute
¶
Set explicitly by the caller.
SafetyTier ¶
Bases: IntEnum
Per-command safety tier. See design doc §6.1.
DANGEROUS
class-attribute
instance-attribute
¶
Baud/SBN change, reset, calibration init, protocol switch. Requires confirm=True.
PERSISTENT
class-attribute
instance-attribute
¶
Parameter writes, save menu, communication settings. Requires confirm=True.
READ_ONLY
class-attribute
instance-attribute
¶
Weight, status, identity, capacity, increment, temperature, parameter reads.
STATEFUL
class-attribute
instance-attribute
¶
Transient state change (tare, zero). No EEPROM write.
Session ¶
Session(
*,
xbpi_client=None,
sbi_client=None,
active_protocol,
family=BalanceFamily.UNKNOWN,
capabilities=NO_CAPABILITY,
firmware=None,
src_sbn=1,
dst_sbn=9,
strict=False,
default_timeout=1.0,
serial_settings=None,
)
One balance, one serial port. Enforces gates, serialises I/O.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
xbpi_client
|
XbpiProtocolClient | None
|
Client for xBPI dispatch, or |
None
|
active_protocol
|
ProtocolKind
|
Which protocol this session currently speaks. |
required |
family
|
BalanceFamily
|
Balance family discriminator (from |
UNKNOWN
|
capabilities
|
Capability
|
Bitmap of capabilities believed present. |
NO_CAPABILITY
|
firmware
|
FirmwareVersion | None
|
Firmware version, if known. |
None
|
src_sbn
|
int
|
Host SBN address for xBPI frames. |
1
|
dst_sbn
|
int
|
Balance SBN address for xBPI frames. |
9
|
strict
|
bool
|
If |
False
|
default_timeout
|
float
|
Per-call timeout when the caller passes
|
1.0
|
Source code in src/sartoriuslib/devices/session.py
sbi_autoprint_active
property
¶
Whether the SBI session has observed unsolicited autoprint output.
serial_settings
property
¶
Serial settings the transport was opened with, if known.
Set at construction time by :func:sartoriuslib.open_device and
updated after a successful :meth:Balance.configure_protocol.
None for sessions built from a pre-existing :class:Transport
whose framing the library did not control.
transport
property
¶
The underlying :class:Transport, regardless of active protocol.
Both protocol clients hold the same transport — return whichever
is wired. The constructor enforces that the client matching
active_protocol is non-None, so this always returns a real
:class:Transport.
aclose
async
¶
Close the underlying transport, if one is wired.
Idempotent — safe to call multiple times. The factory owns the transport's construction and hands it into the session via the protocol client; closing the session closes the transport.
Both clients hold the same transport, so close it once via the
session's :attr:transport accessor rather than once per client
slot — guards against a future caller that wires both clients
simultaneously.
Source code in src/sartoriuslib/devices/session.py
availability_of ¶
Current availability for command_name (UNKNOWN if unseen).
cache_snapshot ¶
Copy of cache_key → counter_snapshot for test assertions.
The actual cached values are intentionally not surfaced — tests assert on the presence and counter pinning of an entry, not its decoded content.
Source code in src/sartoriuslib/devices/session.py
cached_execute
async
¶
Dispatch command with a 0xBA-keyed result cache.
Sessions without :attr:Capability.CONFIG_COUNTER fall through
to :meth:execute. When the capability is present, the session
re-reads the config counter before returning a cached value;
any change flushes the entry and the command re-runs.
cache_key is caller-supplied so a command that takes
arguments (capacity(area=N), read_parameter(idx)) can
cache separate entries per distinct call.
Source code in src/sartoriuslib/devices/session.py
check_state ¶
Raise :class:SartoriusConnectionError if the session is BROKEN.
Public alias for the internal gate run by every dispatch path,
so lifecycle helpers on :class:Balance can reuse the same
guard without poking at private attributes.
Source code in src/sartoriuslib/devices/session.py
execute
async
¶
Dispatch command with full pre-I/O gating.
Gates fire in the design doc §6.1 order; each raise happens before any byte is sent, so a gate failure is observably equivalent to the call never leaving the host.
Source code in src/sartoriuslib/devices/session.py
execute_raw_sbi
async
¶
Send an arbitrary SBI command token and return the parsed reply.
Source code in src/sartoriuslib/devices/session.py
execute_raw_xbpi
async
¶
Send an arbitrary xBPI opcode and return the raw reply frame.
Bypasses the declarative :class:Command layer — the opcode is
a per-call parameter, so none of the prior / capability gating
applies. The one hard gate is a safe-list check: opcodes in
:data:sartoriuslib.commands.raw.SAFE_READ_ONLY_OPCODES run
freely; anything else requires confirm=True because the
library cannot know it is safe.
The availability cache is not updated — raw calls are opaque at the command-name level.
Source code in src/sartoriuslib/devices/session.py
invalidate_cache ¶
Drop one cached entry, or clear all when cache_key is None.
The :class:Balance facade calls this after writes whose
0xBA bump isn't guaranteed (the §6.3 caveat). Unknown
keys are a silent no-op — idempotent.
Source code in src/sartoriuslib/devices/session.py
mark_broken ¶
Transition the session to :attr:SessionState.BROKEN.
Called only from lifecycle operations
(Balance.configure_protocol) when a rollback fails. Once
broken, every subsequent dispatch refuses with
:class:SartoriusConnectionError.
Source code in src/sartoriuslib/devices/session.py
read_sbi_autoprint_reading
async
¶
Read the next valid SBI autoprint weight line without writing.
Source code in src/sartoriuslib/devices/session.py
read_sbi_line
async
¶
Read one unsolicited SBI line, used by autoprint streaming.
Source code in src/sartoriuslib/devices/session.py
refresh_sbi_autoprint_state
async
¶
Passively re-sniff whether SBI autoprint is currently active.
Source code in src/sartoriuslib/devices/session.py
replace_clients ¶
Swap protocol clients atomically — used by configure_protocol.
The host-side flip closes the old protocol client, reopens the
transport at new serial framing, builds a new client, and
verifies. On verification success this method installs the new
clients and the new active protocol; the availability cache,
prior warnings, and result cache are all
cleared because the command surface changes when the protocol
does (xBPI-only commands have no SBI variant and vice versa,
and any 0xBA-pinned cache entries belong to the old session).
Refuses to install ProtocolKind.AUTO — detection must
resolve to XBPI or SBI first. Refuses if the
corresponding client for active_protocol is missing.
Source code in src/sartoriuslib/devices/session.py
set_dst_sbn ¶
Update the destination SBN.
Used after :meth:Balance.write_sbn_address on multidrop links
where the new address must address the device going forward.
Source code in src/sartoriuslib/devices/session.py
update_identity ¶
Replace session-level identity state after a live identify call.
Called by :func:sartoriuslib.devices.factory.open_device after
running the identify commands, so subsequent prior gating sees
the discovered family and capabilities instead of the
placeholder UNKNOWN / empty values.
Each argument left as None keeps the existing value.
Source code in src/sartoriuslib/devices/session.py
Public dataclasses¶
sartoriuslib.devices.models ¶
Public frozen dataclasses returned by the :class:Balance facade.
See design doc §7. All types are immutable (frozen=True, slots=True) so
they are safe to share, pass across task boundaries, and log.
Reading and BalanceStatus are protocol-neutral: the xBPI decoder
and the Phase-7 SBI decoder both build the same shape. That is the whole
point of the dual-protocol seam (design §4).
BalanceState ¶
Bases: StrEnum
High-level weighing state derived from the status block.
BalanceStatus
dataclass
¶
Status-block snapshot from xBPI 0x30 (or SBI equivalent).
adc_trusted and isocal_due are MSE-only signals; on WZA/BCE
they decode to None. raw_state and raw_status are the
untouched wire bytes (as integers for xBPI, strings for SBI where
applicable) so callers can cross-check against docs/protocol.md
§8.2 without re-decoding.
CalRecord
dataclass
¶
Last-calibration snapshot from 0xB9.
Layout per docs/protocol.md §7.12. The 17-byte RAM buffer is
cleared on cold boot, so :attr:temperature_celsius can be present
(the kernel maintains it separately) while :attr:signature and
:attr:counters are all-zero. Callers that just want "was there a
cal?" should check :attr:has_metadata.
has_metadata
property
¶
True if any metadata byte is non-zero.
All-zero :attr:signature + :attr:counters means the balance
has never recorded a cal in the current RAM buffer (post cold
boot). :attr:temperature_celsius can still be valid in that
state — see §7.12's three-tier storage note.
DeviceInfo
dataclass
¶
DeviceInfo(
manufacturer,
model,
serial,
factory_number,
software,
firmware,
family,
protocol,
capacity,
increment,
sbn,
serial_settings,
capabilities,
probe_report=_empty_probe_report(),
temperature_sensor_indices=None,
)
Identity snapshot produced by :meth:Balance.identify.
Populated at :func:open_device time when identify=True and
cached on the :class:Balance. Most fields are None for
balances we have not yet RE'd beyond the model-string classifier.
capacity and increment are populated by the metrology
probe and otherwise default to None. capabilities is
seeded from the family discriminator at identify time and
refined as commands probe the device.
temperature_sensor_indices is populated only when a caller
has explicitly run :meth:Balance.discover_temperature_sensors,
which probes the device at runtime and records exactly which
indices replied. None (the default) means "not yet probed" —
no assumption baked in. Some firmwares expose sparse indices
(the MSE1203S we tested replies at 0, 1, 3 and the
7f ff ff ff sentinel at 2), some expose contiguous, some
expose none at all; the device is the source of truth.
ParameterEntry
dataclass
¶
One parameter-table entry from 0x55.
current and max are the two u8 TLVs returned in the reply.
Callers normally route through the typed Balance.get_X() /
Balance.set_X() accessors which decode current through the
:class:sartoriuslib.registry.parameters.ParameterSpec table.
ProbeOutcome
dataclass
¶
One capability's current availability plus provenance.
See design §5.1: Availability is the derived state, while
ProbeOutcome is the observation record that produced it.
Quantity
dataclass
¶
Scalar value with its unit. Used for capacity, increment, etc.
Reading
dataclass
¶
Reading(
value,
unit,
sign,
stable,
overload,
underload,
decimals,
sequence,
status_flags,
protocol,
received_at,
monotonic_ns,
raw,
)
One decoded weight reading.
value is None on the off-scale sentinel; the measurement body
alone cannot disambiguate overload from underload, so callers that
need that distinction should invoke :meth:Balance.status.
stable comes from the universal measurement-frame flag bit
0x40 (design §7 note) — more portable across MSE/WZA/BCE than
the family-specific status-block state byte.
status_flags carries a bag of protocol-specific signals
("stable", "off_scale", and in long-frame reads
"isocal_due" / "adc_trusted") so power users can inspect
without reaching for the raw bytes.
__format__ ¶
Delegate format specs to :attr:value so f"{r:.4f}" works.
The empty spec falls back to :func:str (the frozen
dataclass default) so f"{r}" still prints the structured
repr. Off-scale readings (value is None) format as
"None" for any non-empty numeric spec rather than raising —
a stream of mixed valid/None readings is the common case during
a tare or zero settling window and crashing in a log-line
f-string would be a surprising failure mode.
Source code in src/sartoriuslib/devices/models.py
as_dict ¶
Flatten the reading into a row-shaped dict for tabular sinks.
Content-only — timing provenance (received_at,
monotonic_ns) lives on the surrounding
:class:~sartoriuslib.streaming.sample.Sample because sample-
level send/receive boundaries are the authoritative timeline
(design §10). Booleans render as 0 / 1 so SQLite picks
INTEGER affinity and CSV / JSONL round-trip cleanly through
every stdlib reader.
Source code in src/sartoriuslib/devices/models.py
TemperatureReading
dataclass
¶
One sensor's temperature read (xBPI 0x76).
:attr:celsius is None when the sensor index is not installed
— the balance returns the 7f ff ff ff sentinel in that case
(docs/protocol.md §9). :attr:sensor is the TLV-21 index the
caller passed; :attr:raw is the 5-byte typed-float body.
Family taxonomy¶
sartoriuslib.devices.kind ¶
Balance family taxonomy. See design doc §5.
BalanceFamily ¶
Bases: StrEnum
Classification from the model string returned by xBPI 0x02 or SBI identify.
- :attr:
CUBIS— MSE and related Cubis strings; full xBPI plus Cubis extensions. - :attr:
OEM_WEIGH_CELL— WZ/WZA; ships from the factory in SBI autoprint (1200-7-O-1) and requires a front-panel menu change to switch to xBPI. (MSE and BCE also ship in SBI by default — switching to xBPI is a front-panel menu change on every family.) - :attr:
BASIC_LAB— BCE*; MSE opcode subset, no Cubis extensions. - :attr:
UNKNOWN— anything we have not classified; every call becomes a live probe.
classify_family ¶
Classify a balance family by its model-string prefix.
Rules (design §5):
MSE*and related Cubis strings → :attr:BalanceFamily.CUBISWZ*/WZA*→ :attr:BalanceFamily.OEM_WEIGH_CELLBCE*→ :attr:BalanceFamily.BASIC_LAB- anything else → :attr:
BalanceFamily.UNKNOWN
Case-insensitive and whitespace-tolerant. Returns UNKNOWN for
empty input — every call becomes a live probe for unclassified
devices (design §5.1).
Source code in src/sartoriuslib/devices/kind.py
Capability flags + safety tier¶
sartoriuslib.devices.capability ¶
Capability flags, safety tiers, and probe/availability enums.
See design doc §5 (families + capabilities) and §6.1 (gates).
Availability ¶
Bases: StrEnum
Derived state the session consults when dispatching a command.
See design doc §5.1, §6.1.1.
INAPPLICABLE
class-attribute
instance-attribute
¶
Device responded with xBPI 0x06. Retryable; state-dependent.
SUPPORTED
class-attribute
instance-attribute
¶
Directly confirmed by a successful call or probe.
UNKNOWN
class-attribute
instance-attribute
¶
Never exercised; priors may exist but no device observation yet.
UNSUPPORTED
class-attribute
instance-attribute
¶
Device responded with xBPI 0x04 / equivalent SBI refusal. Sticky per session.
Capability ¶
Bases: Flag
Feature capabilities derived from family defaults + live probing.
Flag bitmap carries capabilities currently believed SUPPORTED. Full
tri/quad-state per capability lives in DeviceInfo.probe_report.
ProbeSource ¶
Bases: StrEnum
Where an :class:Availability value came from.
FAMILY_TABLE
class-attribute
instance-attribute
¶
Seeded prior from our captures.
LIVE_CALL
class-attribute
instance-attribute
¶
Updated by the device's response to a normal command.
TARGETED_PROBE
class-attribute
instance-attribute
¶
Explicit probe during identify() / discovery.
USER_OVERRIDE
class-attribute
instance-attribute
¶
Set explicitly by the caller.
SafetyTier ¶
Bases: IntEnum
Per-command safety tier. See design doc §6.1.
DANGEROUS
class-attribute
instance-attribute
¶
Baud/SBN change, reset, calibration init, protocol switch. Requires confirm=True.
PERSISTENT
class-attribute
instance-attribute
¶
Parameter writes, save menu, communication settings. Requires confirm=True.
READ_ONLY
class-attribute
instance-attribute
¶
Weight, status, identity, capacity, increment, temperature, parameter reads.
STATEFUL
class-attribute
instance-attribute
¶
Transient state change (tare, zero). No EEPROM write.
Balance facade¶
sartoriuslib.devices.balance ¶
The :class:Balance facade.
One protocol-neutral class — design §5 ("no family subclasses"). The
balance dispatches every call through :meth:Session.execute so all
gates run before any byte leaves the host, and runtime behaviour stays
identical whether the device is on xBPI or SBI.
Surfaces:
Weight & state:
:meth:poll, :meth:read_net, :meth:read_gross,
:meth:read_tare_value, :meth:tare, :meth:zero,
:meth:status, :meth:identify, :meth:raw_xbpi.
Metrology, parameters, cal, persistence:
:meth:capacity, :meth:increment, :meth:temperature,
:meth:read_parameter, :meth:write_parameter,
:meth:save_menu, :meth:reload_menu,
:meth:last_cal_record, :meth:internal_adjust.
Typed parameter pairs
:meth:get_filter_mode / :meth:set_filter_mode,
:meth:get_display_unit / :meth:set_display_unit,
:meth:get_auto_zero / :meth:set_auto_zero,
:meth:get_isocal_mode / :meth:set_isocal_mode,
:meth:get_tare_behavior / :meth:set_tare_behavior,
:meth:get_menu_access / :meth:set_menu_access.
Balance ¶
Protocol-neutral balance facade.
Construct via :func:sartoriuslib.open_device. Every method is a
thin wrapper around :meth:Session.execute (or
:meth:Session.cached_execute for repeat-read metrology /
parameter accessors); the session owns the I/O lock and runs the
pre-I/O safety / protocol / availability / prior gates
(design §6.1).
Source code in src/sartoriuslib/devices/balance.py
info
property
¶
Identity snapshot from the last :meth:identify call (or None).
Populated automatically by :func:open_device when
identify=True.
aclose
async
¶
capacity
async
¶
Read the weighing capacity for area (xBPI 0x0C).
Cached by the session when :attr:Capability.CONFIG_COUNTER
is present — the balance's 0xBA counter bumps on display-
accuracy changes (p08) which is the only thing that would
move this value in practice.
The wire's typed-float reply does not carry a unit byte
(contrast the 8-byte measurement body's byte [6]). To return
a complete :class:Quantity we read the current display unit
(p07) and fold it in here. get_display_unit() is itself
cached on 0xBA via the parameter-table cache, so two
successive capacity() calls only re-read 0xBA (twice,
once per call) and not 0x0C or 0x55. If
get_display_unit() itself fails (e.g. the parameter table
is unreachable), the unit falls back to :attr:Unit.UNKNOWN
and the numeric value still returns — fail-open is more useful
than a hard error for a metadata read.
Source code in src/sartoriuslib/devices/balance.py
configure_protocol
async
¶
configure_protocol(
target,
*,
baudrate=None,
parity=None,
stopbits=None,
timeout=None,
confirm=False,
)
Switch this balance's active wire protocol (and optionally framing).
DANGEROUS — requires confirm=True. The flip is purely
host-side: per docs/protocol.md §2.1 the device's protocol
mode changes via the front-panel menu, never via xBPI for the
PC-USB port (verified empirically 2026-04-25 on MSE1203S — the
PC-USB protocol selector is not in the xBPI parameter table at
all; only Device → PC-USB → Dat.Rec. on the front panel
flips it). On the 9-pin peripheral port, p35 write +
SAVE_MENU + cold boot does work programmatically, but most
users are on PC-USB.
This method reconciles the host with the user's front-panel
change by closing the current protocol client, reopening the
transport at the new serial framing (any None argument keeps
the existing value), and building the new client. It then
verifies via an identity probe and refreshes :attr:info from
the new protocol.
If the user has NOT actually flipped the front panel before
calling this method, the post-switch identity probe will fail
— typically with a :class:SartoriusFrameError ("bad marker
byte 0x20") when xBPI is requested but the wire is still
emitting SBI autoprint, or a timeout when SBI is requested
but the wire is still on xBPI. Treat that error as a strong
signal that the front-panel mode does not match target.
On any failure during the switch the method attempts to roll
back to the original serial framing. If that rollback also
fails the underlying :class:Session transitions to
:attr:SessionState.BROKEN and a
:class:SartoriusConnectionError is raised — the caller must
close this balance and re-open via
:func:sartoriuslib.open_device to recover.
Same-protocol no-op: when target equals the current
protocol and no framing override is supplied, the call returns
the cached :class:DeviceInfo (or runs :meth:identify if
none has been cached yet) without touching the transport.
Source code in src/sartoriuslib/devices/balance.py
873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 | |
discover_temperature_sensors
async
¶
Probe the device for installed temperature sensors at runtime.
Iterates indices 0..max_index calling :meth:temperature
on each. Records every index that replies — both real
readings (celsius is a float) and sentinel slots
(celsius is None because the firmware returned
7f ff ff ff). Stops early on
:class:SartoriusIndexOutOfRangeError, which the device
emits past the last valid index. Updates the cached
:class:DeviceInfo's
:attr:temperature_sensor_indices with the discovered tuple
and returns it.
Device-agnostic by design — no family table is consulted, so a balance we have never tested still produces an honest sensor map.
max_index is a safety cap, not a contract: 8 is the
default headroom (Cubis MSE captures stop at 3). Probing a
device with no sensors produces an empty tuple. Each probe
is one round-trip; the result is not cached on 0xBA
because per-call temperature reads are not cached either —
callers re-discover on demand.
Source code in src/sartoriuslib/devices/balance.py
get_auto_zero
async
¶
Read p06 (auto-zero tracking) as an :class:AutoZeroMode.
Source code in src/sartoriuslib/devices/balance.py
get_display_unit
async
¶
Read p07 (display unit) as a :class:Unit.
Source code in src/sartoriuslib/devices/balance.py
get_filter_mode
async
¶
Read p01 (filter mode) as a :class:FilterMode.
Source code in src/sartoriuslib/devices/balance.py
get_isocal_mode
async
¶
Read p15 (isoCAL mode) as an :class:IsoCalMode (Cubis only).
Source code in src/sartoriuslib/devices/balance.py
get_menu_access
async
¶
Read p40 (front-panel menu lock) as :class:MenuAccessMode.
Source code in src/sartoriuslib/devices/balance.py
get_tare_behavior
async
¶
Read p05 (tare-on-stability behaviour) as :class:TareBehavior.
Source code in src/sartoriuslib/devices/balance.py
identify
async
¶
Read every identity opcode and compose a :class:DeviceInfo.
Runs in sequence — the session's I/O lock keeps them serialised
on the wire. The session carries the serial framing it was
opened with (the device doesn't expose its own baud/parity), so
:class:DeviceInfo reports those settings directly; sessions
constructed without framing fall back to a placeholder.
After the textual identity primitives, the factory probes
capacity and increment via :meth:capacity / :meth:increment
and writes them onto the returned :class:DeviceInfo for
WZG-family balances where the metrology commands are known to
respond. Failures are swallowed — a balance that refuses
0x0C keeps the old behaviour of capacity=None.
Source code in src/sartoriuslib/devices/balance.py
352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 | |
increment
async
¶
Read the display increment for area (xBPI 0x0D).
See :meth:capacity for the caching contract and the
composite-unit fold.
Source code in src/sartoriuslib/devices/balance.py
internal_adjust
async
¶
Start an internal adjustment (xBPI 0x28). DANGEROUS.
cal_type defaults to the canonical internal-adjust selector
(0x78) — see
:data:sartoriuslib.commands.calibration.INTERNAL_ADJUST_CAL_TYPE.
Callers can pass another value in the 0x70..0x7B range to
drive external cal / linearization variants per
docs/protocol.md §7.7.
Source code in src/sartoriuslib/devices/balance.py
last_cal_record
async
¶
Read the last-calibration snapshot (xBPI 0xB9, §7.12).
poll
async
¶
Read the live net weight at standard resolution.
Short-cut for :meth:read_net with no arguments. One-shot
request/response; to stream at a cadence use
:func:sartoriuslib.streaming.record.
Source code in src/sartoriuslib/devices/balance.py
raw_sbi
async
¶
Send an arbitrary SBI command and return parsed line replies.
Source code in src/sartoriuslib/devices/balance.py
raw_xbpi
async
¶
Send an arbitrary xBPI opcode and return the raw reply frame.
Opcodes in the built-in read-only safe-list
(:data:sartoriuslib.commands.raw.SAFE_READ_ONLY_OPCODES) run
freely; anything else requires confirm=True. Intended for
RE and one-off probes — typed commands are the preferred path
for everything the library already models.
Source code in src/sartoriuslib/devices/balance.py
read_gross
async
¶
Read the gross weight.
Source code in src/sartoriuslib/devices/balance.py
read_net
async
¶
Read the net weight.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
hires
|
int
|
|
0
|
Source code in src/sartoriuslib/devices/balance.py
read_parameter
async
¶
Read one parameter-table entry (xBPI 0x55).
Returns the (current, max) u8 pair untouched; typed
accessors layer on top to decode via the
:class:sartoriuslib.registry.parameters.ParameterSpec table.
Cached on 0xBA.
Source code in src/sartoriuslib/devices/balance.py
read_tare_value
async
¶
Read the stored tare value (the reference, not a live operation).
refresh_sbi_autoprint_state
async
¶
Re-sniff whether an SBI session is currently in autoprint mode.
Use this after changing autoprint from the balance front panel during an open session. A quiet sniff clears autoprint mode so command/reply SBI APIs become available again; observed output keeps the session in consume-only autoprint mode.
Source code in src/sartoriuslib/devices/balance.py
reload_menu
async
¶
Reload the saved menu from EEPROM (xBPI 0x46).
save_menu
async
¶
Persist the current runtime menu to EEPROM (xBPI 0x47).
Source code in src/sartoriuslib/devices/balance.py
set_auto_zero
async
¶
Write p06. Accepts :class:AutoZeroMode, a string, or wire int.
Source code in src/sartoriuslib/devices/balance.py
set_baud_rate
async
¶
Send xBPI 0x5C and reopen the transport at the new baud.
DANGEROUS — requires confirm=True. wire_code is the
device-side encoding from docs/protocol.md §7.10
(0x00=9600, 0x01=19200, 0x02=38400, 0x03=57600);
baudrate is the matching host-side baud (the transport's
framing is reopened at this value). The library does not map
between the two automatically because the encoding is
documented as "different from p31 / p63" and we have no RE
captures verifying the mapping yet — the caller passes both so
the on-wire byte and the host framing stay explicit.
After the device-side ACK the transport is reopened, identity
is reprobed for verification, and the cached :class:DeviceInfo
is refreshed. Verification failure rolls the transport back to
the original baud; if rollback fails the session enters
:attr:SessionState.BROKEN.
SBI sessions are not supported here — the 0x5C opcode is
xBPI-only. Call :meth:configure_protocol first to switch to
xBPI if needed.
Source code in src/sartoriuslib/devices/balance.py
1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 | |
set_display_unit
async
¶
Write p07. Accepts :class:Unit, a fuzzy string, or wire code.
Source code in src/sartoriuslib/devices/balance.py
set_filter_mode
async
¶
Write p01. Accepts :class:FilterMode, a fuzzy string, or wire int.
Fuzzy strings ("stable" / "very stable" / "vs")
route through :func:resolve_filter_mode.
Source code in src/sartoriuslib/devices/balance.py
set_isocal_mode
async
¶
Write p15.
Source code in src/sartoriuslib/devices/balance.py
set_menu_access
async
¶
Write p40.
Source code in src/sartoriuslib/devices/balance.py
set_tare_behavior
async
¶
Write p05.
Source code in src/sartoriuslib/devices/balance.py
status
async
¶
stream ¶
Create a per-balance streaming session.
mode="poll" is the default and requires rate_hz.
mode="autoprint" consumes existing SBI autoprint output without
changing device settings.
Source code in src/sartoriuslib/devices/balance.py
tare
async
¶
temperature
async
¶
Read one temperature sensor (xBPI 0x76).
Returns a :class:TemperatureReading with celsius=None
when sensor is not installed (balance returns the
7f ff ff ff sentinel per docs/protocol.md §9).
Not cached — temperature changes continuously and callers expect a fresh read every call.
Sensor indexing is device-specific — some firmwares are
contiguous, some are sparse with reserved slots (the MSE1203S
we tested has sensors at 0/1/3 and a sentinel at 2).
Use :meth:discover_temperature_sensors to enumerate.
READ_TEMPERATURE is :attr:Command.parameterized, so an
out-of-range index raises
:class:SartoriusIndexOutOfRangeError without poisoning the
availability cache for in-range indices.
Source code in src/sartoriuslib/devices/balance.py
write_parameter
async
¶
Write one parameter-table entry (xBPI 0x56). PERSISTENT.
Requires confirm=True. Invalidates the cached entry for
index afterwards — conservative so the §6.3 caveat rows
(p13 / p50, whose writes don't bump 0xBA) stay
consistent.
Source code in src/sartoriuslib/devices/balance.py
write_sbn_address
async
¶
Send xBPI 0x72 to change the balance's SBN address.
DANGEROUS — requires confirm=True. Returns the value
read back via 0x71 after the write so the caller can
verify. The session's dst_sbn is left unchanged by default
because the balance accepts dst_sbn=0x09 regardless of its
configured SBN on a direct point-to-point link
(docs/protocol.md §2.2). Pass update_session_dst=True
on multidrop links where the new SBN must address the device
going forward.
Source code in src/sartoriuslib/devices/balance.py
Session¶
sartoriuslib.devices.session ¶
Session: I/O lock, safety gates, availability cache, prior checks.
One :class:Session wraps one balance on one serial port. It is the
single dispatch point between the :class:Balance facade and the
protocol clients. Every :meth:Session.execute call walks the
gates in the design-doc §6.1 order, then runs the command via the
active protocol's client, then updates the per-command availability
cache per §6.1.1.
What lives here
- Active-protocol selection (xBPI vs SBI).
- Safety-tier gate (PERSISTENT / DANGEROUS need confirm=True).
- Protocol gate (command's variant for the active protocol must be set).
- Availability gate (commands known UNSUPPORTED short-circuit pre-I/O).
- Prior gate (family / capability hints; soft by default, hard under strict).
- Per-call availability update (success → SUPPORTED, 0x04 → UNSUPPORTED,
0x06 → INAPPLICABLE, else unchanged).
What does not live here - Transport I/O: delegated to the protocol client. - Device identification: the factory pre-computes family / capabilities / firmware and hands them in at construction.
Result cache
:meth:cached_execute fronts results keyed on command + caller-
supplied cache_key and xBPI's 0xBA config counter
(docs/protocol.md §7.11 and design §6.3). Before returning a
cached value, the session re-reads 0xBA; a mismatch flushes the
entry. Writes invalidate the affected key explicitly — the §6.3
caveat says p13 / p50 writes don't bump the counter, so the
cache stays correct only if the :class:Balance facade clears
those entries on its own.
Sessions without :attr:`Capability.CONFIG_COUNTER` (WZA, SBI) fall
back to un-cached dispatch transparently — ``cached_execute``
behaves identically to :meth:`execute` in that mode.
Session ¶
Session(
*,
xbpi_client=None,
sbi_client=None,
active_protocol,
family=BalanceFamily.UNKNOWN,
capabilities=NO_CAPABILITY,
firmware=None,
src_sbn=1,
dst_sbn=9,
strict=False,
default_timeout=1.0,
serial_settings=None,
)
One balance, one serial port. Enforces gates, serialises I/O.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
xbpi_client
|
XbpiProtocolClient | None
|
Client for xBPI dispatch, or |
None
|
active_protocol
|
ProtocolKind
|
Which protocol this session currently speaks. |
required |
family
|
BalanceFamily
|
Balance family discriminator (from |
UNKNOWN
|
capabilities
|
Capability
|
Bitmap of capabilities believed present. |
NO_CAPABILITY
|
firmware
|
FirmwareVersion | None
|
Firmware version, if known. |
None
|
src_sbn
|
int
|
Host SBN address for xBPI frames. |
1
|
dst_sbn
|
int
|
Balance SBN address for xBPI frames. |
9
|
strict
|
bool
|
If |
False
|
default_timeout
|
float
|
Per-call timeout when the caller passes
|
1.0
|
Source code in src/sartoriuslib/devices/session.py
sbi_autoprint_active
property
¶
Whether the SBI session has observed unsolicited autoprint output.
serial_settings
property
¶
Serial settings the transport was opened with, if known.
Set at construction time by :func:sartoriuslib.open_device and
updated after a successful :meth:Balance.configure_protocol.
None for sessions built from a pre-existing :class:Transport
whose framing the library did not control.
transport
property
¶
The underlying :class:Transport, regardless of active protocol.
Both protocol clients hold the same transport — return whichever
is wired. The constructor enforces that the client matching
active_protocol is non-None, so this always returns a real
:class:Transport.
aclose
async
¶
Close the underlying transport, if one is wired.
Idempotent — safe to call multiple times. The factory owns the transport's construction and hands it into the session via the protocol client; closing the session closes the transport.
Both clients hold the same transport, so close it once via the
session's :attr:transport accessor rather than once per client
slot — guards against a future caller that wires both clients
simultaneously.
Source code in src/sartoriuslib/devices/session.py
availability_of ¶
Current availability for command_name (UNKNOWN if unseen).
cache_snapshot ¶
Copy of cache_key → counter_snapshot for test assertions.
The actual cached values are intentionally not surfaced — tests assert on the presence and counter pinning of an entry, not its decoded content.
Source code in src/sartoriuslib/devices/session.py
cached_execute
async
¶
Dispatch command with a 0xBA-keyed result cache.
Sessions without :attr:Capability.CONFIG_COUNTER fall through
to :meth:execute. When the capability is present, the session
re-reads the config counter before returning a cached value;
any change flushes the entry and the command re-runs.
cache_key is caller-supplied so a command that takes
arguments (capacity(area=N), read_parameter(idx)) can
cache separate entries per distinct call.
Source code in src/sartoriuslib/devices/session.py
check_state ¶
Raise :class:SartoriusConnectionError if the session is BROKEN.
Public alias for the internal gate run by every dispatch path,
so lifecycle helpers on :class:Balance can reuse the same
guard without poking at private attributes.
Source code in src/sartoriuslib/devices/session.py
execute
async
¶
Dispatch command with full pre-I/O gating.
Gates fire in the design doc §6.1 order; each raise happens before any byte is sent, so a gate failure is observably equivalent to the call never leaving the host.
Source code in src/sartoriuslib/devices/session.py
execute_raw_sbi
async
¶
Send an arbitrary SBI command token and return the parsed reply.
Source code in src/sartoriuslib/devices/session.py
execute_raw_xbpi
async
¶
Send an arbitrary xBPI opcode and return the raw reply frame.
Bypasses the declarative :class:Command layer — the opcode is
a per-call parameter, so none of the prior / capability gating
applies. The one hard gate is a safe-list check: opcodes in
:data:sartoriuslib.commands.raw.SAFE_READ_ONLY_OPCODES run
freely; anything else requires confirm=True because the
library cannot know it is safe.
The availability cache is not updated — raw calls are opaque at the command-name level.
Source code in src/sartoriuslib/devices/session.py
invalidate_cache ¶
Drop one cached entry, or clear all when cache_key is None.
The :class:Balance facade calls this after writes whose
0xBA bump isn't guaranteed (the §6.3 caveat). Unknown
keys are a silent no-op — idempotent.
Source code in src/sartoriuslib/devices/session.py
mark_broken ¶
Transition the session to :attr:SessionState.BROKEN.
Called only from lifecycle operations
(Balance.configure_protocol) when a rollback fails. Once
broken, every subsequent dispatch refuses with
:class:SartoriusConnectionError.
Source code in src/sartoriuslib/devices/session.py
read_sbi_autoprint_reading
async
¶
Read the next valid SBI autoprint weight line without writing.
Source code in src/sartoriuslib/devices/session.py
read_sbi_line
async
¶
Read one unsolicited SBI line, used by autoprint streaming.
Source code in src/sartoriuslib/devices/session.py
refresh_sbi_autoprint_state
async
¶
Passively re-sniff whether SBI autoprint is currently active.
Source code in src/sartoriuslib/devices/session.py
replace_clients ¶
Swap protocol clients atomically — used by configure_protocol.
The host-side flip closes the old protocol client, reopens the
transport at new serial framing, builds a new client, and
verifies. On verification success this method installs the new
clients and the new active protocol; the availability cache,
prior warnings, and result cache are all
cleared because the command surface changes when the protocol
does (xBPI-only commands have no SBI variant and vice versa,
and any 0xBA-pinned cache entries belong to the old session).
Refuses to install ProtocolKind.AUTO — detection must
resolve to XBPI or SBI first. Refuses if the
corresponding client for active_protocol is missing.
Source code in src/sartoriuslib/devices/session.py
set_dst_sbn ¶
Update the destination SBN.
Used after :meth:Balance.write_sbn_address on multidrop links
where the new address must address the device going forward.
Source code in src/sartoriuslib/devices/session.py
update_identity ¶
Replace session-level identity state after a live identify call.
Called by :func:sartoriuslib.devices.factory.open_device after
running the identify commands, so subsequent prior gating sees
the discovered family and capabilities instead of the
placeholder UNKNOWN / empty values.
Each argument left as None keeps the existing value.
Source code in src/sartoriuslib/devices/session.py
SessionState ¶
Bases: Enum
Lifecycle state of a :class:Session.
OPERATIONAL is the normal state — commands dispatch freely.
BROKEN is entered when an atomic lifecycle operation
(Balance.configure_protocol) cannot reconcile the transport
with the device's new state. A BROKEN session refuses every
subsequent :meth:execute with :class:SartoriusConnectionError;
the caller must construct a fresh session (typically via
:func:sartoriuslib.open_device) to recover.
Factory — open_device / open_balance¶
sartoriuslib.devices.factory ¶
open_device — primary async entry point for the library.
Supports forced xBPI, forced SBI, and ProtocolKind.AUTO opens.
open_device is the name documented for cross-library uniformity
with alicatlib; :func:open_balance is a friendly alias.
The factory owns the transport's construction and wires it through
the xBPI protocol client and the :class:Session into the returned
:class:Balance. The balance's async-context-manager exit closes the
transport. If any step between open and balance-construction fails,
the factory closes the transport before propagating the exception.
open_balance
async
¶
open_balance(
port,
*,
protocol=ProtocolKind.XBPI,
serial_settings=None,
timeout=1.0,
src_sbn=1,
dst_sbn=9,
strict=False,
identify=True,
)
Friendly alias for :func:open_device.
Returns the same :class:Balance as :func:open_device with
identical arguments. open_device is documented as primary for
cross-library uniformity with alicatlib; open_balance reads
more naturally inside the sartoriuslib namespace.
Source code in src/sartoriuslib/devices/factory.py
open_device
async
¶
open_device(
port,
*,
protocol=ProtocolKind.XBPI,
serial_settings=None,
timeout=1.0,
src_sbn=1,
dst_sbn=9,
strict=False,
identify=True,
)
Open a serial port, wire up the protocol stack, and return a :class:Balance.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
port
|
str | Transport
|
Serial-port path (e.g. |
required |
protocol
|
ProtocolKind
|
Which wire protocol to speak. :attr: |
XBPI
|
serial_settings
|
SerialSettings | None
|
Override the default 8-O-1 @ 9600 baud
configuration. Ignored when |
None
|
timeout
|
float
|
Per-call default timeout for both transport I/O and
:class: |
1.0
|
src_sbn
|
int
|
Host xBPI bus address (default |
1
|
dst_sbn
|
int
|
Balance xBPI bus address (default |
9
|
strict
|
bool
|
If |
False
|
identify
|
bool
|
Run the identify commands on open and cache
:class: |
True
|
Raises:
| Type | Description |
|---|---|
SartoriusError
|
|
SartoriusConnectionError
|
Transport failed to open. |
Returns:
| Name | Type | Description |
|---|---|---|
A |
Balance
|
class: |
Balance
|
closes the transport. |
Source code in src/sartoriuslib/devices/factory.py
39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 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 | |
Discovery¶
sartoriuslib.devices.discovery ¶
Port discovery + :class:DiscoveryResult.
Wide baud/parity sweeps live here — not in open_device. See
design doc §4.3 and §16 Q2.
A single helper, :func:discover_port, opens the caller's transport at
the given serial settings, runs the conservative
:func:sartoriuslib.protocol.detect_protocol probe, and returns a
:class:DiscoveryResult. Wider serial-settings probing is deferred —
the design doc explicitly leans toward "user supplies serial params"
(§16 Q2) and adding sweeps now would commit to a sweep order before
field evidence shapes it.
DiscoveryResult
dataclass
¶
DiscoveryResult(
port,
baudrate,
parity,
stopbits,
protocol,
autoprint_active=False,
pending_lines=tuple(),
error=None,
)
Outcome of probing one port at one serial-settings configuration.
Attributes:
| Name | Type | Description |
|---|---|---|
port |
str
|
The port label (path or pre-built transport's label). |
baudrate |
/ parity / stopbits
|
Effective framing during the probe. |
protocol |
ProtocolKind | None
|
Resolved :class: |
autoprint_active |
bool
|
Whether the probe observed unsolicited SBI autoprint output during the passive sniff window. |
pending_lines |
tuple[bytes, ...]
|
Sniffed autoprint lines (with CRLF) the caller may want to re-queue on a follow-up open. |
error |
str | None
|
Human-readable error description when |
discover_port
async
¶
discover_port(
port,
*,
serial_settings=None,
timeout=1.0,
sniff_window=0.25,
src_sbn=1,
dst_sbn=9,
)
Open port at serial_settings and run the conservative detect.
Returns a :class:DiscoveryResult capturing the chosen framing and
the detector's verdict. The transport is closed before returning.
Failures during detect_protocol (no responsive device, hard
transport faults) surface in the result's error field rather
than being raised — discovery is meant to be safe to call against
unknown ports without crashing the caller.