Skip to content

Tips for ergonomically handling the order of read registers #197

Description

@psychon

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);

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions