Repository navigation
Tips for ergonomically handling the order of read registers #197
Description
Hi,
I'm trying to read some numbers and strings from a modbus server in Rust. Methods like read_holding_registers() give me a Vec<Indexed<u16>>. I did not find any documentation about what guarantees that I can get from this, so do I really have to handle arbitrary orders of results? How are others handling this?
For reading u32, I came up with something like this (for simplicity, error handling is replaced with explicit panics and unwraps):
async fn read_u32_handling_orders(channel: &mut Channel, addr: u16) -> u32 {
let count = 2;
let data = channel
.read_holding_registers(
RequestParam::new(UnitId::new(0), std::time::Duration::from_secs(10)),
AddressRange::try_from(addr, count).unwrap(),
)
.await
.unwrap();
if data.len() != usize::from(count) {
panic!();
}
let (lower, higher) = if (data[0].index, data[1].index) == (addr, addr + 1) {
(data[0].value, data[1].value)
} else if (data[0].index, data[1].index) == (addr + 1, addr) {
(data[1].value, data[0].value)
} else {
panic!();
};
let [a, b] = lower.to_be_bytes();
let [c, d] = higher.to_be_bytes();
u32::from_be_bytes([a, b, c, d])
}This only works since there are only two possible orders that read_holding_registers() could return its result. For u64, I am reading four words, which gives 24 possible orders. These are too many to type out by hand, so my next best idea would be to put them into a map for ordering:
async fn read_u32_using_map(channel: &mut Channel, addr: u16) -> u32 {
let count = 2;
let data = channel
.read_holding_registers(
RequestParam::new(UnitId::new(0), std::time::Duration::from_secs(10)),
AddressRange::try_from(addr, count).unwrap(),
)
.await
.unwrap();
// TODO: Do I have to handle the case where a ".index" appears multiple times?
let map = data
.into_iter()
.map(|entry| (entry.index, entry.value))
.collect::<std::collections::BTreeMap<_, _>>();
let [a, b] = map.get(&addr).unwrap().to_be_bytes();
let [c, d] = map.get(&(addr + 1)).unwrap().to_be_bytes();
u32::from_be_bytes([a, b, c, d])
}Looking at the current implementation of rodbus, it seems that the results are always returned in-order, so I could also do something like this, but this relies on undocumented behaviour:
async fn read_u32_assume_implementation_returns_certain_order(
channel: &mut Channel,
addr: u16,
) -> u32 {
let count = 2;
let data = channel
.read_holding_registers(
RequestParam::new(UnitId::new(0), std::time::Duration::from_secs(10)),
AddressRange::try_from(addr, count).unwrap(),
)
.await
.unwrap();
assert_eq!(
(addr..(addr + count)).collect::<Vec<_>>(),
data.iter().map(|e| e.index).collect::<Vec<_>>()
);
let data = data.into_iter().map(|e| e.value).collect::<Vec<_>>();
if data.len() != usize::from(count) {
panic!();
}
let [a, b] = data[0].to_be_bytes();
let [c, d] = data[1].to_be_bytes();
u32::from_be_bytes([a, b, c, d])
}- How are others handling this problem?
- Could you perhaps document that guarantees about the order of results where
Vec<Indexed<...>>is used (or point me at the existing documentation that I missed) - My favorite solution would be a function like
read_holding_registers_in_order_todo_good_name(&mut self, param: RequestParam, range: AddressRange) -> Result<Vec<u16>, RequestError>that is documented to return the read registers in-range and return an error if the modbus server "does something funny".
Thanks for any pointers!
Edit:
I came up with yet another approach using const generics that avoids some unnecessary overhead:
async fn read_holding_registers<const BYTES_COUNT: usize>(
channel: &mut Channel,
address: u16,
) -> [u8; BYTES_COUNT] {
if !BYTES_COUNT.is_multiple_of(2) {
panic!("Invalid bytes count {BYTES_COUNT}, must be even");
}
let count_u16 = u16::try_from(BYTES_COUNT / 2).unwrap();
let range = AddressRange::try_from(address, count_u16).unwrap();
let data = channel
.read_holding_registers(
RequestParam::new(UnitId::new(0), std::time::Duration::from_secs(10)),
range,
)
.await
.unwrap();
let mut result = [0; BYTES_COUNT];
for index in 0..count_u16 {
let expected_index = address + index;
match data.get(usize::from(index)) {
Some(indexed) if indexed.index == expected_index => {
let [a, b] = indexed.value.to_be_bytes();
result[2 * usize::from(index)] = a;
result[2 * usize::from(index) + 1] = b;
}
_ => panic!()
}
}
result
}
macro_rules! impl_read {
($type:ty, $size:literal, $func:ident) => {
pub async fn $func(channel: &mut Channel, address: u16) -> Result<$type> {
let registers = read_holding_registers::<$size>(address).await?;
Ok(<$type>::from_be_bytes(registers))
}
};
}
impl_read!(u16, 2, read_uint16_holding_register);
impl_read!(i16, 2, read_int16_holding_register);
impl_read!(i32, 4, read_int32_holding_register);
Activity
jadamcrain commentedon Oct 15, 2026
Hi @psychon, thanks for the detailed write-up.
Indexed<T>is only a convenience. Theindexfield doesn't come from the server. A Modbus read response contains no addresses, just a byte count followed by the values in address order. rodbus fills inindexasrange.start + iso you don't have to work out each value's address yourself.So there's no ordering to handle. The returned
Vecis always in ascending order starting atrange.start, with exactlyrange.countentries. If the server sends too few or too many bytes, the request fails with an error.That means you can just iterate over the values:
The same pattern works for
u64with a count of 4. If you only want the raw values, useregs.into_iter().map(|x| x.value). You're right that the docs should say this, and I'll add a note toIndexedand theread_*methods.Document ordering and count guarantees of read results (#197) (#206)
jadamcrain commentedon Oct 16, 2026
addressed this in #206