bertmelis / esp32ModbusRTU

modbus RTU client for ESP32
MIT License
70 stars 44 forks source link

Type casting and data #26

Closed Hasenburg closed 4 years ago

Hasenburg commented 4 years ago

I am playing around with the library for some days and was able to make it communicate with a Siemens Sinamics V20 VFD.

So far so good, thanks to your great library, I was able to write and read registers correctly (unfortunately I am not able to start the motor, but that problem relates to the VFD).

When I tried to make the response of the VFD more readable, I got pretty confused.

Code example:

       modbus.onData([](uint8_t serverAddress, esp32Modbus::FunctionCode fc, uint8_t* data, size_t length) {
                Serial.printf("id 0x%02x fc 0x%02x len %u: 0x", serverAddress, fc, length);

                for (size_t i = 0; i < length; ++i) {
                     Serial.printf("%02x", data[i]);
                }

                std::reverse(data, data + 4);  // fix endianness

                Serial.printf("\nval_int: %02x", *reinterpret_cast<int*>(data));   //Float cast always showed 0.0
                Serial.print("\n\n");
       });

When I send a read request i.e. to register 0x02 of the VFD with 1 byte, I get the following response from the VFD:

Console output:

id 0x01 fc 0x03 len 2: 0x4000
val_int: 40008984

which is kind of fine.

But if I use write single register 0x06 i.e. register 0x63, value 0x107E

Console output:

id 0x01 fc 0x06 len 0: 0x
val_int: 63107e35

The for loop does not print any data, but the reinterpret_cast<int> does show some data. I don't understand, why that happens?

Furthermore the data shown in the int* cast shows 63(register), 107e(value) and 35(1st crc byte). The second crc byte is cut out for some reason.

Do you understand why all this is happening? I guess the differences has something to do with adding 4 bytes in the reverse function, but I am not sure what to do in order to print out the full received data in order to process it the way I want to

bertmelis commented 4 years ago

I'll take a look on how to improve.

Miq1 commented 4 years ago

@Hasenburg: you are taking the data pointer, which is pointing to a single byte, and force it with the reinterpret_cast to point to an int - 4 bytes in a row. So no wonder you get whatever is behind the data. The response to a write request is an echo of the request, so the data you got looks like it.

I am picking up this issue because it shows a significant weakness of the "raw request" feature I am working on, as that will depend on a response length byte not all responses will hold - as in your example. Thank you for the hint!

Hasenburg commented 4 years ago

@Miq1: What do you mean that I cast it 4 bytes in a row?

Do you have a hint how to convert that data pointer into an (for me) usable data type (int, char, string)?

It seems that the data only shows its value within

for (uint8_t i = 0; i < length; ++i) {
        Serial.printf("%02x", data[i]);
    }

without any additional bytes. I tried to convert it in many different ways, but it always results in some totally different values.

Reversing the data and reinterpret_cast it to int, was the closest I got to show the correct values...

Miq1 commented 4 years ago

@Miq1: What do you mean that I cast it 4 bytes in a row?

You have the data pointer, that has a type of uint8_t *, it will point you to a single uint8_t byte. Your reinterpret_cast<int*> forces the compiler to take the data pointer as if it were a pointer to int, which is 4 bytes long. Hence the printf takes the 4 bytes for an int starting from the byte data is pointing at. So whatever is behind the data data was supposed to point at, is taken as a part of an int value. So you will get data printed even if dataLen was shorter than 4.

Do you have a hint how to convert that data pointer into an (for me) usable data type (int, char, string)?

As I wrote, you are using FC 0x06 here. The Modbus standard mandates the response to a writing access (as FC 0x06 is) to be an echo of the request. The correct full response for your request then would have been 01 06 00 63 17 0E CC CC (CC CC for the two CRC bytes), but it did not, because you missed to give the register address as uint16_t = 0063, but used a single byte 63.

So no wonder the response was 01 06 63 17 0E CC CC - the echo of your malformed request. The device you are using seems to be pretty ignorant to format errors, by the way - another would have responded with an error response like ILLEGAL_ADDRESS or ILLEGAL_DATA_VALUE.

It seems that the data only shows its value within

for (uint8_t i = 0; i < length; ++i) {
        Serial.printf("%02x", data[i]);
    }

without any additional bytes. I tried to convert it in many different ways, but it always results in some totally different values.

Reversing the data and reinterpret_cast it to int, was the closest I got to show the correct values...

You better should stick to the for loop to print out the data byte-wise in hexadecimal. Since Modbus messages are MSB-first by definition, you will need to write a conversion function yourself, that takes care of the different lengths of the data fields in a well-formed Modbus message. (Side note: beware the inverse order of the CRC bytes 😁 )

Hasenburg commented 4 years ago

@Miq1: Thanks for your help!

Sorry, there was some confusion in my communication as I did not accurately re-read my first message in Jun. The big problem wasn't FC0x06 as you explained twice. I struggled storing the 2 byte data of FC0x03 data into one variable that I can work with. But your answer gave me a much better understanding of how the response works.

I wrote a little function that, seems (at least to me) to be quite universal. It converts the answer into an uint16_t array (adapting according to length of data), which works like a charm.

Function

std::vector<uint16_t> readValues(size_t length, uint8_t* data) {
    std::vector<uint16_t> value;
    for (size_t i = 0; i < length; i+=2) {
            value.push_back(((uint16_t)data[i] << 8) | data[i+1]);
    }
    return value;
}

handleData Function Pointer

void handleData(uint8_t serverAddress, uint8_t fc, uint8_t* data, size_t length) {
    Serial.printf("id 0x%02x fc 0x%02x len %u: 0x", serverAddress, fc, length);
    for (size_t i = 0; i < length; ++i) {
        Serial.printf("%02x", data[i]);
    }
    Serial.print("\n");
    std::vector<uint16_t> myValue = readValues(length, data);
    for (int i = 0; i < myValue.size(); i++) {
      Serial.printf("Value #%d: ", i);
      Serial.printf("Hex %02x, ", myValue[i]);
      Serial.printf("Dec: %d\n\n", (int)myValue[i]);
    }
}

I did not face any problems with inverse order of CRC bytes. Do you see any problems with that code?

Miq1 commented 4 years ago

Good that you found something usable in my comments 😀

I see one tiny issue in the function returning the vector<uint16_t>: if you will encounter a data message with an odd (not even) length value, you will grab a byte behind the data proper, potentially causing a SEGFAULT error.

As a side note, the FC 0x03 will return a length byte in the response, immediately before the uint16_t values are beginning. Bert's library does filter that byte for FCs 01, 02, 03 and 04, but if you are going to use another library, you may be surprised 😉