Skip to content

XTREM Protocol

Robin Krämer edited this page Sep 29, 2026 · 2 revisions

This page covers the xtrem::protocol module: the wire format and how register payloads are decoded. The module is pure: no sockets and no timers. Section numbers (§) refer to the XTREM protocol specification v3.007.

Frame format

Every message is one ASCII frame, whether it's a request or a response:

STX  ID_O  ID_D  F  D_ADDRESS  D_L  DATA  LRC  ETX  [CR LF]
 1    2     2    1      4       2   D_L    2    1     2      ← bytes
0x02                                            0x03
Field Encoding Meaning
STX / ETX 0x02 / 0x03 start and end of the frame
ID_O 2 hex chars sender's network address (device ID)
ID_D 2 hex chars destination's network address. FF (BROADCAST_ID) is broadcast: every module answers (§5.1).
F 1 char function, see below
D_ADDRESS 4 hex chars register
D_L 2 hex chars length of DATA in bytes, so at most 255 (MAX_DATA_LEN)
DATA raw ASCII, not hex-encoded payload. Every byte must be in 0x20..=0xFF.
LRC 2 hex chars XOR of every byte from ID_O through the last DATA byte (§5.6)
CR LF optional outside the frame and not part of the LRC. The Wi-Fi/Ethernet module requires it (§17), and it's also the serial port's factory default (register 0012h).

The encoder writes uppercase hex. The decoder also accepts lowercase.

Functions

Uppercase is a request, lowercase is the matching response (§5.2):

Request Response Function variants
R read r ReadRequest / ReadResponse
W write w WriteRequest / WriteResponse
E execute e ExecuteRequest / ExecuteResponse

Function::response() maps a request to the response it expects, and is_request() tells them apart.

Write and execute responses carry a single result byte:

Byte WriteResult ExecuteResult
'0' Ok Ok
'1' SealProtected: the sealing switch is locked SealProtected
'2' ReadOnly Other(b'2')
'3' InvalidValue: wrong value or out of range Other(b'3')
other FlashError(byte) Other(byte)

For execute, codes above '1' mean something different for each register. For example, tare (0102h) answers '4' for "timed out waiting for stability" and '3' for "tare above Max1".

Encoding and decoding

use xtrem::{Frame, DataAddress};

let req = Frame::read(0x00, 0x01, DataAddress::WeighingRegister);   // host 00 → device 01
let bytes = req.to_bytes(true);                                      // true = append CR+LF

let resp = Frame::decode(&datagram, /* verify_lrc */ true)?;
  • Constructors: Frame::read, Frame::write(.., data) and Frame::execute. Only a write carries data.
  • encode(&mut buf, crlf) reuses a buffer. to_bytes(crlf) allocates a new one.
  • decode parses the first complete frame in the bytes. Anything before STX or after ETX is ignored, as §4 requires, which also absorbs the trailing CR+LF.
  • verify_lrc: false is for modules that have LRC checking switched off (register 0011h). The LRC field is still parsed and length-checked; only the comparison is skipped.

Decode errors (ProtocolError)

Variant Cause
MissingStx / MissingEtx no 0x02, or no 0x03 after it
TooShort(n) fewer than the 13 mandatory characters between STX and ETX
NotHex(field, raw) an ID, address, length or LRC field isn't hex
UnknownFunction(b) F isn't one of RrWwEe
DataLengthMismatch(declared, actual) D_L doesn't match the bytes present
IllegalDataByte(b) a DATA byte below 0x20
LrcMismatch(computed, received) only with verify_lrc on
MalformedValue(addr, why) the frame decoded, but the payload doesn't have the shape the register promises (see below)
WriteRejected / ExecuteRejected defined for rejected commands. The scale driver reports these as XtremError::Write / Execute instead.

Registers

DataAddress names the registers the crate models. Any other address round-trips as DataAddress::Other(u16).

