-
Notifications
You must be signed in to change notification settings - Fork 2
XTREM Protocol
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.
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.
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".
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)andFrame::execute. Only a write carries data. -
encode(&mut buf, crlf)reuses a buffer.to_bytes(crlf)allocates a new one. -
decodeparses the first complete frame in the bytes. Anything beforeSTXor afterETXis ignored, as §4 requires, which also absorbs the trailing CR+LF. -
verify_lrc: falseis for modules that have LRC checking switched off (register0011h). The LRC field is still parsed and length-checked; only the comparison is skipped.
| 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. |
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).
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. |
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.
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.
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.
-
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.
EtherCAT HAL
EtherCAT Devices
XTREM
Other crates