Apollo3zehn / FluentModbus

Lightweight and fast client and server implementation of the Modbus protocol (TCP/RTU).
MIT License
188 stars 69 forks source link

ModbusEndianness not working correctly #86

Open fabio622 opened 1 year ago

fabio622 commented 1 year ago

I try to connect with LittleEndian mode but reads result is in BigEndian mode. If i try to connect with BigEndian mode the reads result is in LittleEndian mode.

modbusClient.Connect(new IPEndPoint(IPAddress.Parse(_ip), _port, ModbusEndianness.LittleEndian);
modbusClient.ReadHoldingRegistersAsync<float>(_unitIdentifier, _address, countVariable).Result
Apollo3zehn commented 1 year ago

ModbusEndianness denotes the endianness of the server you connect to so that the library is able to convert the data to the endianness of your machine.

It looks like your server sends big endian data over the wire and thus you should use the BigEndian mode to get little endian data on your little endian machine.

Apollo3zehn commented 1 year ago

Maybe it should have been called ServerEndianness :-)

fabio622 commented 1 year ago

The problem is that I try to read the same tags from the same machine via Kepware and via PLC Siemens using LittleEndian. So, I'm sure that Server send LittleEndian data.

Apollo3zehn commented 1 year ago

There is not much going on in this library regarding endiannes: First, find out if there is an endianness mismatch: https://github.com/Apollo3zehn/FluentModbus/blob/40b6a56c98ad78e595964f585b82b060a44f4522/src/FluentModbus/Client/ModbusTcpClient.cs#L159-L160

if true, swap bytes: https://github.com/Apollo3zehn/FluentModbus/blob/40b6a56c98ad78e595964f585b82b060a44f4522/src/FluentModbus/Client/ModbusClient.cs#L127-L128

I guess your machine is little endian, so if you then specify ModbusEndianness.LittleEndian, the raw data are being left untouched.

The source of confusion must lie elsewhere ... Maybe because our terms are not precise enough. The Modbus protocol spec defines that the data should be send as BigEndian over the wire. It does not specify how the data are being stored on the server side.

Some servers are using little-endian for the wire format - against the specification - and that is why FluentModbus supports both ways.

Maybe Kepware and Siemens PLC always convert the big-endian wire data transparently to little-endian on your machine. And when you specify the endianness this does not refer to the wire format but how the data is actually stored in the server. I.e. if you specify big-endian, your data get converted twice, which is equal to no conversion - while FluentModbus would convert it once, since it actually talks about the wire format (so ModbusEndianness should not be called ServerEndianness as stated above but WireEndiannes).

I have no other explanation right now :-/

Mjollnirs commented 1 year ago

The current design is somewhat confusing to use. When I use it, I tend to read the byte directly, and then convert it to Span<short> for processing, which can avoid many such problems, as follows:

Span<byte> datasets = client.ReadHoldingRegisters(_unitIdentifier, _address, countVariable);
Span<short> _dataset = MemoryMarshal.Cast<byte, short>(datasets).ToArray();
float bigEndian = _dataset.GetBigEndian<float>(offset);
float littleEndian = _dataset.GetLittleEndian<float>(offset);
float midLittleEndian = _dataset.GetMidLittleEndian<float>(offset);
fabio622 commented 1 year ago

Thank you @Apollo3zehn for clarification!

Apollo3zehn commented 1 year ago

@fabio622 you are welcome!

@Mjollnirs thank your for sharing your workflow. I know the current API might be a bit confusing. It has been designed with maximum performance in mind. My use case is Modbus TCP with hundreds of float32 variables @25 Hz. For that the generic ReadXXX methods are ideal. Especially when the endiannes conversion is hardware accelerated using AVX/SSE2 (which is not yet the case but I did it for a different project already).

I will leave this issue open as a reminder to think about how the API could be changed to make its usage clearer.

pf-yibourk commented 3 months ago

i have a problem too with Endianness. my computer ist LittleEndian and i call Connect(IPEndPoint remoteEndpoint), also with default little endian too I must swape value with IPAddress.NetworkToHostOrder(value) before get bytes and sending to the modbus server

clee781 commented 2 months ago

@Apollo3zehn It looks like the low-level API methods do not check SwapBytes. Is this intentional? The modbus server I am using sends registers in BE. Since I don't have access to ModbusUtils.SwitchEndianness(), I need to swap the bytes on my own. Is there any reason for not applying the endianness inside the method?

Chuo1st commented 1 month ago

so how to solve this problem? the data that read from the server in littleendianness mode is wrong

Chuo1st commented 1 month ago

