Sartorius xBPI Protocol — Reverse-Engineered Reference¶
A reference to the Sartorius xBPI binary protocol, reverse engineered across three balances spanning Sartorius's product line:
| Model | Class | Capacity / resolution | xBPI default |
|---|---|---|---|
| MSE1203S-100-DR | Cubis analytical | 1200 g / 1 mg | 19200-8-O-1 |
| WZA8202-N | OEM weigh cell | 8200 g / 10 mg | 1200-8-O-1 |
| BCE3202-1S | Basic laboratory | 3200 g / 10 mg | 9600-8-O-1 |
All three families ship from the factory in SBI mode; switching to xBPI requires a front-panel menu change on each unit. The "xBPI default" column above is the framing the unit uses once it has been switched to xBPI — not the protocol it runs out of the box.
Cross-referenced with the publicly available WZG OEM weigh-cell manual, which documents a partial command list for a related OEM cell family. Many commands overlap across all three balances, but the Cubis has expanded the protocol — most notably by requiring TLV-wrapped arguments where WZG shows plain byte arguments.
Where protocol features are uniform across all three balances, they are presented as the xBPI baseline. Where they differ, the text calls out which balance family behaves how. §14 consolidates the cross-family differences in one place.
To the best of our knowledge, this is the first open-source reverse engineering of xBPI. Every "Sartorius driver" on GitHub/PyPI implements the ASCII SBI protocol, not the binary xBPI documented here.
Confidence legend¶
- [SURE] — verified by live captures AND/OR matching the WZG manual.
- [LIKELY] — strongly supported by evidence but not bulletproof.
- [GUESS] — plausible interpretation, not yet validated.
- [UNKNOWN] — observed but meaning not understood.
1. What is xBPI?¶
Sartorius balances speak three documented protocols:
| Protocol | Encoding | Public drivers? |
|---|---|---|
| SBI (Sartorius Balance Interface) | ASCII (ESC P, ESC T…) |
Many |
| BPI (Binary Processor Interface) | Binary, legacy | None |
| xBPI (eXtended BPI) | Binary, with SBN addressing | None |
xBPI is the modern binary protocol used by Cubis MSE, Secura, Quintix (when set to binary mode), and OEM weigh cells. It is not publicly documented by Sartorius. Sartorius sells a paid OPC server product whose entire purpose is to bridge xBPI; that product exists because the protocol is closed.
2. Physical and link layer¶
2.1 Serial parameters¶
| Parameter | Value |
|---|---|
| Default baud | family-specific: MSE = 19200, BCE = 9600, WZA = 1200 |
| Supported baud | 600, 1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200 (via p31) |
| Data bits | 8 |
| Parity | Odd |
| Stop bits | 1 |
| Flow control | None (hardware CTS documented but not required) |
8-O-1 framing is universal across all three balances. The WZG manual claims "7-bit ASCII" for xBPI, which is wrong — captured frames contain bytes like 0x88, 0xBB that cannot fit in 7 bits. Use 8-O-1.
The PC-USB port is parity-forgiving on receive — the balance accepts requests at any parity regardless of its menu-configured parity, so the "PC-USB parity" menu setting only affects TX from the balance.
All three families ship from the factory in SBI mode and require a
front-panel menu change to switch to xBPI. WZA's SBI default is
autoprint at 1200-7-O-1 (+ 0.00 g \r\n); MSE and BCE default to
SBI command/reply at their respective baud rates with no autoprint
enabled.
MSE has two physical ports with separate protocol configuration:
the 9-pin SUB-D peripheral port and the USB-B "PC-USB" port. The
peripheral port's protocol is settable via xBPI parameter p35
(see §10) — write + 0x47 SAVE_MENU + cold boot to apply. The
PC-USB port's protocol is NOT exposed via the xBPI parameter table
(indices 0-79 swept empirically 2026-04-25; only the front-panel
Device → PC-USB → Dat.Rec. menu can flip it). Consequence: the
package's Balance.configure_protocol(...) is host-side only on
PC-USB — it cannot rewrite the wire's protocol; the user must
flip the front-panel menu first. On the peripheral port, by
contrast, the package can both read and re-program p35.
2.2 SBN addressing¶
xBPI supports RS-485 multidrop with a Sartorius Bus Node (SBN) address. On a point-to-point RS-232 link, both host and balance still include their SBN in every host-to-balance frame:
- Host SBN =
0x01(by convention) - Balance SBN = set via opcode
0x72(write) / read via0x71 - Our MSE1203S reports
0x00[SURE] - The balance accepts
dst_sbn = 0x09(its factory default) and responds regardless of its own SBN when directly connected. [LIKELY]
3. Frame format¶
3.1 Host → balance (TX)¶
[len] [src_sbn=01] [dst_sbn=09] [opcode] [args...] [chk]
\____/ \___/
= bytes-after-len-byte (incl. chk) = sum(all-prev-bytes) & 0xFF
len= count of bytes that follow the length byte (i.e.len + 1= total frame size). [SURE]chk=sum(bytes_0..N-1) & 0xFFwhere N is the last index. [SURE]- Default SBNs:
src=0x01,dst=0x09. [SURE]
3.2 Balance → host (RX)¶
markeris always0x41. [SURE]subtypeclassifies the payload shape (see §4). [SURE]chk= same formula as TX. [SURE]
3.3 Worked examples¶
Read SBN: 04 01 09 71 7f len=4, op=0x71, chk=0x7F
Reply (subtype 0x21): 04 41 21 00 66 len=4, subtype=0x21, body=00
Read net weight: 04 01 09 1e 2c
Reply (measurement): 0b 41 48 bb a3 d7 0a 3d 30 82 45 55
len=11, subtype=0x48, 8-byte measurement body, chk=0x55
4. Response subtype encoding¶
The subtype byte (byte[2] of a device reply) follows a type-length
convention:
| Subtype | Family | Body length | Meaning |
|---|---|---|---|
0x00 |
ack |
0 (usually) | Generic ACK; success with no data. Exception: 0xBC returns subtype 0x00 with a variable-length body. |
0x01 |
error |
1 | Error code byte follows (see §6) |
0x12 |
bargraph/nested |
2 (or variable) | u16 BE bar-graph value; sometimes TLV-nested data |
0x14 |
structured_u32 |
4 | u32 BE; seen on BCE 0x75 (raw load ADC) |
0x21 |
short_data |
1 | u8 |
0x22 |
short_data |
2 | Typically [state][status] |
0x24 |
short_data |
4 | u32 BE or 2×u16 BE |
0x34 |
typed_float_alt |
4 | float32 BE (no aux byte) |
0x35 |
typed_float |
5 | [float32 BE][aux byte] — used by max/increment/temperature |
0x41 |
short_blob |
1 | 1-byte blob; seen on BCE 0x7C |
0x43 |
long_data |
3 | Raw blob |
0x45 |
long_data |
5 | Raw blob (e.g. factory number) |
0x48 |
measurement |
8 or 17 | Measurement frame (short OR streaming-long) |
0x4A |
long_data |
10 | Raw blob (e.g. software version) |
0x50 |
long_data |
16 | ASCII string, null-padded (e.g. "Sartorius") |
0x51 |
long_data |
17 | e.g. cal record at 0xB9 |
0x54 |
long_data |
20 | ASCII string (e.g. model number) |
5. TLV argument/value encoding¶
Where the WZG manual documents an opcode taking a plain single-byte arg, the Cubis MSE requires the arg wrapped as a Type-Length-Value (TLV) record:
The TLV tag encodes the value's size in its low nibble:
| Tag | Value size | Meaning |
|---|---|---|
0x11 |
1 byte | u8 (rarely used as request arg) |
0x12 |
2 bytes | u16 BE |
0x14 |
4 bytes | u32 BE |
0x21 |
1 byte | u8 — most common request-arg wrapper |
0x22 |
2 bytes | u16 BE (seen in response bodies) |
0x24 |
4 bytes | u32 BE (seen in response bodies) |
5.1 Worked examples¶
WZG form: 0x0C 00 "Read max, area 0" — ERRORS on Cubis
Cubis form: 0x0C 21 00 "Read max, area 0" — returns 1200.0 g ✓
WZG form: 0x1E 01 "Read net weight, 10× resolution" — ERRORS
Cubis form: 0x1E 21 01 "Read net weight, 10× resolution" — works
5.2 Multi-TLV responses¶
Response bodies may contain multiple TLVs concatenated. Example — 0x55
(read parameter table) at index 0:
TX: 06 01 09 55 21 00 86
RX: 06 41 21 02 21 04 8f
└─┘ ├──┘ ├──────┐├──┐└─┘
len mkr (byte [2] is
subtype 0x21, also
the first TLV's tag)
└─ TLV1: 0x21 0x02 ─┘└─ TLV2: 0x21 0x04 ─┘
So the parameter-table entry at idx 0 has (current=0x02, max=0x04).
5.3 parse_tlv_sequence helper¶
The Python helper sartoriustesting.protocol.parse_tlv_sequence(body) walks a
bytes object and yields (tag, value_bytes) tuples. To parse a multi-TLV
response body like above, prepend the subtype byte:
6. Error codes¶
When the balance returns subtype 0x01, the single body byte is an error
code:
| Code | Meaning |
|---|---|
0x03 |
Value out of range (TLV value unacceptable) |
0x04 |
Unknown opcode (not implemented by this balance) |
0x06 |
Operation not applicable (e.g., abort with nothing to abort) |
0x07 |
Invalid or missing args (wrong form, or missing required TLV) |
0x10 |
Index out of range (arg well-formed but index exceeds slot count) |
0x11 |
Unknown meaning; returned by 0x67 on BCE for a no-args call. Adjacent to 0x10 so possibly a second index-range error variant. |
The distinction between 0x04 (no such command) and 0x07 (bad args) is
useful for discovery — it identifies opcodes that EXIST but need the right
arg form. All codes above are reported identically across MSE, WZA, and
BCE (except 0x11, observed only on BCE).
7. Opcode reference¶
Opcode inventory for the Cubis MSE1203S. Annotations:
[WZG]— documented in the WZG OEM manual.[OBS]— observed on our unit.[SURE]/[LIKELY]/[GUESS]/[UNKNOWN]— semantic confidence.
7.1 Device information¶
| Op | Name | Arg | Confidence | Response | Notes |
|---|---|---|---|---|---|
0x00 |
read_software_version | — | [WZG+OBS][SURE] | subtype 0x4A, 10b packed | Our unit: 00 39 21 00 39 01 39 01 00 01 |
0x01 |
read_factory_number | — | [WZG+OBS][SURE] | subtype 0x45, 5b | Our unit: 00 31 80 11 65 |
0x02 |
read_weigh_cell_model | — | [WZG+OBS][SURE] | subtype 0x54, 20b ASCII | Our unit: "MSE1203S-100-DR" |
0x03 |
read_user_id | — | [WZG+OBS][UNKNOWN] | subtype 0x48, zeroed 8b body | Returns a zeroed measurement-format frame regardless of args — the Cubis user-ID storage has moved elsewhere and is not exposed by this opcode |
0x04 |
write_user_id | ? | [WZG] | — | SIDE-EFFECTING, skip |
0x05 |
read_oem_text | — | [WZG+OBS][SURE] | subtype 0x50, 16b ASCII | Our unit: "Sartorius" |
0x07 |
read_manufacturer | — | [WZG+OBS][SURE] | subtype 0x50, 16b ASCII | Our unit: "Sartorius" |
0x08 |
undoc_08 | — | [OBS][LIKELY] | subtype 0x43, body 5b 67 df (= 5,990,367) |
Factory-burned constant (firmware-build ID, ROM checksum, or hardware-variant token). Stable across cold boots, warm resets, and every tested state. Accepts any arg form; always returns the same 3 bytes. |
0x09 |
undoc_09 | ? | [OBS][UNKNOWN] | Varies; arg 0x43 → ACK |
Only arg 0x43 returns anything; benign (no observed side effect) |
0x0A |
read_configuration_data | — | [WZG+OBS][SURE] | subtype 0x48, 8b | Our unit: 01 01 00 00 00 03 02 01 |
0x0F |
read_balance_info | — | [WZG+OBS][SURE] | subtype 0x24, 4b = 2×u16 | Our unit: 02d8 01a4 (728, 420). Bit 6 of the low byte flips between 0x01E4 (xBPI sees weighing) and 0x01A4 (xBPI sees app mode) — a weighing/app flag mirrored by the xBPI-visible 0x62 register. |
7.2 Metrological constants¶
All require TLV-21 args on Cubis. Arg = "area index" 0..3; our unit reports same value for all areas (single weighing range).
| Op | Name | Confidence | Our unit's value |
|---|---|---|---|
0x0B |
read_threshold_0b | [OBS][LIKELY] | float32 = 0.1 — "coarse" stability/zero-tracking threshold (10× increment) |
0x0C |
read_max | [WZG+OBS][SURE] | 1200.0 g ✓ matches spec |
0x0D |
read_increment_d | [WZG+OBS][SURE] | 0.001 g = 1 mg ✓ matches spec; tracks front-panel display-accuracy setting (see p08) |
0x0E |
read_threshold_0e | [OBS][LIKELY] | float32 = 0.01 — "fine" threshold (10× increment) |
All return subtype 0x35 (typed_float, 5-byte body [float32][aux]).
Coupled triple: 0x0B, 0x0D, and 0x0E scale together. Changing the
display-accuracy menu option to "-1 digit" moved all three by exactly 10×
in one step (0x0D: 0.001→0.01, 0x0B: 0.1→1.0, 0x0E: 0.01→0.1). 0x0D is
the canonical live increment; 0x0B and 0x0E are fixed ratios above
it — likely "stability threshold in display units" and "minimum zero-tracking
step", pegged to 10× and 100× the increment. [LIKELY]
7.3 Tare and zeroing¶
| Op | Name | Confidence | Notes |
|---|---|---|---|
0x13 |
delete_tare | [WZG] | Sub-args: 00=main, 01=appl 1, 02=appl 2. Side-effecting. |
0x14 |
initiate_combined_tare | [WZG+OBS][SURE] | Canonical TARE command. No args. Reply: 03 41 00 44 ACK, synchronous. |
0x15 |
abort_combined_tare | [WZG+OBS][SURE] | Returns err 0x06 if no tare active. |
0x16 |
initiate_tare | [WZG] | Alternate tare variant. Side-effecting. |
0x17 |
abort_tare | [WZG+OBS][SURE] | Returns err 0x06 if no tare active, ACK if one is. Incidentally reveals which app modes auto-tare on entry: count, net_totl, calc, density do; weighing, unit, percent, total, animal_w don't. |
0x18 |
initiate_zeroing | [WZG+OBS][SURE] | Starts a zeroing operation. ACK reply. |
0x19 |
abort_zeroing | [WZG+OBS][SURE] | Test vehicle for error 0x06. |
0x1A |
initiate_appl_tare | [WZG] | Sub-arg: 01=appl 1, 02=appl 2. |
0x1B |
abort_appl_tare | [WZG] | — |
0x1D |
write_appl_tare | [WZG] | Preset tare values. Side-effecting. |
7.4 Weight and gross-weight reads¶
| Op | Name | Confidence | Notes |
|---|---|---|---|
0x1E |
read_net_weight_std | [WZG+OBS][SURE] | No-arg returns short measurement. TLV 21 00 = std, 21 01 = 10×, 21 02 = 100× resolution per WZG. |
0x1F |
read_net_weight_hires | [WZG+OBS][SURE] | Requires TLV arg. Res 01 = 0.1 mg, res 02 = 0.01 mg (sub-mg readings exposed). unit_raw byte[5] changes (0x30/0x40/0x50) to signal precision. |
0x1C |
read_appl_tare | [WZG+OBS][SURE] | Read stored application tare. TLV args: 21 01 / 21 02. On our unit returns same as 0x22 (no appl tare set). |
0x20 |
read_gross_weight_std | [WZG+OBS][SURE] | Live gross weight. Verified: net + tare = gross (199.99 + 330.53 = 530.52 ✓) with 200 g test. |
0x21 |
read_gross_weight_hires | [WZG+OBS][SURE] | Same as 0x20 but requires TLV arg. |
0x22 |
read_tare | [WZG+OBS][SURE] | Stored main tare reference. NOT live. |
0x23 |
read_tare_alias | [OBS][LIKELY] | Returns same as 0x22 — probably an alias. |
7.5 Filter / weighing-mode configuration¶
Filter and sampling-rate are independent axes:
| Op | Name | Confidence | Notes |
|---|---|---|---|
0x26 |
read_weighing_mode | [WZG+OBS][SURE] | Returns 1/2/3/4 = very stable / stable / unstable / very unstable. Equivalent to param-table p01 (filter/ambient mode). |
0x2C |
write_weighing_mode | [WZG+OBS][SURE] | Sets filter preset. ACK reply. Side-effecting. |
0x57 |
read_cycle_time | [WZG+OBS][SURE] | subtype 0x24 = u32 BE milliseconds. Tracks p01 (filter mode) with a 50/100/200/400 ms doubling curve across p01 values 1-4. See §14.4. |
7.6 Status queries¶
| Op | Name | Confidence | Notes |
|---|---|---|---|
0x30 |
read_balance_status_block | [WZG+OBS][SURE] | Returns subtype 0x48 with 8-byte status block (format in §8.2). |
0x32 |
read_balance_status | [WZG+OBS][SURE] | Returns subtype 0x22: [state][status] 2 bytes (see §8.2 for bit meanings). |
0x35 |
read_time_stamp | [WZG+OBS][SURE] | 1-byte tick counter. On MSE, increments at the measurement rate. On WZA ticks at ~50 Hz independently of cycle_time. |
0x36 |
read_on_off_status | [WZG+OBS][SURE] | 1 byte: 01 = on. |
0x2E |
read_slot_by_index | [OBS][LIKELY] | Takes TLV-21 index. Valid range 0–2; index 3+ → error 0x10. Returns multi-TLV body [21 <status-byte>][48 <8-byte measurement>]. On our unit all slots return value 0.0; slots 0, 1 have status=0xff (uninitialized), slot 2 has status=0x02 (configured?). Likely three check-weighing limits (upper/target/lower) or three stored reference masses. |
0x2F |
read_gross_bargraph | [WZG+OBS][SURE] | subtype 0x12 u16 BE. Scale 0..1000, 1000 = max capacity. Verified: 442 = 44.2% of 1200 g @ 530 g load. |
7.7 Calibration / adjustment¶
| Op | Name | Confidence | Notes |
|---|---|---|---|
0x28 |
start_adjustment | [WZG] | TLV arg selects type (112..123 — internal cal, external cal, linearization, etc.). SIDE-EFFECTING. |
0x29 |
abort_adjustment | [WZG+OBS][SURE] | Returns err 0x06 when no adjustment is running. |
0x76 |
read_temperature_sensors | [WZG+OBS][SURE] | See §9 — 4-sensor array. |
0x78 |
read_adjustment_unit | [WZG] | Returns current cal unit. |
0x79 |
write_adjustment_unit | [WZG] | 02=g, 03=kg, 04=ct. Side-effecting. |
7.8 Presettings and menus¶
| Op | Name | Confidence | Notes |
|---|---|---|---|
0x46 |
read_menu_from_eeprom | [WZG+OBS][SURE] | Reload saved menu settings from EEPROM. ACK. |
0x47 |
save_menu_to_eeprom | [WZG+OBS][SURE] | Persist current menu to EEPROM. ACK. |
0x4A |
read_user_memory_isi | [WZG] | ISI scratch space. Not probed. |
0x4B |
write_user_memory_isi | [WZG] | Not probed. |
0x54 |
read_stop_flags | [WZG+OBS][SURE] | subtype 0x24 u32. Our unit: 0f 00 00 00. |
0x55 |
read_parameter_table | [WZG+OBS][SURE] | TLV 21 <idx> → returns 2 u8 TLVs (current, max). See §10. |
0x56 |
write_parameter_table | [WZG] | TLV 21 <idx> 21 <val>. SIDE-EFFECTING. |
7.9 Basic / system¶
| Op | Name | Confidence | Notes |
|---|---|---|---|
0x40 |
reconfiguration | [WZG] | SIDE-EFFECTING. |
0x41 |
reset_temporary_errors | [WZG] | Side-effecting. |
0x58 |
initiate_reset | [WZG] | Warm/cold reset. DANGEROUS. |
7.10 Data interface¶
| Op | Name | Confidence | Notes |
|---|---|---|---|
0x5C |
set_baud_rate | [WZG] | 00=9600, 01=19200, 02=38400, 03=57600. DANGEROUS (changes serial comms mid-flight). Note: uses different encoding than param-table p31 / p63. |
0x71 |
read_sbn_address | [WZG+OBS][SURE] | Our unit: 0x00. |
0x72 |
write_sbn_address | [WZG] | DANGEROUS. |
7.11 Extended opcodes (not in WZG)¶
Opcodes in this section are not in the WZG OEM manual but are implemented in Sartorius firmware. Availability varies by family:
- Most are present on MSE (Cubis); a subset (
0xBA,0xB9,0xAA) are also present on BCE3202. - WZA lacks most of these entirely (
err_04 unknown_opcode). - Truly Cubis-exclusive:
0x62,0x59/0x5A(their side effects on0x62), and the0x80-set state-byte encoding in status blocks (§8.2).
See §14.2 for the cross-family presence matrix.
ACK-only opcodes (resolved)¶
| Op | Name | Confidence | Notes |
|---|---|---|---|
0x59 |
clear_xbpi_app_flag | [OBS][SURE] | ACK. When 0x62 was 3, clears it to 0 and bumps 0xBA by 1. When 0x62 already 0, no-op ACK. Does NOT change the actual UI/functional app mode — the balance display keeps showing its app. Confirms 0x62 is an xBPI-visible flag separate from real app state. |
0x5A |
alias_of_0x59 | [OBS][SURE] | Alias of 0x59. Identical behavior verified by isolated probe. |
0x67 |
nop | [OBS][SURE] | Pure NOP. ACK only, no side effect. |
0xFF |
nop | [OBS][SURE] | Pure NOP. ACK only, no side effect. |
Data-returning opcodes¶
| Op | Name | Confidence | Notes |
|---|---|---|---|
0x25 |
undoc_25_alias_p25 | [OBS][SURE] | u8 register — direct-read alias of parameter-table p25. Both always report identical values across all observed state transitions (warm drift, cold boot, direct probes). An alias scan across 23 captures identified this as the only runtime-varying opcode↔param alias on this unit; the known 0x26↔p01 alias could not be variation-verified because p01 didn't change in captures. Args ignored. |
0x31 |
undoc_31 | [OBS][UNKNOWN] | short_data 2-byte 0x00 81. Args ignored. |
0x33 |
undoc_33 | [OBS][UNKNOWN] | Always returns 2-byte 10 00 — same as the constant in measurement status blocks. Args ignored. |
0x48 |
read_stored_ref_maybe | [OBS][GUESS] | Returns a measurement-format frame with value 1000.0 g. Likely a stored reference/target mass. |
0x50 |
undoc_50_gated_by_62 | [OBS][LIKELY] | u8 register, TLV-21 addressed. idx 0 returns u8=3 only when 0x62 is in certain non-zero states (valid for 0x62 ∈ {3, 8}, errs for 0x62 ∈ {0, 5}). idx 1+ always errs on this unit. Behaves as a shadow of 0x62 with its own gating predicate; the value 3 carries no distinct information from 0x62. |
0x51 |
undoc_51_slotted_weights | [OBS][LIKELY] | 4-slot measurement-shaped register (TLV-21, idx 0–3; idx 4+ → err 0x03). Cold-boot default holds the arithmetic progression {0, 1000, 2000, 3000} g — a firmware-wide default, independent of actual capacity (3000 g exceeds this unit's 1200 g max). Likely a multi-point cal-setpoint or check-weighing-target table. The unit-byte (byte 6) flips between 0x42 (proper g) and 0x62 (base=0x22) when either (a) a physical load is on the pan, or (b) 0x62 is at value 3. Either condition alone suffices. Float values stay constant across the unit-byte flip. 0xBD drives the same flip synthetically. |
0x62 |
xbpi_menu_activity_state | [OBS][SURE] | Multi-valued time-decaying register. Observed values {0, 2, 3, 5, 8, 16}. Cold-boot value = 0. Non-zero values are transient — decay to 0 within ~5 s of inactivity. The specific non-zero value appears to encode event type (menu-entry / menu-exit / mode-activation) but the mapping is not yet decoded. Cleared synchronously by 0x59/0x5A. Not a reliable "which app is selected" signal: captures of the same UI state can show different values depending on how long ago the last menu event fired. |
0x7E |
undoc_7e | [OBS][UNKNOWN] | Returns subtype 0x12 (bargraph family) with u16 BE body. Reads 0 regardless of pan load — not a net bargraph and not a gross alias. Args ignored. Possibly unused on this balance, or reserved for a mode we have not triggered (check-weighing deviation, percent-mode bargraph, etc.). |
0xAA |
undoc_aa | [OBS][UNKNOWN] | Accepts TLV 21 00/01 only. Returns 10-byte body decoding as two 0x14 TLVs: u32 values 0x21120097 and 0x0095b744. Meaning unknown. |
0xB5 |
undoc_b5 | [OBS][UNKNOWN] | 2-slot u8 register (TLV-21, idx 2+ → err 0x03). Cold-boot default: both slots read 24. idx 1 drifts at runtime (observed 0 and 2 alongside p25/p56/p58 class movement) and resettles to 24 on its own after some events (e.g., overload/underload recovery) — not strictly monotonic, not persistent. Slot 0 is stable at 24 across all observed states. |
0xB7 |
undoc_b7 | [OBS][UNKNOWN] | 2-slot register (TLV-21, idx 0/1 both → typed_float_alt 1.5; idx 2+ → err 0x03). Both slots return the same value; could be a setting + mirror pair or a two-axis reading that happens to be equal. Value 1.5 may be a gain or multiplier constant. |
0xBD |
undoc_bd_trigger | [OBS][SURE] | Mode-trigger, not a pure read. 2-slot u32 register (subtype 0x44, 4-byte body): idx 0 → 180, idx 1 → 0; idx 2 → err 0x03. A call briefly puts the balance into "active measurement" mode, causing the 0x51 slot unit-byte to flip from 0x42 (g) to 0x62 (base=0x22). Two calls sandwich a single measurement cycle so the unit-byte appears to toggle back. The first call also populates 0xBB from pristine ff ff ff to 00 e8 00 (sticky across subsequent calls). Value 180 is stable across repeated calls. Kept in the wide-sweep SKIP list because of the mode-trigger side effect. |
0xBE |
undoc_be_multiblock | [OBS][UNKNOWN] | Returns long_data subtype 0x4A with a body of N repeated [4a] [10-byte sw-version-like payload] blocks — same shape as 0xBC's module list but a different block count. Args ignored. Likely a separate firmware/module enumeration. |
0xB9 |
read_last_cal_record | [OBS][SURE] | Returns 17-byte body = cal-temperature + 13-byte cal metadata (subtype 0x51). Temperature matches sensor 0's live reading at the cal moment to LSB precision. Ignores all TLV args. See §7.12 for byte-level layout. |
0xBA |
config_generation_counter | [OBS][SURE] | 1-byte counter that increments on most runtime-config changes. Useful for cache invalidation. Not every param write bumps it — boot-time flags like p13 (tare-on-power-on) and p50 (acoustic) change without bumping BA. BA tracks runtime weighing-pipeline config; UI-preference changes live in a separate persistence class. Resets to 1 on a full cold boot. Warm-reset behaviour is less clearly characterised (BA has drifted upward during such events, but the reset itself vs. incidental activity wasn't isolated). |
0xBB |
undoc_bb | [OBS][LIKELY] | 3-byte body. Args ignored. Multi-valued register tracking some session/state history. Observed states: ff ff ff (pristine post-cold-boot), 00 e8 00 (after first 0xBD call; sticky), 4f d5 00 (after some menu/overload sequences), ff 85 01 (separate historical session). At least 4 distinct states — more than a single-bit event flag. |
0xBC |
read_module_list_likely | [OBS][LIKELY] | Returns subtype 0x00 with body 22 XX XX [11-byte-block...]×N. Each 11-byte block matches the 0x00 sw-version response format (4a 00 39 21 00 39 01 39 01 00 01). Prefix 0x22 is likely a block-count tag; next 2 bytes are a freshness counter (varies read-to-read); N varies across sessions (observed 2, 3, 4). Likely a firmware module/plugin list — each block = one installed component's version. Rejects TLV args with err 0x07. |
7.12 The cal record (0xB9) byte layout¶
17-byte body, subtype 0x51:
| Bytes | Field | Meaning | Confidence |
|---|---|---|---|
[0..3] |
cal_temperature | float32 BE — ambient temperature at the moment the last cal ran; matches sensor 0's live reading to LSB | [SURE] |
[4..12] |
cal_signature | 9 bytes identifying which cal-type was run. Identical across multiple internal cals on our unit: 01 09 00 04 02 06 00 07 01 |
[LIKELY] |
[13..15] |
cal_counters | Counters that increment per cal event. On our unit [14] and [15] each incremented +1 per cal; [13] incremented +2 per cal (possibly auto-isoCAL piggybacking, or internal adjustment iterations counted separately). | [LIKELY] |
[16] |
padding | 0x00 terminator |
[LIKELY] |
If the balance has never had a cal recorded in the current RAM buffer, the
metadata bytes [4..16] are all zero. The temperature prefix [0..3] may
still hold the last-cal reading (kept in a separable field — see below).
Storage architecture (three tiers, all with empirical support):
- Cal coefficients live in EEPROM-backed RAM loaded by the boot path
into the measurement pipeline. Not exposed via
0xB9. Weighing reads remain accurate even when0xB9is empty, confirming this layer is independent of the visible buffer. - The
0xB9cal-record RAM buffer (what this opcode reads) is populated on cal events and cleared on every cold boot (plug-pull verified: the full 17 bytes read as zeros immediately after boot). - Separable fields within the buffer. Certain undocumented writes
(
0x2D,0x53,0x61,0x6B,0xB6) can zero[4..16]while leaving[0..3]intact at the last-cal temperature. This proves cal-metadata and cal-temperature occupy separate backing locations that alias into the same readable record.
8. Measurement frame decoding¶
8.1 Short measurement (8-byte body)¶
Returned by 0x1E, 0x1F, 0x20, 0x21, 0x1C, 0x22, 0x23 —
subtype 0x48 with 8-byte body:
byte field type meaning
[0-3] value float32 BE displayed value in reported units
[4] aux u8 usually last byte of the float
— extra-precision echo [LIKELY]
[5-6] unit 2 bytes see §8.4
[7] flags u8 bit 0x40 = stable; low nibble varies
by opcode/mode (NOT persistent)
Overload / underload is signalled simultaneously in three places:
- The measurement-body sentinel
bytes[0..4] == 7f ff ff ff ff. - The status block's state byte:
0x82overload,0x84underload. - Status byte bit
0x08clears briefly on the recovery transition out of the off-scale state (sub-second window) but is unchanged during the off-scale state itself — see §8.2.
[SURE] Detect the state itself by watching the sentinel + state byte; the bit-0x08 clearing window is a secondary signal useful for knowing when the ADC has fully re-settled after recovery.
8.2 Status block (also 8-byte body, subtype 0x48, returned by 0x30)¶
byte field type meaning
[0] aux_flag u8 Varies by family (see below).
0x00 on WZA/BCE; observed 0x08
on MSE under some states.
[1-2] marker 2 bytes always `00 81`
[3] state u8 family-dependent — see below
[4] status u8 family-dependent — see below
[5-6] marker_suffix 2 bytes always `10 00`
[7] seq u8 monotonic measurement counter.
On MSE, increments at the
measurement rate. On WZA, the
tick counter (0x35) ticks at
~50 Hz independently of the
measurement rate, so the `seq`
here may track a different
clock on simpler balances.
State / status byte encoding is family-dependent. Two patterns observed:
| Condition | Cubis (MSE) state | Cubis (MSE) status | non-Cubis (WZA, BCE) state | non-Cubis status |
|---|---|---|---|---|
| stable | 0x88 |
0x18 (ADC-trust + isoCAL-due off) |
0x08 |
0x20 |
| unstable | 0x80 |
0x08 |
0x00 |
0x00 |
| overload | 0x82 |
— | — (not captured) | — |
| underload | 0x84 |
— | — (not captured) | — |
Interpretation:
- On Cubis, bit 0x80 in state is the "measurement valid" base and
bit 0x08 on top marks stable. Status-bit 0x10 = isoCAL
attention/due; bit 0x08 = ADC-trust.
- On non-Cubis (WZA, BCE), the state byte never sets 0x80; bit
0x08 alone marks stable. Status bit 0x20 appears to carry a
"result-ready / settled" signal.
- Bit 0x08 is the stable indicator on both families — just placed
in different bytes.
Cross-family-portable stable detection: read the measurement-frame
flags byte's 0x40 bit (§8.1). That bit is universal; the
status-block state byte is not.
Byte [0] behaves as an aux-flag: 0x00 on WZA and BCE across all
observed states, but observed as 0x08 on MSE in one capture session
while state cycled 0x80/0x88 normally. Most parsimonious interpretation
is that byte[0] mirrors or extends the MSE-only ADC-trust bit; needs
more in-state captures to pin down. Parsers should not assume byte[0]
is zero.
Distinguishing short-measurement from status-block (both subtype 0x48,
both 8 bytes): status-block bytes [1..2] are reliably 00 81, status-block
bytes [5..6] are reliably 10 00. Short-measurement frames rarely
match those patterns.
8.3 Long streaming measurement (17-byte body, subtype 0x48)¶
Returned by 0x1E 09 30 — a short measurement concatenated with a status
block via a single-byte delimiter:
[0..7] short measurement (8 bytes, as §8.1)
[8] delimiter (always 0x48 — same as subtype!)
[9..16] status block (8 bytes, as §8.2)
The length byte of the overall frame is 0x14 (20), making the total frame 21 bytes. [SURE]
8.4 Unit encoding (bytes [5..6] of measurement body)¶
Byte [5] high nibble = number of decimal places displayed.
This is the universal rule, confirmed across MSE, WZA, and BCE for g,
kg, and mg displays at standard, 10×-hires (0x1F 21 01), and 100×-hires
(0x1F 21 02) resolutions. Observed values 0x00 (0 dec) through
0x70 (7 dec). The increment opcode 0x0D reports its value in the
current display unit, so the decimal count corresponds to displayed
resolution, not a fixed unit.
Byte [6] = sign_bits | base_unit_id:
| Top 2 bits of byte [6] | Sign meaning |
|---|---|
0x00 (00) |
exactly zero |
0x40 (01) |
positive |
0x80 (10) |
negative |
Base-unit IDs (low 6 bits of byte [6]):
| byte[6] low-6 | Unit |
|---|---|
0x02 |
g |
0x03 |
kg |
0x0D |
mg |
0x17 |
N |
So a complete decode is:
decimals = byte[5] >> 4
sign = byte[6] & 0xC0 # 0x00 zero, 0x40 pos, 0x80 neg
unit_id = byte[6] & 0x3F
mg low-nibble anomaly: on WZA at display=mg, byte[5] comes back with
low-nibble 3 (values 0x03, 0x13, 0x23 at std/10×/100× hires).
On MSE at display=mg (1 mg resolution, 0 decimals displayed) low-nibble
is 0. The anomaly correlates with WZA's 10 mg display resolution — a
balance that can't show 1 mg granularity in mg mode — but the exact
semantic isn't decoded. BCE's mg behaviour hasn't been captured. The
practical decoder rule is: use byte[5] >> 4 for decimals; treat the
low nibble as advisory/quirky.
9. Temperature sensors (opcode 0x76)¶
Live multi-sensor temperature read. TLV arg selects the sensor index. Installed sensors are family-dependent:
| Arg | Sensor role | MSE1203S | WZA8202-N | BCE3202-1S |
|---|---|---|---|---|
21 00 |
ambient / weighing zone | ~26–28 °C | 20.85 °C | 20.35 °C |
21 01 |
second ambient / internal | ~26–28 °C | sentinel | sentinel |
21 02 |
(reserved) | sentinel | sentinel | sentinel |
21 03 |
electronics / motor | ~37 °C | 30.15 °C | sentinel |
21 04+ |
— | err 0x04 | err 0x04 | err 0x04 |
Installed sensors → 3 (MSE) / 2 (WZA) / 1 (BCE); not-installed slots
return the sentinel 7f ff ff ff. A single-sensor basic balance is
expected for the simpler product classes.
Confirmed live (not stored) by observing LSB jitter across repeated reads on every balance — sensor-0 readings drift by tens of µ°C per sample on empty pans.
Return format: subtype 0x35 (typed_float, 5-byte body). Temperature is float32 °C.
On MSE, sensor 0's live reading is what the cal record (0xB9) snapshots into its first 4 bytes at the moment of a calibration event.
10. Parameter table (opcode 0x55)¶
Parameter-table size is family-dependent:
| Balance | Valid indices |
|---|---|
| MSE1203S | 70 (ranges 0-18, 24-25, 27-45, 50-79) |
| BCE3202 | 70 (same ranges as MSE) |
| WZA8202-N | 8 (indices 1-7, 9 only) |
MSE and BCE share the full 70-index parameter table; WZA's 8-index
subset is the outlier. The subsections below document MSE+BCE index
semantics; WZA exposes the first 8-9 indices with the same meanings
but narrower max values on a few (e.g. max=2 on p05 vs MSE's 3;
max=23 on p07 vs MSE's 24; max=5 on p09 vs MSE's 18) — consistent
with WZA being a simpler balance with fewer available options.
The 70 indices valid on MSE+BCE are organised in contiguous ranges:
Each read returns two u8 TLVs: (current_value, max_value). Writing is done
via 0x56 with TLVs (idx, val) — the frame shape is observed in legacy
captures but we have not exercised writes.
Address space is exactly 80 u8 indices, TLV-21 only. 0x55 rejects
wider-tag reads (22 XX XX, 24 XX XX XX XX, alt tags 11/12/14)
with err 0x07, and u8 indices 80..255 return err 0x03. There is no hidden
extended table reachable via wider TLV addressing. The 19 unmapped slots
on this unit are either firmware-reserved-but-unused or live behind
service-menu state that xBPI alone cannot trigger.
The max field is firmware-family-wide, not product-line-wide. On
MSE+BCE many params show max=18 or max=24 even though only a
handful of values are accepted — gaps are reserved for balances within
the family that expose more menu options. WZA has narrower max
values on a few indices, reflecting its simpler feature set.
10.1 Identified parameter mappings¶
48 of 70 indices mapped. Sorted by index:
| idx | Meaning | Values seen | max | Confidence |
|---|---|---|---|---|
| 1 | filter/ambient mode | 1=very stable, 2=stable, 3=unstable, 4=very unstable | 4 | [SURE] — equivalent to opcode 0x26 |
| 2 | application filter | 1=final_rd, 2=filling, 3=reduc, 4=off | 4 | [SURE] — fully mapped; orthogonal to p01 and p03 |
| 3 | stability range | 1=max_acc, 2=v_acc, 3=acc, 4=fast, 5=v_fast, 6=max_fast | 6 | [SURE] — fully mapped; orthogonal to p01 |
| 4 | stability delay | 1=no, 2=short, 3=average, 4=long | 4 | [SURE] — fully mapped |
| 5 | tare behavior | 1=wo_stab, 2=w_stab, 3=at_stab | 3 | [SURE] — fully mapped |
| 6 | auto-zero tracking | 1=on, 2=off | 2 | [SURE] — fully mapped |
| 7 | display unit | 1=userdef, 2=g, 3=kg, 4=ct, 5=lb, 6=oz, 7=troy_oz, 8=hktael, 9=sngtael, 10=twntael, 11=grains, 12=pennywt, 13=mg, 14=ptplb, 15=chntael, 16=mommes, 17=austrct, 18=tola, 19=baht, 20=mesgal, 21=tons, 22=lb_oz, 23=newton, 24=µg | 24 | [SURE] — fully mapped. userdef=1 is a user-defined multiplier (defaults to ×1 ≈ grams); the multiplier + label live in a separate register not yet located |
| 8 | display-accuracy mode | 1=default, 2=lponoff, 6=div1, 7=-1 digit | 18 | [SURE] — fully mapped for MSE1203S (only 4 menu options on this unit; gaps reserved for other balances). Value 7 drops 0x0D increment by 10× |
| 9 | cal button assignment | 1=cal_ext, 3=e_cal_usr, 4=cal_int, 8=set_prel, 9=del_prel, 10=blocked, 12=select, 17=set_ext_w | 18 | [SURE] — 8 live values on MSE1203S; gaps reserved |
| 10 | cal/adjust mode | 1=adj, 2=cal_adj | 4 | [LIKELY] — "adjust only" vs "calibrate+adjust". Values 3/4 unexplored |
| 11 | zero range | 1=1pec (1%), 2=2pec (2%) | 5 | [LIKELY] — runtime auto-zero range. Values 3–5 likely 5%/10%/20% |
| 12 | init-zero range | 1=default, 2=2perc | 7 | [LIKELY] — boot-time auto-zero range, distinct from p11. Values 3–7 unexplored |
| 13 | tare-on-power-on | 1=on, 2=off | 3 | [SURE] — does NOT bump 0xBA (boot-time flag, persisted separately) |
| 14 | cycle rate | 1=normal, 2=highvar, 3=slow, 4=medium, 5=fast, 6=very_fast, 7=max | 7 | [LIKELY] — values fully enumerated. p14 does NOT drive 0x57 cycle_time — 0x57 is driven by p01, see §14.4. What p14 actually drives is open. The ms mapping next to each value is a front-panel label; its correspondence to actual sampling/cycle behaviour is unverified. |
| 15 | isoCAL mode | 1=off, 2=note, 3=on | 4 | [SURE] — value 4 reserved. This is the persistent isoCAL mode. Status byte bit 0x10 is not the enable flag; after triggering internal adjustment with 0x28 21 78, p15 still read 3 while status changed 0x18 → 0x08 and the front-panel isoCAL symbol went from flashing to solid. Interpret bit 0x10 as isoCAL attention/due. |
| 16 | external-cal lock | 1=free, 2=locked | 2 | [SURE] — fully mapped |
| 25 | runtime-state register (class "boot-resets-to-1") | resets to current=1, max=0 on cold boot; drifts upward during runtime (observed 1 → 2 in normal operation). Max oscillates 0 ↔ 5 paired with the reset. |
5 (warm) / 0 (cold) | [LIKELY] — part of a register class with p56 and p58 (all three reset to 1 on cold boot together). Opcode 0x25 is a direct-read alias. 0xB5 idx 1 co-moves with the same class. Semantic meaning (what drives the runtime drift) undetermined. |
| 31 | peripheral baud rate | 3=600, 4=1200, 5=2400, 6=4800, 7=9600, 8=19200, 9=38400, 10=57600, 11=115200 | 11 | [SURE] — 9 live values on MSE1203S; gaps 1/2 reserved (150, 300). Uses different encoding than opcode 0x5C |
| 32 | peripheral parity | 3=odd, 4=even, 5=none | 5 | [LIKELY] — gaps 1/2 reserved (mark/space) |
| 33 | peripheral stop bits | 1=1, 2=2 | 2 | [SURE] — fully mapped |
| 34 | peripheral handshake | 1=software, 2=hardware, 3=none | 3 | [SURE] — fully mapped |
| 35 | peripheral data-receiver protocol | 1=sbi, 2=xbpi, 4=rem_disp, 7=uni_print, 8=lab_print, 10=off | 10 | [SURE] — gaps 3/5/6/9 reserved. Controls the 9-pin peripheral port only, NOT the PC-USB port. Verified 2026-04-25: writing p35=1 + 0x47 SAVE_MENU + soft and hard reboot did not flip PC-USB protocol; PC-USB stayed in xBPI throughout. The PC-USB protocol selector is not in the parameter table at all (indices 0-79 swept). Front-panel Device → PC-USB → Dat.Rec. is the only path. |
| 36 | SBI output mode | 1=ind_no, 2=ind_after, 3=ind_at, 4=auto_wo, 5=auto_w | 6 | [SURE] — (trigger × stability-filter) matrix. Value 6 unexplored. auto_wo = without stability filter (continuous, ~60 Hz on MSE1203S); auto_w = with stability filter (only stable values). Does NOT bump 0xBA on write (verified 2026-04-25); same persistence class as p13/p50. Setting p36 ≥ 4 arms autoprint but only takes effect when the port is physically in SBI mode — switching protocol mode is what activates the stream. |
| 37 | auto-output stop | 1=off, 2=on | 2 | [SURE] — fully mapped; paired with p36 auto modes |
| 38 | auto-output cycle/interval | 1=each_val, 2=after_2 | 7 | [LIKELY] — values 3–7 likely after_5/10/20/50/100 cycles |
| 39 | SBI output format | 1=16_char, 2=22_char, 4=extra_line | 4 | [LIKELY] — value 3 unexplored |
| 40 | menu access mode | 1=can_edit, 2=rd_only | 2 | [SURE] — fully mapped (menu-lock flag) |
| 41 | external-key assignment | 1=print, 2=z_tare, 3=cal, 5=cf, 6=enter, 9=dr_shield, 10=ionizer, 11=appl, 12=asterisk | 12 | [SURE] — 9 live values on MSE1203S; gaps 4/7/8 reserved |
| 44 | cal unit | 1=g, 2=kg, 4=user_def | 4 | [SURE] — value 3 reserved (likely ct). Note: encoding ≠ opcode 0x79's args |
| 50 | acoustic signal (beep on stable) | 1=off, 2=on | 2 | [SURE] — note reversed polarity vs p13. Does NOT bump 0xBA |
| 53 | power-on mode | 1=off_on_sb, 2=off_on_ao, 3=on_sb, 4=auto_on | 5 | [SURE] — value 5 reserved |
| 56 | runtime-state (class "boot-resets-to-1") | observed 2 → 1 on cold boot | 5 | [LIKELY] — same register class as p25 and p58; all three reset to 1 on cold boot. Semantic meaning undetermined. |
| 57 | door-open resolution | 1=all, 2=reduc | 2 | [SURE] — display behavior when draft-shield door is open |
| 58 | runtime-state (class "boot-resets-to-1") | observed 3 → 1 on cold boot | 4 | [LIKELY] — same register class as p25 and p56; all three reset to 1 on cold boot. Semantic meaning undetermined. |
| 59 | level-check behavior | 1=off, 2=note_to, 3=err_msg | 3 | [SURE] — fully mapped. Paired with p60 |
| 60 | leveling mode | 1=key (manual), 2=auto | 2 | [SURE] — fully mapped. Paired with p59 |
| 61 | peripheral data bits | 1=7, 2=8 | 2 | [SURE] — fully mapped. Not adjacent to p31–p34 block |
| 63 | PC-USB baud rate | 4=1200, 5=2400, 6=4800, 7=9600, 8=19200, 9=38400, 10=57600 | 11 | [SURE] — same encoding as p31. 3=600 and 11=115200 inferred by pattern (not verified) |
| 64 | PC-USB parity | 3=odd, 4=even, 5=none | 5 | [SURE] — same encoding as p32. Gaps 1/2 reserved (mark/space) |
| 65 | PC-USB stop bits | 1=1, 2=2 | 2 | [SURE] — same encoding as p33 |
| 66 | PC-USB handshake | 1=software, 2=hardware, 3=none | 3 | [SURE] — same encoding as p34 |
| 67 | PC-USB data bits | 1=7, 2=8 | 2 | [SURE] — same encoding as p61 |
| 68 | auto-tare after output | 1=off, 2=on | 2 | [SURE] — fully mapped |
| 69 | print-param result mode | 1=man_no, 2=man_after, 3=man_at, 6=auto_lc | 6 | [LIKELY] — values 4/5 unexplored (likely auto_wo/auto_w per p36 pattern) |
| 70 | print-param output format | 1=16_char, 2=22_char, 4=extra_line | 4 | [LIKELY] — value 3 unexplored. Same encoding as p39 |
| 71 | print init/header | 1=off, 2=all, 3=main_par | 3 | [SURE] — fully mapped |
| 72 | GLP print mode | 1=off, 2=cal_adj, 3=always | 3 | [SURE] — fully mapped |
| 73 | tare-print | 1=off, 2=on | 2 | [SURE] — fully mapped |
| 74 | time format (print) | 1=24_hr, 2=12_hr | 2 | [SURE] — fully mapped |
| 75 | date format (print) | 1=dd_mmm_yy, 2=mmm_dd_yy | 2 | [SURE] — fully mapped |
Unmapped indices (22): p00, p17, p18, p24, p25 (unknown), p27–p30, p42, p43, p45, p51, p52, p54–p56, p58, p62, p76–p79.
These did not surface through any front-panel menu toggle we ran. Likely homes: application-program sub-settings (per-app tolerances, nominal weights, sample sizes, density reference liquids) or deeper service menus not accessible without elevated credentials.
Two distinct persistence classes exist in the balance, distinguishable
by whether 0xBA (config counter) bumps on write:
- Weighing-pipeline config (bumps 0xBA): filter mode, stability range, display accuracy, display unit, cal settings, communication settings — most of the table.
- UI preferences / boot flags (don't bump 0xBA):
p13tare-on-power-on,p36SBI output mode (autoprint switch),p50acoustic signal. These writes bypass the main config-generation counter, likely because they're persisted to a separate EEPROM section that the host doesn't need to resynchronize on. (Cache-invalidation logic indevices/session.pymust therefore not rely on0xBAto detect changes to these indices — invalidate explicitly on the write path instead.)
10.2 What is NOT in the parameter table¶
Several menu-accessible settings are not reachable through the parameter table or any other xBPI probe we have found:
- Application mode and its sub-settings (weighing / counting / percent /
net-total / totalizer / animal-weighing / calculation / density; plus the
per-app sub-menus and mode-activation workflows like count's
reference-quantity entry). Toggling through all 8 modes and exercising
count-mode sub-settings (resolution, ref-upt) against the full 80-index
param table, every structured read, and every no-args 0x00–0xFF sweep
produces zero byte differences between settings.
0xBAbumps on every commit, confirming the balance persists each change — the setting values are simply not exposed via xBPI. The only app-activity signals are0xBAbumps on commits and transient0x62/0x50/0x51movements that carry state-machine information but no setting values.
Hypothesis: app mode and its sub-settings are front-panel/UI concepts that reconfigure display and post-processing but not the measurement stream xBPI exposes. [LIKELY] Consequence: the unmapped param-table indices (p27–p30, p42–p45, p51/p52/p54/p55, etc.) are NOT in app-menu territory — they must live elsewhere, most likely behind service-menu access (§13.3 C).
-
User ID (free-entry 7-char alphanumeric, "Input" menu): setting it bumps
0xBAbut the string appears nowhere — not in the param table, not in any structured probe, not in the full 256-opcode no-args wide sweep.0x03(read_user_id per WZG) returns a zeroed measurement frame regardless of args on this unit. The ID is stored in a register that requires specific TLV-addressed reads not yet discovered. By extension, the "date", "time", "password", and "cal weight" free-entry fields in the Input menu are likely in the same category. -
Display language (English / Deutsch / français / italiano / español / …): bumps
0xBAbut produces zero readable diffs across any language pair, even with the wide sweep. Language is committed to balance state but unreachable via current xBPI probing. Likely stored in a display-controller chip on a separate bus from the main xBPI processor.
11. Known workflows¶
11.1 Tare¶
No arguments, no polling. 0x14 is the real tare command.
11.2 Read weight¶
11.3 Stream weight (long format)¶
→ 06 01 09 1e 09 30 67 read net with status block attached
← 14 41 48 <17-byte long measurement> <chk>
The balance does not stream continuously — each frame is one-shot per
request. To poll, re-send. The internal measurement rate is tied to p14
cycle rate (2.5 Hz at "normal" down to 100 Hz at "max"), so polling faster
than the measurement rate yields duplicate frames until the internal seq
counter advances.
11.4 Read all 4 temperature sensors¶
for sensor in (0, 1, 3): # index 2 is "not installed" on MSE1203S
send( 0x76 + [21, sensor] )
parse reply as subtype 0x35 typed_float
11.5 Read last calibration record¶
Decode per §7.12. If all metadata bytes are zero, no cal has been recorded since the last EEPROM clear.
12. Implementation notes¶
- The checksum (sum & 0xFF) is trivial; no CRC involved.
- Frame boundaries are unambiguous: read length byte, then
lengthmore bytes, then validate checksum. - Expect to handle unexpected frames if the balance has continuous
output enabled from SBI mode (seen once in this project — the balance
auto-printed SBI-style ASCII lines even when set to xBPI because
"autoprint" was left on from a prior SBI session). Before opening
a new session, clear the input buffer and consider probing
0x32to validate xBPI is live. -
Wide-sweep self-poisoning: when enumerating every opcode 0x00–0xFF to discover behavior, exclude at minimum the ACK-only opcodes that have side effects —
0x59and0x5Aclear the xBPI 0x62 flag and bump0xBA. Including them in a sweep corrupts subsequent diffs with phantom "0x62 changes" caused by the tool itself, not the system under test. Thesartoriustesting.cli.snapshottool excludes them from its wide sweep. -
TLV-args-unlock writes: many opcodes that err on no-args silently ACK (executing a write) when given TLV-21 args. An arg-diverse wide sweep that only skips the known-destructive no-args set is unsafe — an undocumented write register can push the balance into an overload state. The SKIP list in snapshot.py:254 covers every WZG-side-effecting op plus every empirically-identified ACK-on-TLV unknown. Always run arg-diverse sweeps behind a recent baseline so any residual state drift is visible on recovery.
-
Bootloader-drop risk on wide sweeps — SKIP list may be insufficient for unfamiliar balances: a read-only no-args opcode sweep on a BCE3202 (with an MSE-learned SKIP list plus the WZA-learned
0x9Fsilent-ACK opcode) left the balance stuck in firmware bootloader mode ("B.LOADER" on front panel). Power-cycling recovered cleanly. The reply log showed no opcode that ACK'd unexpectedly, meaning at least one opcode in the err-0x07 invalid-args class on BCE is not a pure read — its no-args call has a delayed or state-sensitive side effect. Do not run an undifferentiated opcode sweep on an unfamiliar balance. Characterise each novel opcode with a single-shot probe bracketed by a baseline-diff (reads of known stable registers before and after) and a front-panel visual check. The cost of catching a bootloader drop early is low; the cost of a stuck unit without recovery is catastrophic. -
xBPI signals vs real state:
0x62(and correlated0x0Fbit 6) are menu-activity signals on the xBPI side only; they do not reliably track which app is running on the front panel. Use0xBAbumps as the canonical "something persisted" signal rather than inferring mode from any single register.
13. Outstanding items¶
13.1 Relationship to the WZG manual¶
This document is a strict superset of the WZG manual. WZG documents 28
opcodes (0x00–0x07, 0x0A–0x0F, 0x13–0x22, 0x26, 0x28–0x29,
0x2C–0x2F, 0x32, 0x35–0x36, 0x40–0x41, 0x46–0x47, 0x4A–0x4B,
0x54–0x58, 0x5C, 0x71–0x72, 0x76, 0x78–0x79); all are covered here
plus ~25 Cubis-specific extensions. WZG offers no additional leads on our
outstanding unknowns: the data-returning unknowns (0x08, 0x25, 0x31,
0x33, 0x48, 0x7E, 0xAA, 0xB5, 0xB7, 0xBB, 0xBD) don't appear
there, the parameter-table section is a bare "Timeless menu index" stub, the
status byte has no bit-level documentation, app mode / language / user-ID
storage are absent, the 0xB9 cal record is not mentioned, and service access
is referenced only as a physical hardware port with no xBPI command.
13.2 Data-returning opcodes of unclear semantic meaning¶
Each returns stable structured data but the meaning isn't decoded. Most
likely service/diagnostic registers (calibration coefficients, factory
trim values, hardware-version identifiers): 0x31, 0x33, 0x48, 0x7E,
0xAA, 0xB5, 0xB7, 0xBB, 0xBD. Individual characterisations are
in §7.11.
13.3 Undocumented write opcodes¶
Five Cubis-specific writes characterised only by error-code texture under
TLV-21 args 00/01/02. One of them (most likely 0xB6) caused an
overload-state lockup during discovery — they remain in the wide-sweep
SKIP list pending isolated probe-op characterisation against a known
baseline.
| Op | arg 00 | arg 01 | arg 02 | Interpretation |
|---|---|---|---|---|
0x2D |
out_of_range | ACK | not_applicable | Single-valid-arg write; arg 1 triggers a one-shot action |
0x53 |
ACK | out_of_range | ACK | Accepts 0 and 2, rejects 1 |
0x61 |
ACK | ACK | unknown_opcode | Binary arg |
0x6B |
ACK | ACK | out_of_range | Binary arg |
0xB6 |
ACK | ACK | ACK | Broadest acceptance; prime overload-state suspect |
13.4 Parameter-table indices still unmapped¶
19 of 70 indices remain unmapped after exhausting app-layer and user-menu
toggles: p00, p17, p18, p24, p27–p30, p42, p43, p45, p51, p52, p54, p55,
p62, p76–p79. Three more (p25, p56, p58) are partially characterised as
the "boot-resets-to-1" class but their semantic meaning is undetermined.
These slots are reachable via 0x55 reads but don't respond to any
user-accessible menu — they most likely live behind the service menu
(§13.6).
13.5 State-gated opcode cluster¶
A long-arg opcode sweep found opcodes that exist in firmware but return err 0x06 (operation_not_applicable) rather than err 0x04 (unknown_opcode) regardless of arg shape:
0x4C,0x4D,0x4F— adjacent to the EEPROM/ISI ops (0x46/0x47/0x4A/0x4B); probably cal- or EEPROM-workflow sub-ops.0x77,0x7A— adjacent to temperature / adjustment-unit ops (0x76/0x78/0x79); probably cal-family.0xA0,0xA2,0xA3,0xA4,0xA5,0xA7,0xAB,0xAE,0xB0— a contiguous cluster with no known siblings. Strongest candidate for a service-menu API surface.
The gating condition is unknown. Most likely requires entering a specific balance state (active cal, active tare, service-mode-unlocked) before responding. Worth a future session that deliberately enters cal / tare / zeroing states and re-sweeps the cluster to see which become valid in each.
13.6 Service menu (Cubis challenge-response)¶
Cubis balances protect service-level settings behind a 6-digit
challenge-response. The balance displays a per-session random 6-digit
nonce and requires a 6-digit response F(nonce) that only Sartorius
service tooling knows. Brute force across sessions is infeasible — the
nonce resets each attempt and the search space is 10⁶.
xBPI has no visibility into this flow. The authentication runs on the front-panel / display-controller microcontroller, which shares a bus with language selection, user-ID, and app-layer state (§10.2). At-the-prompt and at-entry snapshots contain no copy of the nonce in any encoding (u32/ u24/u16 BE or LE, BCD, ASCII), no param-table change, and no state-gated opcode activation — the state-gated cluster above does NOT unlock in the password-entry state.
Service-access mechanisms across the Sartorius product line differ:
- Older / industrial balances (Combics, Masterpro, Signum, Expert LE)
combine a hardware "menu access switch" (S1 jumper) on the mainboard
with static default passwords —
202122(service) and40414243(general), following a sequential-integer pattern (documented in the Combics CAW1P and Masterpro manuals). - Cubis I/II and newer use the 6-digit challenge-response described
above. No hardware switch is documented in user materials. Neither
202122nor the identity-key (response == nonce) unlocks the Cubis.
The service software implementing F is SARTOCAS Service Program for
PCs v1.10+ (older equivalent: PSION server CAS v4.5+). Distributed only
to authorised Sartorius service engineers via MySartorius service-tier
accounts; no public leak found. If available, a copy would enable two
attacks: (a) sniff the xBPI traffic between Sartocas and the balance via
the sarto-tap CLI to observe the unlock sequence, and (b) static
analysis of the binary to extract F directly.
Nonce-generator characterisation (15 consecutive samples): no
LCG-compatible pattern across standard moduli (10⁶, 2²⁰, 2²⁴, 2³⁰, 2³¹,
2³²), GCD of consecutive differences = 1, no XOR or multiplicative
structure between pairs. Nonces span most of the 6-digit range (117k–899k)
with roughly uniform overall digit frequencies. Statistically significant
last-digit bias toward 0 (6 of 15 nonces end in 0 vs 10% expected,
p ≈ 0.002) — suggests display-formatting truncation or a timer-derived
source with coarser internal resolution than the 6-digit display. The
bias characterises the generator but does not recover F.
Status: closed for xBPI-only reachability. Access requires one of (a) a Sartocas binary for analysis, (b) an undocumented hardware jumper on the Cubis mainboard, (c) a JTAG/SPI firmware dump of the front-panel microcontroller, or (d) a legitimate Sartorius service engagement.
13.7 Miscellaneous open questions¶
0xB9bytes [13..15] are confirmed cal-event counters but their exact per-field semantics — and specifically why byte [13] increments by +2 per cal where [14] and [15] increment by +1 — are unresolved.- TLV-addressed reads with wider tags. Fuzz testing of
0x03,0x08,0x25,0x31,0x33,0x48,0x50,0x51,0x7E,0xAA,0xB5,0xB7,0xBBunder TLV-11/12/14/22/24 args returned no new data — the protocol appears to be strictly TLV-21-only for slot addressing, with other opcodes either ignoring args or rejecting wider tags.0x03(WZG-documented read_user_id) continues to return a zeroed measurement frame on any arg form; the user-ID storage location remains unknown. - Per-unit correlation. If a second Cubis is accessible, comparing
0x08,0xAA,0xB7, and the other factory-constant-looking opcodes would identify which fields are factory-trim / serial-derived (differ per-unit) vs. universal product constants (identical).
13.8 Destructive opcodes (use caution)¶
The following must not be exercised without clear intent:
0x04write user ID0x28start adjustment (wrong args can trigger a real calibration)0x40reconfiguration0x41reset temporary errors0x56write parameter table0x58initiate reset (warm/cold)0x5Cset baud rate (can silently break comms)0x72write SBN address (can silently break comms)0x79write adjustment unit
Plus the five undocumented writes in §13.3 (0x2D, 0x53, 0x61,
0x6B, 0xB6) — any of these can push the balance into an overload
state that only clears on cold boot.
14. Cross-family observations¶
Everything in §1-13 has been validated across three balances. Protocol features described there without a family qualifier are universal unless otherwise noted. This section consolidates the family-specific behaviours and the opcodes that exist on some balances but not others.
14.1 Balance identification¶
| Attribute | MSE1203S-100-DR | WZA8202-N | BCE3202-1S |
|---|---|---|---|
| Class | Cubis analytical | OEM weigh cell | Basic laboratory |
| Capacity × resolution | 1200 g / 1 mg | 8200 g / 10 mg | 3200 g / 10 mg |
| xBPI default baud | 19200 | 1200 | 9600 |
| Ships-from-factory mode | SBI command/reply | SBI autoprint (1200-7-O-1) | SBI command/reply |
| SBN | 0x00 | 0x00 | 0x00 |
0x02 model string |
"MSE1203S-100-DR" |
"WZA8202-N" |
"BCE3202-1S" |
| Parameter table size | 70 indices | 8 indices | 70 indices |
| Temperature sensors installed | 3 (idx 0, 1, 3) | 2 (idx 0, 3) | 1 (idx 0) |
14.2 Opcode presence matrix¶
Key opcodes and their behaviour on each balance. ✓ = works,
err_04 = unknown_opcode, err_07 = invalid-args (exists but needs
args not yet characterised):
| Opcode | Purpose | MSE | WZA | BCE |
|---|---|---|---|---|
0x00–0x07 |
identity reads | ✓ | ✓ | ✓ |
0x08 |
factory constant | 5b 67 df |
00 00 00 |
00 00 00 |
0x0B–0x0F |
metrology, balance info | ✓ | ✓ | ✓ |
0x1E/0x1F/0x20/0x22 |
measurement | ✓ | ✓ | ✓ |
0x26 |
weighing mode | ✓ | ✓ | ✓ |
0x2F |
gross bargraph | ✓ | ✓ | ✓ |
0x30/0x32 |
status block/short | ✓ | ✓ | ✓ |
0x35/0x36 |
timestamp/on-off | ✓ | ✓ | ✓ |
0x48 |
stored reference | 1000.0 g | zeroed | 5000.0 g |
0x55/0x57 |
param table, cycle time | ✓ | ✓ | ✓ |
0x62 |
menu-activity register | ✓ (active) | reads 0 (dead) | err_04 |
0x71 |
SBN read | ✓ | ✓ | ✓ |
0x76 |
temperature | ✓ | ✓ | ✓ |
0x78 |
read adjustment unit | — (unprobed) | — | ✓ (= 2, g) |
0xAA |
universal constant | ✓ | ✓ | ✓ |
0xB9 |
last cal record | ✓ | err_04 | ✓ (zeroed — no cal) |
0xBA |
config generation counter | ✓ | err_04 | ✓ |
0xBC |
module list | ✓ | — (untested) | — |
0xFF |
NOP | ACK | ACK | ACK |
| BCE-specific (see §14.6) | various | err_04 | err_04 | various |
14.3 Metrological thresholds (0x0B / 0x0E)¶
0x0D is the live increment (display-unit dependent). 0x0B and
0x0E are thresholds that scale with the increment — but the 0x0B
ratio depends on display resolution:
| Balance | Resolution | 0x0D increment | 0x0B | 0x0E |
|---|---|---|---|---|
| MSE1203S | 1 mg | 0.001 g | 0.1 g (= 100× incr) | 0.01 g (= 10× incr) |
| WZA8202-N | 10 mg | 0.01 g | 0.5 g (= 50× incr) | 0.1 g (= 10× incr) |
| BCE3202 | 10 mg | 0.01 g (10⁻⁵ kg at kg display) | 0.5 g (= 50× incr) | 0.1 g (= 10× incr) |
0x0E is consistently 10× increment across families. 0x0B is 100×
on 1-mg balances, 50× on 10-mg balances — a resolution-dependent
ratio, not a family-dependent one.
14.4 Cycle-time curve is universal (p01-driven)¶
0x57 cycle_time tracks p01 (filter / ambient mode) identically on
every balance that has both indices:
| p01 | label | 0x57 |
|---|---|---|
| 1 | very stable | 50 ms |
| 2 | stable | 100 ms |
| 3 | unstable | 200 ms |
| 4 | very unstable | 400 ms |
Confirmed on MSE at p01=4 (400 ms), WZA at p01 ∈ {2,3,4} (100/200/400 ms),
and BCE at p01 ∈ {2,3} (100/200 ms). The doubling sequence is universal.
p14 exists on MSE and BCE as a separate parameter but does NOT drive
0x57: isolating on BCE with only p01 changing (and p14 held at 1) moved
0x57 200→100 ms as p01 went 3→2. What p14 drives is open.
14.5 State / status byte encoding by family¶
See §8.2 for the layout and family-specific state byte meanings. In summary:
- Cubis (MSE): state byte has bit
0x80set (unstable0x80, stable0x88); status byte uses bit0x08for ADC-trust and bit0x10for isoCAL-due. - Non-Cubis (WZA, BCE): state byte never sets
0x80— stable is0x08, unstable is0x00; status byte uses bit0x20for a "measurement ready" signal.
Cross-family stable detection should use the measurement-frame flags
byte's 0x40 bit (§8.1), which is universal.
14.6 BCE-specific opcodes¶
All of the following return err_04 unknown_opcode on MSE and WZA.
They are BCE-family reads.
14.6.1 0x75 raw load ADC¶
Returns a 4-byte body under novel subtype 0x14, linearly correlated
with load:
0x75 ≈ 310,118,054 + 79,767 × mass_g(≈ 12.5 µg per ADC count)
Verified on BCE3202 across three loaded mass points (18.27 g / 199.86 g /
699.54 g) with < 0.01% residuals. The empty-pan reading sits ~785,000
counts above the extrapolated zero-mass intercept (≈ 9.8 g equivalent),
because the balance's displayed "0.00 g" is a software-tared value that
0x75 doesn't participate in.
Practical use — field-calibrate two points and interpolate:
- Tare or leave pan empty, read
0x75→ baselineB. - Place known mass
m_ref, read0x75→ referenceR. - For any later reading
v:mass ≈ (v − B) × m_ref / (R − B).
Do NOT anchor on the literal y-intercept constant 310,118,054 — it
isn't an observable state.
0x75 exposes sub-display-LSB dynamics that the balance's digital
filter hides: ~500-count oscillation (~6 mg) on a stable empty pan;
visible creep/relaxation (hundreds of counts) during mass settling;
tighter absolute noise at larger loads (160 counts at 700 g vs 500 at
18 g). Candidate applications: custom filters (EMA/Kalman), creep
monitoring, sub-LSB averaging for steady-state measurements.
First publicly-documented raw-ADC access on any Sartorius xBPI balance. No published SBI/xBPI driver surfaces this.
14.6.2 0x3B — likely user-unit label storage¶
Stable 12-byte body (subtype 0x48, novel length):
Decomposes as: multiplier: float32 (1.0 default), 4 middle bytes
(flags or count), 4 trailing ASCII bytes ("Cg "). Hypothesis:
storage for p07's userdef=1 user-defined display unit, matching
§10.1's note that the user-defined multiplier and label "live in a
separate register not yet located." Unchanged across 5 s — a static
register, not time-varying. To confirm, program a user-defined unit
via the front panel and watch the label field change.
14.6.3 Short novel registers¶
All return 1-byte bodies (subtype 0x21 or 0x41), stable across reads:
| Opcode | Subtype | Body | Notes |
|---|---|---|---|
0x24 |
0x21 | 01 |
Unknown u8 register |
0x33 |
0x22 | 10 00 |
The "magic 1000" constant now reachable as an opcode response (same value that appears in status-block byte [5..6]) |
0x34 |
0x24 | 00 00 00 |
3-byte body on a subtype nominally 4-byte — possibly malformed or different layout |
0x3D |
0x21 | 00 |
Unknown u8 |
0x5B |
0x21 | 00 |
Unknown u8 |
0x6F |
0x21 | 10 |
Unknown u8 |
0x7C |
0x41 | 00 |
Novel subtype (0x41 = long_data family, 1-byte length) |
14.6.4 Args-accepting reachable surface (uncharacterised)¶
52 opcodes on BCE return err_07 invalid_or_missing_args on no-args
and all TLV tag variations tried (11/12/14/21/22/24). They exist but
want argument shapes not yet guessed — most likely multi-TLV
structured args (e.g. 21 <slot> 35 <float32> <aux> write-with-value
forms). Characterisation is blocked by the bootloader-drop risk:
0x06, 0x1B, 0x27, 0x2A, 0x3A, 0x3E, 0x3F, 0x42, 0x43, 0x44, 0x45, 0x49,0x4B, 0x4C, 0x4D, 0x4E, 0x4F, 0x52, 0x5D, 0x63, 0x65, 0x6C, 0x6E, 0x70,0x7A, 0x7B, 0x7D, 0x7F, 0x80, 0x82, 0x83, 0x8D, 0xA9, 0xAA, 0xAC, 0xAD,0xB0, 0xB1, 0xB8, 0xBF, 0xC1, 0xC3, 0xC4, 0xC5, 0xC7, 0xC8, 0xC9, 0xCA,0xCB, 0xD3, 0xD5, 0xD7, 0xD8
Requires per-opcode characterisation against a baseline with cold-boot recovery budget. See §12 for the bootloader-drop incident.
14.6.5 0x9F — silent-ACK write¶
Acks every TLV-21 arg with no visible change in any monitored register. Almost certainly a write. On the permanent SKIP list until individually characterised.
14.7 MSE (Cubis)-specific features¶
0x62— active menu-activity register on MSE (value varies transiently with menu interaction). Reads as constant0x00on WZA (dead register) and is entirely absent on BCE (err_04). Treat as Cubis-exclusive.0x08— returns factory-trim body5b 67 dfon MSE. Both WZA and BCE return00 00 00. Appears to be MSE-populated factory data (hardware-variant token or build checksum) with the register unpopulated elsewhere.- State byte with
0x80set — Cubis-only encoding. See §8.2. - Cycle-time driven by p01 only — MSE has both p01 and p14 but
0x57tracks p01 just like WZA and BCE; p14's effect remains open. - Status-block byte [0] may be non-zero on MSE — one session
observed
0x08consistently. See §8.2 notes.
14.8 WZA-specific features¶
- Reduced parameter table: 8 valid indices (p01-p07 plus p09) versus MSE+BCE's 70. See §10.
- mg-display byte[5] low-nibble anomaly: at display=mg,
byte[5]comes back with low-nibble3(values0x03/0x13/0x23) instead of0. Correlates with WZA's 10 mg resolution (display can't reach 1 mg granularity in mg mode). See §8.4. - Time-stamp tick decoupled from measurement rate:
0x35on WZA ticks at ~50 Hz independently of cycle_time (which is 100-400 ms). On MSE,0x35ticks at the measurement rate. 0xBAand0xB9are absent (err_04 unknown_opcode) despite working on both MSE and BCE. The0x0B9cal-record register and0xBAconfig-gen counter are genuinely missing from WZA firmware, not Cubis-specific as we originally hypothesised.
14.9 Universal firmware constants (0xAA)¶
0xAA with TLV-21 arg 00 or 01 returns body
14 21 12 00 97 14 00 95 b7 44 on all three balances, byte-for-byte
identical. Decodes as two u32 TLVs:
0x14 21120097= 0x21120097 (decimal 555,745,431)0x14 0095b744= 0x0095b744 (decimal 9,811,268)
TLV-21 args ≥ 02 return err_03. Meaning not decoded; strongest candidates are firmware-build ID and protocol-version tokens. The byte-for-byte match across three families makes this the best candidate for a universal xBPI fingerprint.
14.10 Other structural observations¶
0x0Aconfiguration_data embeds the current unit descriptor in its body bytes [5..6]. At display=g, bytes [5..6] =02 02(2 decimals, g); at display=kg on BCE,05 03(5 decimals, kg). The configuration-data structure reuses the unit-descriptor encoding from measurement frames.0x50 slot availabilityvaries: MSE has slots 0 and 1 both valid (u8=3); WZA has the same pair; BCE has only slot 0.0x48stored-reference differs per balance: 1000.0 g on MSE, 5000.0 g on BCE (exceeds BCE's 3200 g max, so not a user-settable reference). Appears to be a firmware-wired constant chosen at manufacture.
14.11 Bootloader-drop incident¶
A read-only no-args opcode sweep on BCE3202-1S (with an MSE-learned
SKIP list plus the WZA-learned 0x9F silent-ACK opcode) left the
balance stuck in firmware bootloader mode ("B.LOADER" on the front
panel). Power-cycling recovered cleanly. No reply in the capture log
showed an opcode ACK'ing unexpectedly, so at least one opcode in the
err_07 invalid-args response class on BCE has a delayed or
state-sensitive side effect when called with no args.
This means the §13.8 SKIP list is not sufficient on unfamiliar balance families. The WZA+MSE-derived safe list, which worked on both those balances without incident, triggered the drop on BCE.
Safe exploration strategy for an unfamiliar balance:
- Characterise each opcode with a single-shot probe, not a sweep.
- Bracket each probe with baseline diffs (reads of
0x32,0xBA, a few param-table indices) before and after. - Between probes, visually check the front panel — a bootloader drop is front-panel-visible before any serial symptom.
- Keep a cold-boot recovery budget — the BCE drop cleared with a single power cycle, but don't rely on it.
14.12 Open items¶
- BCE bootloader-drop culprit. Needs bisection probing with a cold-boot budget per iteration.
- BCE state-byte encoding under shake. Only stable captured.
- BCE args-accepting opcode semantics (§14.6.4). The richest untapped reverse-engineering surface.
- What p14 actually drives. Confirmed not
0x57; still unknown. 0x3Buser-unit label change via front-panel unit programming, to confirm the user-defined-unit storage hypothesis.0x75on MSE alternatives. Confirmed absent on MSE; whether MSE has a different raw-ADC opcode is open.- Per-unit variation of
0x08within the MSE family (we have one MSE). A second MSE would tell us whether5b 67 dfis per-unit factory trim or family-wide. - Status-block byte [0] semantics on MSE — observed
0x08in one capture session,0x00in earlier ones. Needs state-correlated captures.
15. References¶
- This repository: everything under
src/sartoriustesting/— the Python reference implementation of the findings documented here. - WZG OEM weigh cell manual: shipped with Sartorius WZG OEM weigh cells. A local copy lives at datalab-output-Bilance_Tecniche_Sartorius_OEM_IP65_DS-WZG-e.md in this project.
- Experiment playbook: EXPERIMENT_PLAYBOOK.md — runbook for the snapshot → toggle → diff workflow.
- Captured sessions:
captures/— JSONL and JSON files with raw xBPI frames and decoded state snapshots from every experiment feeding into this document. Organized by experiment:paramdiff/vNN-<setting>/for menu-toggle captures,acks/for opcode-probe experiments, plus the earlyexpN-*.jsonlcaptures from initial frame-format discovery.