I’ve written some code that intercepts data packets and modifies them. 4 bytes in the data packets represent a float value, but in reverse byte order (little-endian). And that creates a lot of issues for me. Let me explain with a simplified piece of code:
#include <Arduino.h>
typedef struct {
uint8_t start_seq[2];
uint8_t body_size;
uint8_t type;
uint8_t unknown;
uint8_t entity_id;
uint8_t reserved[3];
float value;
uint8_t end_seq[2];
} Packet;
void printRawPacket(const char* message, const uint8_t* packet, size_t length) {
Serial.print(message);
for (int i = 0; i < length; i++) {
Serial.printf("%02X ", packet[i]);
}
Serial.println();
}
// Packet interceptor function
void handlePacket(uint8_t* raw_packet, size_t length) {
// Create a pointer to the packet structure
Packet* packet = (Packet*)raw_packet;
printRawPacket("Original bytes: ", raw_packet, sizeof(raw_packet));
// Correctly Prints: "Original bytes: BE EF 08 03 00 09 00 00 00 00 00 C0 40 EF BE"
// Access the float value without creating a copy of the 4 bytes
Serial.print("Original value: ");
Serial.println(packet->value);
// Incorrectly prints a random value or "ovf" instead of 6.00
// Modify the float value without creating a copy of the 4 bytes
packet->value = -120.0f; // little endian would be 00 00 F0 C2; big endian would be C2 F0 00 00
Serial.print("Modified value: ");
Serial.println(packet->value); // Prints -120.0 correctly
printRawPacket("Modified bytes: ", raw_packet, sizeof(raw_packet));
// Incorrectly Prints: "Modified bytes: BE EF 08 03 00 09 00 00 00 00 00 C0 00 00 F0"
// Showing the float value was not stored correctly and the footer was overwritten
// Expected output would have been: "Modified bytes: BE EF 08 03 00 09 00 00 00 00 00 00 00 F0 C2 EF BE"
}
void setup() {
Serial.begin(115200);
// Example packet
uint8_t raw_packet[] = {
// ------ HEADER ------
0xBE, 0xEF, // start sequence
0x08, // body size uint8_t
0x03, // type uint8_t
0x00, // unknown
// ------ HEADER ------
// ------ BODY ------
0x09, // entity id uint8_t
0x00, 0x00, 0x00, // reserved
0x00, 0x00, 0xC0, 0x40, // value (float, little-endian) // 6.0f
// ------ BODY ------
// ------ FOOTER ------
0xEF, 0xBE // end sequence
// ------ FOOTER ------
};
handlePacket(raw_packet, sizeof(raw_packet));
}
void loop() {
// Nothing to do here
}
As explained in the comments, this doesn’t work on multiple levels.
I can easily work around that issue by making the value field a uint8_t value[4]
and then using a getter and setter function like this:
typedef struct {
uint8_t start_seq[2];
uint8_t body_size;
uint8_t type;
uint8_t unknown;
uint8_t entity_id;
uint8_t reserved[3];
uint8_t value[4];
uint8_t end_seq[2];
// Function to get the float value from the raw packet in little-endian format
float getFloatLeValue() const {
// reverse byte order and return as float
uint32_t temp = (value[3] << 24) | (value[2] << 16) | (value[1] << 8) | value[0];
float result;
memcpy(&result, &temp, sizeof(result)); // Copy uint32_t to float
return result;
}
// Function to set the float value into the raw packet in little-endian format
void setFloatLeValue(float newValue) {
// Copy float to uint32_t
uint32_t temp;
memcpy(&temp, &newValue, sizeof(temp));
// Copy uint32_t to value in little-endian format
value[0] = temp & 0xFF;
value[1] = (temp >> 8) & 0xFF;
value[2] = (temp >> 16) & 0xFF;
value[3] = (temp >> 24) & 0xFF;
}
} Packet;
But this just seems so unnecessarily complex and complicated when all I wanna do is interpret 4 bytes in reverse.
Is there a simpler more elegant way?
(I’m writing my code in C++ on PlatformIO for an ESP32 using the Arduino Framework.)
7
Let’s see how this would look with bit_cast
suggested by @PasserBy. We can take an example implementation using memcpy from https://en.cppreference.com/w/cpp/numeric/bit_cast
Also let’s use htonl
/ntohl
for the actual swap. Alternatively there is bswap_32
.
template<class To, class From>
To my_bit_cast(const From& src) noexcept
{
To dst;
memcpy(&dst, &src, sizeof(To));
return dst;
}
float getFloatLeValue() const {
return my_bit_cast<float, uint32_t>(
ntohl(
my_bit_cast<uint32_t, uint8_t[4]>(value)));
}
void setFloatLeValue(float newValue) {
uint32_t temp = htonl(my_bit_cast<uint32_t, float>(newValue));
memcpy(&value, &temp, sizeof(value));
}
It’s uh… Not that great?
About the float_le
wrapper, the suggestion is:
class float_le {
uint8_t value[4];
public:
float_le(float v) {
// same as setFloatLeValue
}
operator float() const {
// same as getFloatLeValue
}
}
This is trivially copyable (e.g by memcpy) and is assignable/castable to/from ordinary float. Use inside the Packet struct as float_le value;
. Could be useful if you need the flipped float in multiple places.
May have nasty alignment/padding issues. Let’s see what comments point out.