Thank you @Apollo3zehn for clarification!

how to solve this problem?

Apollo3zehn commented 1 month ago

@clee781 If you try to read data of type short or ushort from the server, then you could use the generic method ReadHoldingRegisters<ushort> instead of the low level one.

The endianness cannot be applied in the low level methods because the data may be e.g. of type long but without that knowledge it is impossible to say how to swap bytes (it all depends on the size of the actual data type).

The main confusion comes from the fact that Modbus defines 16-bit registers but actual data may be larger (e.g. 32 bit) and Modbus servers behave inconsistently in that case. Some do swap bytes at the 16-bit boundaries and some swap them at the 32 bit boundaries (or whatever the data type size is).

In case you know that your server swaps data always at the 16-bit boundary, you could do the following to read e.g. long data:

var registerData = client.ReadHoldingRegisters<ushort>(...);
var longData = MemoryMarshal.Cast<ushort, long>(registerData);

This way ModbusUtils.SwitchEndianness will be applied to the data if necessary by swapping pairs of bytes. The data will then be casted to the actual data type (long in this example).

If both of you, @clee781 and @Chuo1st, could answer the following, I may give you better advise

Thanks!

Chuo1st commented 1 month ago

@clee781 If you try to read data of type short or ushort from the server, then you could use the generic method ReadHoldingRegisters<ushort> instead of the low level one.

The endianness cannot be applied in the low level methods because the data may be e.g. of type long but without that knowledge it is impossible to say how to swap bytes (it all depends on the size of the actual data type).

The main confusion comes from the fact that Modbus defines 16-bit registers but actual data may be larger (e.g. 32 bit) and Modbus servers behave inconsistently in that case. Some do swap bytes at the 16-bit boundaries and some swap them at the 32 bit boundaries (or whatever the data type size is).

In case you know that your server swaps data always at the 16-bit boundary, you could do the following to read e.g. long data:

var registerData = client.ReadHoldingRegisters<ushort>(...);
var longData = MemoryMarshal.Cast<ushort, long>(registerData);

This way ModbusUtils.SwitchEndianness will be applied to the data if necessary by swapping pairs of bytes. The data will then be casted to the actual data type (long in this example).

If both of you, @clee781 and @Chuo1st, could answer the following, I may give you better advise

  • how many data do you want to read from the server (a single value or multiple values?)
  • data type of the data on the server (byte, short, int, long, ...)
  • endianness of the server and how the server behaves (are bytes swapped at register boundaries (16-bit) or at data type size boundaries)?

Thanks!

谢谢(thanks), it works correctly after adding the snippets!

clee781 commented 1 month ago

@clee781 If you try to read data of type short or ushort from the server, then you could use the generic method ReadHoldingRegisters<ushort> instead of the low level one.

The endianness cannot be applied in the low level methods because the data may be e.g. of type long but without that knowledge it is impossible to say how to swap bytes (it all depends on the size of the actual data type).

The main confusion comes from the fact that Modbus defines 16-bit registers but actual data may be larger (e.g. 32 bit) and Modbus servers behave inconsistently in that case. Some do swap bytes at the 16-bit boundaries and some swap them at the 32 bit boundaries (or whatever the data type size is).

In case you know that your server swaps data always at the 16-bit boundary, you could do the following to read e.g. long data:

var registerData = client.ReadHoldingRegisters<ushort>(...);
var longData = MemoryMarshal.Cast<ushort, long>(registerData);

This way ModbusUtils.SwitchEndianness will be applied to the data if necessary by swapping pairs of bytes. The data will then be casted to the actual data type (long in this example).

If both of you, @clee781 and @Chuo1st, could answer the following, I may give you better advise

  • how many data do you want to read from the server (a single value or multiple values?)
  • data type of the data on the server (byte, short, int, long, ...)
  • endianness of the server and how the server behaves (are bytes swapped at register boundaries (16-bit) or at data type size boundaries)?

Thanks!

Thank you for the suggestion. I'm using the read ushort workaround now.

I understand that Modbus servers can be inconsistent about endianness of the 16-bit registers in a 32-bit word, but the spec is very clear about using big-endian for the register itself regardless of what the register order of the word is. Can't we safely swap the endianness at the 16-bit boundary? As it is now, I don't believe the low level methods are applicable in any scenario since the register endianness will always need to be swapped regardless of the word order .Virtually all Modbus slaves will send 16-bit registers in big-endian format. I think the default should be to assume this. Is there a device I don't know about that doesn't send the registers as big-endian over the wire?

For my current application, I am reading in a couple dozen values of different datatypes. I prefer to do this in one transaction because it's faster.