Skip to content

XTREM Scale

kraemr edited this page Sep 29, 2026 · 3 revisions

XtremScale is the driver for one XTREM weighing module. It implements XtremDevice, a polling contract meant for a synchronous control loop.

The polling contract: XtremDevice

pub trait XtremDevice {
    fn send_next_request(&mut self) -> Result<(), anyhow::Error>;
    fn handle_response(&mut self) -> Result<(), anyhow::Error>;
    fn as_any(&self) -> &dyn std::any::Any;
    fn as_any_mut(&mut self) -> &mut dyn std::any::Any;
}

Call both methods once per tick. Neither one blocks.

  • send_next_request sends at most one request. It does nothing while a request is still waiting for an answer; requests aren't pipelined.
  • handle_response drains every frame that has arrived for this device and applies it to the driver's state. It only returns Err if the bus has shut down.

Problems talking to the module, like timeouts, refusals and bad payloads, don't come back as Err. They're stored in last_error, so one bad reading doesn't stop the machine that owns the scale.

Creating a scale

use xtrem::{ScaleMode, XtremScale};

let scale = XtremScale::from_probe(&bus, &probe, ScaleMode::Poll);   // after discovery (preferred)
let scale = XtremScale::new(&bus, 0x01, ScaleMode::Poll);             // by device ID only
  • from_probe takes the unicast address, serial number and device state from discovery. Requests then go straight to the module's IP.
  • new only knows the device ID, so requests go to the broadcast address until you call set_addr(addr).
  • One scale per device ID per bus. Creating a second scale with the same ID takes over the first one's route (see routing rules). Dropping either scale removes the route for that ID. If discover reports id_collision, assign unique IDs first.

Modes

ScaleMode What the driver does Trade-off
Poll (default) reads 0107h whenever no request is outstanding a steady request/response rhythm, and timeouts show whether the link is alive
Stream { interval_ms } writes interval_ms to 0013h, executes 1011h, then just listens less traffic, but a silent module looks the same as one with no new data

Stream mode details

  • Start-up takes two requests: the interval write and then the start command, one per tick, each waiting for its response.
  • Stale detection: a module that reboots stops streaming without any notice. If nothing has arrived for max(10 × interval_ms, 1 s), the driver sends the start sequence again. The window is measured from the later of the last received frame and the last start attempt, so a restart that also gets no answer still waits a full window before the next try.
  • set_mode(mode) switches modes. Leaving stream mode queues a stop command (1010h).
  • Drop sends a stop command if the scale is in stream mode, so the module doesn't keep flooding the subnet. If the socket buffer is full, the stop is sent from the async runtime instead. Drop always unsubscribes the device ID.

Reading values

use units::mass::{gram, kilogram};

if let Some(r) = scale.reading {
    println!("net {:.1} g, gross {:.3} kg, stable={}",
        r.net.get::<gram>(), r.gross.mass.get::<kilogram>(), r.status.stable());
}
Field Type Updated by
reading Option<Reading> every 0107h response (a poll or a streamed frame)
device_state Option<DeviceState> request_device_state(), or the value discovery found if you used from_probe
last_error Option<XtremError> anything that went wrong. Take it with take_error(), which also clears it.

Reading is { gross: Weight, tare: Weight, net: Mass, status: WeighingStatus, at: Instant }. net is gross − tare, and at is when the frame arrived. See XTREM protocol for the WeighingStatus bits and Weight fields for units.

reading keeps its last value when later requests fail, so check reading.at if you need to know how recent it is.

Getters: device_id(), serial(), addr(), mode(). set_request_timeout(d) changes the response timeout, which defaults to DEFAULT_REQUEST_TIMEOUT (1 s, the longest the spec allows between STX and ETX of a message).

Commands

Commands go into a queue and are sent from send_next_request, one per tick. They take priority over polling.

Method Sends Notes
tare() execute 0102h tares to the current weight. The module waits for a stable reading, and answers '4' if it doesn't get one.
clear_tare() execute 1103h
zero() execute 0105h
request_device_state() read 0100h the result goes into device_state

A refused command shows up as XtremError::Execute(address, result) in last_error. A refused write (for example the stream interval) shows up as XtremError::Write.

Errors (XtremError)

Variant Meaning
Timeout(address) no answer to the request for address within the request timeout. It is recorded on the next send_next_request after the timeout expires, and that call then sends the next request.
Protocol(ProtocolError) a frame arrived, but its payload didn't decode (MalformedValue, …)
Write(address, WriteResult) the module refused a write, for example SealProtected or InvalidValue
Execute(address, ExecuteResult) the module refused an execute
Send(String) the socket refused the send, for example a full buffer. It's retried on the next tick.

Any response with the expected function and address clears the in-flight request, even one with a bad payload. That way one malformed answer doesn't stall the driver until the timeout.

Frames on the device's route that are requests (from another host on the network) are ignored.

Several scales on one bus

One XtremBus serves any number of scales, as long as every module has a unique device ID:

let mut scales: Vec<XtremScale> = probes.iter()
    .map(|p| XtremScale::from_probe(&bus, p, ScaleMode::Poll))
    .collect();

loop {
    for s in &mut scales {
        s.send_next_request()?;
        s.handle_response()?;
    }
    std::thread::sleep(Duration::from_millis(10));
}

examples/poll_multi.rs does exactly this and prints each scale's reading.

Clone this wiki locally