Address Access DataAddress Payload
0000h R SerialNumber decimal ASCII u32. Factory-set and unique, so it's the stable identity.
0001h RW DeviceId the module's network address, 2 hex chars
0007h / 0008h R HardwareVersion / SoftwareVersion text
0009h R SealingSwitchState '0' unlocked, '1' locked
0010h RW BaudRate not decoded (Raw)
0011h RW LrcCheck not decoded (Raw)
0012h RW AddCrLf not decoded (Raw)
0013h RW StreamOutputRate stream interval in ms, decimal ASCII
0100h R DeviceState 2 hex chars, see Device state. Also broadcast unprompted after boot.
0101h R GrossWeight 10-char weight field
0102h R, E TareValue 10-char weight field. Executing it tares to the current weight.
0103h R NetWeight 10-char weight field
0104h R StabilityIndicator '0' / '1'
0105h R, E ZeroIndicator '0' / '1'. Executing it zeroes the scale.
0106h R ZeroTrackingIndicator '0' / '1'
0107h R WeighingRegister gross + tare + status, 26 chars. This is what stream mode sends.
0110h / 0111h R AdcCounts / AdcCountsFiltered integer ADC counts (raw / after the digital filter)
1010h E StopStream
1011h / 1012h / 1013h E StartStreamWeight / StartStreamAdc / StartStreamAdcFiltered start streaming 0107h / 0110h / 0111h
1103h E ClearTare
9999h / EEEEh E DeviceReset / FactoryReset

The module's UDP ports are registers too: 0700h is the port the module sends to, and 0701h the port it listens on. The crate doesn't name them; they're what you put in XtremBusConfig (see XTREM bus and discovery).

Decoding a payload: RegisterValue::parse

RegisterValue::parse(address, &frame.data) picks the decoder from the address:

Registers RegisterValue variant
0000h, 0013h SerialNumber(u32), StreamOutputRate(u32)
0001h DeviceId(u8)
0007h, 0008h HardwareVersion(String), SoftwareVersion(String)
0009h Sealed(bool)
0100h DeviceState(DeviceState)
0101h–0103h Weight(Weight)
0104h–0106h Flag(bool)
0107h Weighing(WeighingRegister)
0110h, 0111h AdcCounts(i64)
anything else Raw(Vec<u8>). Unmodelled registers are returned as-is, not treated as errors.

Weight fields

A weight field is 10 characters: 8 right-aligned digits, then a 2-character unit.

"     0.0g "   →  Weight { mass: 0 g,  unit: WeightUnit::Gram }
Unit suffix WeightUnit
"g " or " g" Gram
"kg" Kilogram
"lb" Pound
"oz" Ounce

Weight.mass is a units::Mass, converted from whatever unit the module reported, and Weight.unit keeps the original unit for display. Never assume the unit: after a factory reset, a module can report grams even though its scale definition says kilograms. Any other suffix is a MalformedValue error.

Spec erratum: the byte tables in §16.1–16.3 give D_L = "08" for the weight registers, but the prose says 0Ah. 0Ah is right (8 digits + 2 unit characters), and it's what the crate uses.

Weighing register (0107h)

W <10-char gross> T <10-char tare> S <3 hex chars>
"W     0.0g T     0.0g S015"

WeighingRegister { gross, tare, status } has a .net() helper that returns gross − tare. That's the same figure 0103h reports, without the extra round trip. Reading 0107h once is cheaper than reading 0101h, 0102h and 0104h separately.

status is a WeighingStatus: a 12-bit word with one method per bit (§16.7):

Bit Method Meaning
0 zero() weight is within ±¼e of zero
1 tare() tare is on
2 stable() reading is stable
3 net() displayed value is net
4 fixed_tare() tare mode is fixed, not normal
5 high_resolution() high-resolution mode is on
6 initial_zero() initial zero after start-up is in progress
7 overload() weight > Max + 9e
8 negative_weight() weight < −19e
9 range_2() multi-range instrument is in range 2
10 preset_tare() a preset tare is active

The raw value is in status.raw.

Device state (0100h)

DeviceState splits the state byte (§8.6) into fields:

Bits Field Values
0–4 weighing_error: WeighingError None, E2promRead, AdcDead, AdcOutOfRange, AdcAbove30Mv, AdcBelowMinus30Mv, LoadCellSupply (supply out of range, shut down), Overload, NegativeWeight, Unknown(code)
5 power_alarm: bool Vcc out < 5.8 V or Vcc in > 8.5 V
6–7 wifi: WifiState NotPresent, Ready, Connected, ConnectionError

is_healthy() is true when there is no weighing error and no power alarm.

Not implemented

  • The §6 software-protection protocol. It uses separate 20-byte binary framing on UART0 and a 128-bit signature that can only be set at the factory. It is only active while the sealing switch is locked. Discovery reads 0009h, so a sealed module can at least be identified as sealed, rather than just failing to respond.
  • RS232 / RS485 transport.
  • Named registers for baud rate, LRC check and CR+LF exist, but their payloads come back as Raw.

Clone this wiki locally