Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Floating point support #131

Open
cblackd7r opened this issue Oct 28, 2022 · 7 comments
Open

Floating point support #131

cblackd7r opened this issue Oct 28, 2022 · 7 comments

Comments

@cblackd7r
Copy link

Hi, is it possible to pass a f32 float with quantity 2 via two u16 registers and only get 2 buffer values? When I pass 2 u16's into the input register vector, it returns 4 buffer values in my modbus client.

@uklotzde
Copy link
Member

uklotzde commented Mar 4, 2023

If anyone thinks that this belongs to the API please make a proposal.

Otherwise encoding/decoding of non-standard types could always be implemented in the application layer. I do not consider that as mandatory or even desirable feature for the library.

@cdbennett
Copy link
Contributor

cdbennett commented Mar 13, 2023

@cblackd7r - being able to encode and decode various data types as data stored in Modbus registers is a very important aspect of a Modbus server or client application. However, it is complex and there are many ways to could be done.

Suppose you have some float values (f32) to send via Modbus. One decision is whether the data is ultimately stored as two u16 values in memory, and then convert to/from f32 on demand; or else you could store the data as f32 in memory and then convert to u16 when building a Modbus request or response. Another decision is what endianness to use in the Modbus register encoding for 32-bit or 64-bit values.

As @uklotzde noted, Modbus register (u16 values only) conversion to other data types is more appropriately done in a distinct layer. It is orthogonal the core Modbus API like tokio-modbus provides.

Probably the most helpful path would be to start with some example client and server programs that demonstrate how one might implement data format encoding/decoding. This would provide several benefits:

  • Give users of the library a concrete example of how to do this common task (sending f32 or u64 values, for instance)
  • Enumerate the possible approaches to data storage / encoding strategies
  • Help illuminate patterns that might be useful to extract into a reusable library to simplify implementation of client/server apps

@lindblandro
Copy link

While I was playing around with tokio-modbus I noticed that there isn't an interface to get the raw response data. Everything seemed to be Vec<Word> (kudos for making Word a distinct type!). So if you read e.g. 100 registers with varying data types, it becomes tedious real fast to add manual conversions everywhere. I think it would be nice to have at least an API function that provides the raw u8 frame values directly. This also would allow implementing missing Modbus function codes in user code.

Generally speaking adding a "read_f32()" feature into this crate will probably create a mess since multi-register values in Modbus are a constant annoyance, and a better alternative IMO would be to inject the decoding logic somehow (preferably derived from a struct definition) when reading the response.

@cdbennett
Copy link
Contributor

It is tricky or at least error-prone to say "give the raw u8 values" for Modbus register data, because then you have a byte-ordering issue to clarify. So, when interpreting something as a f32 value for instance, it's best if you use the u16 values directly to form it, then you eliminate the u8 <-> u16 byte ordering question

E.g. here is one way to do it:

pub fn get_f32(registers: &[u16]) -> f32 {
    assert!(
        registers.len() == 2,
        "wrong size for get_f32, {}",
        registers.len()
    );

    let bits = ((registers[0] as u32) << 16) | (registers[1] as u32);
    f32::from_bits(bits)
}

pub fn get_u32(registers: &[u16]) -> u32 {
    assert!(
        registers.len() == 2,
        "wrong size for get_u32, {}",
        registers.len()
    );

    ((registers[0] as u32) << 16) | (registers[1] as u32)
}

The area of interpreting Modbus register values, would at least belong to its own crate for sure, because there are so many ways you might want to do it.

@cdbennett
Copy link
Contributor

The basic issue is that Modbus register values are 16-bit. Always. However, applications may interpret these in different ways, such as two registers being taken as a 32-bit value. But that is not precisely speaking, a "32-bit Modbus register". Since the Modbus standard defines only 16-bit registers. It's much clearer if we use different terminology for those 32-bit values, i.e. "variable" or "value" or "entity", but not "register". Even though some product documentation will use that imprecise term.

It is important that tokio-modbus hides the details of how the u8 values are ordered within a u16 register value. Applications should not know about the ordering of bytes. In fact, I've written Modbus libraries from scratch more than once, and I still can't remember what order the bytes are, because once you implement that in the library, you can forget it :)

@lindblandro
Copy link

Yes I know the nuances and problems of Modbus when used to relay values that don't fit in a register. Libmodbus has API functions called modbus_get_float_(abcd|badc|cdab|dcba) which indicates just how ridiculous the situation is when only register byte ordering is well defined. 

What I ended up doing with tokio-modbus was to read all registers, implement TryFrom for my sensor abstraction that converts the register values to Cursor<Vec<u8>> and used the ReadDataExt trait from byteorder crate for it's various read_<type> functions and just went through all the data and read the data in correct format. It wasn't very complicated to do, but it's not trivial either.

I know how my sensor represents floats and such and it was possible to do the transformation from Vec<Word> to MySensor. The transformation cannot be done in a general way for all possible sensors, which is just an unfortunate fact of life with Modbus. What can be done is to allow easier access to the underlying response data so that users can implement their own parsers. This isn't at odds with the existing API, but rather another view of the issue that works well when you scan your device for data continuously or create vendor specific function codes, which are allowed by the spec. There is already Request::Custom and Response::Custom (which looked pretty nice by the way) that could be used to implement e.g. Read Device Identification. Both provide byte access to the underlying data.

It is important that tokio-modbus hides the details of how the u8 values are ordered within a u16 register value. Applications should not know about the ordering of bytes.

I almost agree. I mean the spec does specify register byte ordering after all. However, I feel that the ordering should be invisible unless you choose to look at it. Having 16bit registers probably was all the rage back in the 80s and you couldn't possibly need anything with more precision, but just reflecting that example from libmodbus should make it obvious that the world (but not the industry, oh no!) has moved on and floats or even doubles are a common use case nowadays, there needs to be convenient ways of accessing floating point registers.

I think ideally the floating point problem would happen behind the scenes and in an utopia even the Modbus transfer mechanism would be just a data transfer mechanism and you'd have something like

#[modbus_device(floats = "float-abcd")]
struct MySensor {
    #[modbus(address=0x00)]
    temperature: f32,
    #[modbus(address=0x200)]
    pressure: f32,
}
...

let mut s = MySensor::new(serial_device);
s.update()

or something like that.

@cdbennett
Copy link
Contributor

I think we can all agree there should be a way to make it easier to interact with non-u16 register values.

But, instead of putting in the core tokio-modbus crate, it seems like this might be better developed (at least at first) in a separate crate to do the kind of encoding/decoding you suggest. Just because there are so many possible ways to do it, and it's an area of many API options and preferences and trade-offs.

I've been trying to figure out a good way to define Modbus registers and data types myself recently, and implement a clean way to encode/decode and abstract away the gory details too.

I look forward to seeing, or even contributing to, improvements in this area.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

5 participants