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);
Hi,
I'm trying to read some numbers and strings from a modbus server in Rust. Methods like
read_holding_registers()give me aVec<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):This only works since there are only two possible orders that
read_holding_registers()could return its result. Foru64, 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: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:
Vec<Indexed<...>>is used (or point me at the existing documentation that I missed)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: