How to elegantly inherit from struct overriding/adding fields

So as discussed in the previous topic I created, I'm currently writing code to intercept data packets and thanks to the help of the community I got this working in a pretty elegant way. (See Code block at the very bottom of this post.)

But now I'm looking at a new problem that seems really hard to solve in a clean way. I'm not sure how to design multiple packet structs without unnecessary data redundancy and messing up the original byte structure:

struct __attribute__((packed)) GenericPacket {
    uint8_t start_seq[2]; // always {0xBE, 0xEF}
    uint8_t body_size;
    uint8_t packet_type;
    uint8_t header_byte;
    uint8_t body[body_size];
    uint8_t end_seq[2]; // always {0xEF, 0xBE}
};

struct __attribute__((packed)) GenericEntityPacket : public GenericPacket {
    // Replace uint8_t body[body_size]; with:
        uint8_t entity_id;
        uint8_t reserved[3];
        uint8_t remaining_body[body_size-4];
};

// Example: BE EF 08 03 00 58 00 00 00 00 00 C0 40 EF BE
struct __attribute__((packed)) FloatEntityPacket : public GenericEntityPacket {
    // Replace `uint8_t remaining_body[body_size-4];` with
        float value;
};

// Example: BE EF 05 03 00 59 00 00 01 EF BE
struct __attribute__((packed)) BoolEntityPacket : public GenericEntityPacket {
    // Replace `uint8_t value[body_size-4];` with
        uint8 unused[3];
        bool value;
};

// Example: BE EF 05 03 00 5C 00 00 00 04 EF BE
struct __attribute__((packed)) Uint8tEntityPacket : public GenericEntityPacket {
    // Replace `uint8_t value[body_size-4];` with
        uint8 unused[3];
        uint8_t value;
};

// Example: BE EF 01 18 01 02 EF BE
struct __attribute__((packed)) ToggleTunerPacket : public GenericPacket {
    // Replace `uint8_t header_byte;` with:    
        uint8_t state;
    // Replace `uint8_t body[body_size];` with
        uint8_t channel;
};

// Example: BE EF 00 1E 02 EF BE
struct __attribute__((packed)) SdRecordPacket : public GenericPacket {
    // Replace `uint8_t header_byte;` with:    
        uint8_t state;
    // Remove `uint8_t body[body_size];`
};

// Example: BE EF 01 1A 01 04 EF BE
struct __attribute__((packed)) SnapshotPacket : public GenericPacket {
    // Replace `uint8_t header_byte;` with:    
        uint8_t action;
    // Replace `uint8_t body[body_size];` with
        uint8_t slot;
};

Do you have some advice for me on how one would design something like this elegantly in C++ these days? The most obvious issue with my approach is that I mess up the order whrn addibg new fields (i.e. end_seq always needing to be the last field.

Example of I use such structs:

#include <Arduino.h>

struct __attribute__((packed)) Packet {
  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];
};

void printRawPacket(const char* message, Packet* packet) {
  Serial.print(message);
  uint8_t *rawBytes = reinterpret_cast<uint8_t *>(packet);
  for (int i = 0; i < sizeof(Packet); i++) {
    Serial.printf("%02X ", rawBytes[i]);
  }
  Serial.println();
}

// Packet interceptor function
void interceptPacket(Packet* packet) {
  Serial.print("Original value: ");
  Serial.println(packet->value);

  // Modify the float value without creating a copy of the 4 bytes
  packet->value = -120.0f;
  Serial.print("Modified value: ");
  Serial.println(packet->value);
}

// Packet handler wrapper that converts the raw bytes to a Packet struct
void handlePacket(uint8_t* raw_packet) {
  Packet tempPacket;
  memcpy(&tempPacket, raw_packet, sizeof(Packet));
  interceptPacket(&tempPacket);
  // update the raw_packet with the modified value
  memcpy(raw_packet, &tempPacket, sizeof(Packet));
}

void setup() {
  Serial.begin(115200);
  delay(2000);

  // 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 ------
  };


  Serial.println("Original raw_packet: ");
  for (int i = 0; i < sizeof(raw_packet); i++) {
    Serial.printf("%02X ", raw_packet[i]);
  }

  handlePacket(raw_packet);

  Serial.println("Modified raw_packet: ");
  for (int i = 0; i < sizeof(raw_packet); i++) {
    Serial.printf("%02X ", raw_packet[i]);
  }
}

void loop() {
}

pretty common..
separate the header from the packet..
each packet has and starts with a header..
example packets..

good luck.. ~q

Please post the spec for the format of the packets you're working with.

There is no spec. I've been reverse engineering it myself. I found the protocol to build upon UART @ 115200 baud. Everything else appears to be proprietary.
But I'm open to suggestions on what tools/format I could use to document the spec.

Here is a base template struct

template <uint8_t type, typename T>
struct Packet {
    uint8_t start_seq[2] {0xBE, 0xEF};
    uint8_t body_size {sizeof(T) - 1};
    uint8_t packet_type {type};
    T p;
    uint8_t end_seq[2] {0xEF, 0xBE};

    void importP(const uint8_t *raw_packet) {
      memcpy(&p, raw_packet + 4, sizeof(T));
    }
    void exportP(uint8_t *raw_packet) {
      memcpy(raw_packet + 4, &p, sizeof(T));
    }
    virtual void intercept() = 0;
    void handle(uint8_t *raw_packet) {
      // printRawPacket("before", this);
      importP(raw_packet);
      // printRawPacket("import", this);
      intercept();
      // printRawPacket("export", this);
      exportP(raw_packet);
    }
} __attribute__((packed));

First, put the __attribute__((packed)) at the end; it reads better. The struct fields have the start and end, and a hole in the middle for your stuff. The initializers will make a valid packet if the struct is instantiated. The name of your stuff is p, so you will have to use .p.value instead of .value -- sorry.

The methods will read and write from the raw packet (exportP because export is a keyword). Because you have the all the other stuff already, it just copies the T. The offset into the raw is hard-coded with the magic number 4; you could tidy that up. Note that you might try to use offsetof(Packet, p), but that has potential complications. There's a placeholder for an intercept method, called by handle.

Starting with the smallest/simplest variant

struct StateOnlyNoBody {
    uint8_t state;
} __attribute__((packed));

// Example: BE EF 00 1E 02 EF BE
typedef Packet<0x1E, StateOnlyNoBody> SdRecordPacket;

It defines the T, and creates a Packet with the packet_type; then creates a better name for it. Note that "body" fields have no initializer, so you will get random values if you create one of them on the stack. You can fix this with appropriate values.

Two others

// Example: BE EF 01 18 01 02 EF BE
struct ToggleTunerBody {
    uint8_t state;
    uint8_t channel;
} __attribute__((packed));

typedef Packet<0x18, ToggleTunerBody> ToggleTunerPacket;

// Example: BE EF 01 1A 01 04 EF BE
struct SnapshotBody {
    uint8_t action;
    uint8_t slot;
} __attribute__((packed));

typedef Packet<0x1A, SnapshotBody> SnapshotPacket;

Now getting to the interesting part

template <uint8_t id, typename T>
struct EntityBody {
    uint8_t header_byte {0};
    uint8_t entity_id {id};
    uint8_t reserved[3] {0};
    T value;
} __attribute__((packed));

enum packet_type : uint8_t {
  p_entity = 0x03,
  e_float = 0x58,
  e_bool = 0x59,
  e_uint8 = 0x5c,
};

The entities use a nested template. There's an enum for all the magic type IDs, which you could fill in and use for those previous variants. And then move the enum before everything. You'll need those IDs later.

Now the single-byte entities are more of the same, with the nested template

// Example: BE EF 05 03 00 59 00 00 00 01 EF BE
typedef Packet<p_entity, EntityBody<e_bool, bool>> BoolEntityPacket;

// Example: BE EF 05 03 00 5C 00 00 00 04 EF BE
typedef Packet<p_entity, EntityBody<e_uint8, uint8_t>> Uint8EntityPacket;

// Example: BE EF 08 03 00 58 00 00 00 00 00 C0 40 EF BE
struct QuarterEntityPacket : Packet<p_entity, EntityBody<e_float, float>> {
  void intercept() override {
    p.value *= 0.25;
  }
};

And for the last one, instead of a typedef, turn the declaration around and create a subclass. This allows adding code, and handling the same kinds of packets in different ways.

In fact, because of the pure virtual method in the base class, you can't actually create an instance of any of the others. So depending on how you use these, you want either all subclass or all typedef.

Anyway, this one will divide the value by four. Now here's the slightly tedious part

void handlePacket(uint8_t *raw_packet) {
  switch (raw_packet[3]) {
    case p_entity:
      switch (raw_packet[5]) {
        case e_float:
          {
            QuarterEntityPacket floatP;
            floatP.handle(raw_packet);
            break;
          }
      }
      break;
    default:
      break;
  }
}

These nested switches will examine the magic ID fields, using those enums. You may also verify anything else. Because each case creates a new instance, it is done in its own block.

Finally, the setup and loop, with the correct entity ID

void setup() {
  Serial.begin(115200);
  // delay(2000);
  
  // Example packet
  uint8_t raw_packet[] = {
    // ------ HEADER ------
    0xBE, 0xEF, // start sequence
    0x08,       // body size uint8_t
    0x03,       // type uint8_t
    0x00,       // unknown
    // ------ HEADER ------

    // ------ BODY ------
    0x58,                   // 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 ------
  };


  Serial.println("Original raw_packet: ");
  for (int i = 0; i < sizeof(raw_packet); i++) {
    Serial.printf("%02X ", raw_packet[i]);
  }
  Serial.println();

  handlePacket(raw_packet);

  Serial.println("Modified raw_packet: ");
  for (int i = 0; i < sizeof(raw_packet); i++) {
    Serial.printf("%02X ", raw_packet[i]);
  }
  Serial.println();
}

void loop() {
  delay(10);
}

If you want to print the Packet, you can put this function at the top. It is also a template

template <typename T>
void printRawPacket(const char* message, T *packet) {
  Serial.print(message);
  Serial.print(": ");
  auto rawBytes = reinterpret_cast<uint8_t *>(packet);
  for (int i = 4 /* skip vtable */; i < sizeof(T); i++) {
    Serial.printf("%02X ", rawBytes[i]);
  }
  Serial.println();
}

Note that the first byte is at offset 4; before that is what I'm guessing is a pointer to the vtable, or some other implementation detail, which may vary by compiler.

1 Like

What is the device? Are there also reply packets or is communication one way?

I'd look at it like this. The data is coming from a foreign system over which you have no control. It could even (possibly) use data types which have no direct analogue in C++ so somewhere on the boundary between the 2 systems you will anyway be dealing with data representations which falls foul of C++ rules. If you look at, for example, a .WAV (sound) file format Microsoft WAVE soundfile format , which of course people process also with C++, there is a mixture of big endian and little endian, 2 byte an 4 byte integers and more. So you have to make the best of what is there.

If you think that you want a record structure to provide a near 1:1 representation of the incoming record types, then you have to assemble this out of structs which consist of primitive C++ data types. These structs will be filled from the input stream after such activities as validating the format, stripping off the header and trailer, identifying the record type, converting between foreign and C++ data types (which may of necessity involve "unclean" creation of C++ data types out of individual bits/bytes) and other processing.

However, you may discover that you do not require a 1:1 representation of the "transport" format and need only to hold aggregate data or otherwise part processed data.

If this is more a documentation exercise, then document the data in its original format, not in the target C++ format.

It's a Mackie Showbox Basically consisting of two devices:

  • a PA-box with a built-in digital mixer
  • a remote control

The devices communicate through a Cat 5 cable using a proprietary UART-based protocol which completely lacks error-checking. Only some packet types have responses. But the "responses" only "acknowledge the reception of packet of type x" and there is no waiting for responses implemented.

Example:

BE EF 01 1A 01 04 EF BE - snapshot (1A) {action: recall (01), slot: 04}
BE EF 01 01 00 1A EF BE - snapshot command received

I was trying to prepare some suggestions, but there's too much inconsistency in your description of the packet format (which is why I asked for the spec). For example, this:

The above declarations indicate that the structures FloatEntityPacket, BoolEntityPacket, and Uint8tEntityPacket should all be the same size. But your example byte strings are different lengths (and one appears to be wrong):

// Example: BE EF 08 03 00 58 00 00 00 00 00 C0 40 EF BE
// Example: BE EF 05 03 00 59 00 00 01 EF BE
// Example: BE EF 05 03 00 5C 00 00 00 04 EF BE

Also, you allege that BoolEntityPacket, and Uint8tEntityPacket both have the same body_size and packet_type. So, how do you tell one from the other?

I'm sorry for the inconsistency, it was a copy-paste error on my end:
BE EF 05 03 00 59 00 00 01 EF BE is actually BE EF 05 03 00 59 00 00 00 01 EF BE. (All packets of type 0x03 have three 0x00-bytes which I just call reserved bytes).

Unfortunately the protocol itself doesn't directly tell us which data type we're dealing with, so I had to go through the tedious process of manually identifying all existing entities and possible values.

I ended up with:

#include <Arduino.h>
#include <unordered_map>

enum packet_type : uint8_t {
    ACK = 0x01,
    ENTITY = 0x03,
    UNKNOWN_04 = 0x04,
    UNKNOWN_05 = 0x05,
    DATA_REQUEST = 0x06,
    UNKNOWN_15 = 0x15,
    UNKNOWN_16 = 0x16,
    TUNER_TOGGLE = 0x18,
    TUNER_FEEDBACK = 0x19,
    SNAPSHOT = 0x1A,
    SD_CARD_EVENT = 0x1E,
    HEARTBEAT = 0xFF
};

enum entity_data_type : uint8_t {
   BOOL = 0,
   UINT8 = 1,
   FLOAT = 2,
};

enum entity_id : uint8_t {
    FRONT_LED = 0,
    FEEDBACK_ELIM = 1,
    AMP_PA_MODE = 2,
    LOCATION_MODE = 3,
    SELECTED_CHAN = 4,
    INPUT1_GAIN = 5,
    INPUT1_VOLUME = 6,
    INPUT1_MUTE = 7,
    INPUT1_CLIP_OL_PRE = 8,
    INPUT1_CLIP_OL_POST = 9,
    INPUT1_EFFECT_1_MUTE = 10,
    INPUT1_EFFECT_1_AMOUNT = 11,
    INPUT1_EFFECT_2_MUTE = 12,
    INPUT1_EFFECT_2_AMOUNT = 13,
    INPUT1_EQ_ENABLE = 14,
    INPUT1_EQ_LOW_GAIN = 15,
    INPUT1_EQ_MID_GAIN = 16,
    INPUT1_EQ_HIGH_GAIN = 17,
    INPUT1_COMPRESSOR_ENABLE = 18,
    INPUT1_COMPRESSOR_AMOUNT = 19,
    INPUT1_EXT_FX_MUTE = 20,
    INPUT1_EXT_FX_SENDS = 21,
    INPUT2_GAIN = 22,
    INPUT2_VOLUME = 23,
    INPUT2_MUTE = 24,
    INPUT2_CLIP_OL_PRE = 25,
    INPUT2_CLIP_OL_POST = 26,
    INPUT2_EFFECT_1_MUTE = 27,
    INPUT2_EFFECT_1_AMOUNT = 28,
    INPUT2_EFFECT_2_MUTE = 29,
    INPUT2_EFFECT_2_AMOUNT = 30,
    INPUT2_EQ_ENABLE = 31,
    INPUT2_EQ_LOW_GAIN = 32,
    INPUT2_EQ_MID_GAIN = 33,
    INPUT2_EQ_HIGH_GAIN = 34,
    INPUT2_COMPRESSOR_ENABLE = 35,
    INPUT2_COMPRESSOR_AMOUNT = 36,
    INPUT2_EXT_FX_MUTE = 37,
    INPUT2_EXT_FX_SENDS = 38,
    INPUT3_GAIN = 39,
    INPUT3_VOLUME = 40,
    INPUT3_MUTE = 41,
    INPUT3_CLIP_OL_PRE = 42,
    INPUT3_CLIP_OL_POST = 43,
    INPUT3_EFFECT_1_MUTE = 44,
    INPUT3_EFFECT_1_AMOUNT = 45,
    INPUT3_EFFECT_2_MUTE = 46,
    INPUT3_EFFECT_2_AMOUNT = 47,
    INPUT3_EQ_ENABLE = 48,
    INPUT3_EQ_LOW_GAIN = 49,
    INPUT3_EQ_MID_GAIN = 50,
    INPUT3_EQ_HIGH_GAIN = 51,
    INPUT3_COMPRESSOR_ENABLE = 52,
    INPUT3_COMPRESSOR_AMOUNT = 53,
    INPUT3_EXT_FX_MUTE = 54,
    INPUT3_EXT_FX_SENDS = 55,
    INPUT4_GAIN = 56,
    INPUT4_VOLUME = 57,
    INPUT4_MUTE = 58,
    INPUT4_CLIP_OL_PRE = 59,
    INPUT4_CLIP_OL_POST = 60,
    INPUT4_EFFECT_1_MUTE = 61,
    INPUT4_EFFECT_1_AMOUNT = 62,
    INPUT4_EFFECT_2_MUTE = 63,
    INPUT4_EFFECT_2_AMOUNT = 64,
    INPUT4_EQ_ENABLE = 65,
    INPUT4_EQ_LOW_GAIN = 66,
    INPUT4_EQ_MID_GAIN = 67,
    INPUT4_EQ_HIGH_GAIN = 68,
    INPUT4_COMPRESSOR_ENABLE = 69,
    INPUT4_COMPRESSOR_AMOUNT = 70,
    INPUT4_EXT_FX_MUTE = 71,
    INPUT4_EXT_FX_SENDS = 72,
    STEREO_INPUT1_VOLUME = 73,
    STEREO_INPUT1_MUTE = 74,
    STEREO_INPUT1_CLIP_OL_PRE = 75,
    STEREO_INPUT1_CLIP_OL_POST = 76,
    STEREO_INPUT1_EQ_ENABLE = 77,
    STEREO_INPUT1_EQ_LOW_GAIN = 78,
    STEREO_INPUT1_EQ_MID_GAIN = 79,
    STEREO_INPUT1_EQ_HIGH_GAIN = 80,
    EFFECT11_TYPEID = 81,
    EFFECT12_TYPEID = 82,
    EFFECT13_TYPEID = 83,
    EFFECT14_TYPEID = 84,
    EFFECT21_TYPEID = 85,
    EFFECT22_TYPEID = 86,
    MAIN_HEADPHONE_GAIN = 87,
    MAIN_MASTER_GAIN = 88,
    MAIN_MUTE = 89,
    MAIN_CLIP_OL = 90,
    LOOPER_LEVEL = 91,
    LOOPER_STATE = 92,
    FX_BYPASS = 93
};

std::unordered_map<entity_id, entity_data_type> entity_type_mapping = {
    {FRONT_LED, BOOL},
    {FEEDBACK_ELIM, UINT8},
    {AMP_PA_MODE, BOOL},
    {LOCATION_MODE, BOOL},
    {SELECTED_CHAN, UINT8},
    {INPUT1_GAIN, FLOAT},
    {INPUT1_VOLUME, FLOAT},
    {INPUT1_MUTE, BOOL},
    {INPUT1_CLIP_OL_PRE, BOOL},
    {INPUT1_CLIP_OL_POST, BOOL},
    {INPUT1_EFFECT_1_MUTE, BOOL},
    {INPUT1_EFFECT_1_AMOUNT, FLOAT},
    {INPUT1_EFFECT_2_MUTE, BOOL},
    {INPUT1_EFFECT_2_AMOUNT, FLOAT},
    {INPUT1_EQ_ENABLE, BOOL},
    {INPUT1_EQ_LOW_GAIN, FLOAT},
    {INPUT1_EQ_MID_GAIN, FLOAT},
    {INPUT1_EQ_HIGH_GAIN, FLOAT},
    {INPUT1_COMPRESSOR_ENABLE, BOOL},
    {INPUT1_COMPRESSOR_AMOUNT, FLOAT},
    {INPUT1_EXT_FX_MUTE, BOOL},
    {INPUT1_EXT_FX_SENDS, FLOAT},
    {INPUT2_GAIN, FLOAT},
    {INPUT2_VOLUME, FLOAT},
    {INPUT2_MUTE, BOOL},
    {INPUT2_CLIP_OL_PRE, BOOL},
    {INPUT2_CLIP_OL_POST, BOOL},
    {INPUT2_EFFECT_1_MUTE, BOOL},
    {INPUT2_EFFECT_1_AMOUNT, FLOAT},
    {INPUT2_EFFECT_2_MUTE, BOOL},
    {INPUT2_EFFECT_2_AMOUNT, FLOAT},
    {INPUT2_EQ_ENABLE, BOOL},
    {INPUT2_EQ_LOW_GAIN, FLOAT},
    {INPUT2_EQ_MID_GAIN, FLOAT},
    {INPUT2_EQ_HIGH_GAIN, FLOAT},
    {INPUT2_COMPRESSOR_ENABLE, BOOL},
    {INPUT2_COMPRESSOR_AMOUNT, FLOAT},
    {INPUT2_EXT_FX_MUTE, BOOL},
    {INPUT2_EXT_FX_SENDS, FLOAT},
    {INPUT3_GAIN, FLOAT},
    {INPUT3_VOLUME, FLOAT},
    {INPUT3_MUTE, BOOL},
    {INPUT3_CLIP_OL_PRE, BOOL},
    {INPUT3_CLIP_OL_POST, BOOL},
    {INPUT3_EFFECT_1_MUTE, BOOL},
    {INPUT3_EFFECT_1_AMOUNT, FLOAT},
    {INPUT3_EFFECT_2_MUTE, BOOL},
    {INPUT3_EFFECT_2_AMOUNT, FLOAT},
    {INPUT3_EQ_ENABLE, BOOL},
    {INPUT3_EQ_LOW_GAIN, FLOAT},
    {INPUT3_EQ_MID_GAIN, FLOAT},
    {INPUT3_EQ_HIGH_GAIN, FLOAT},
    {INPUT3_COMPRESSOR_ENABLE, BOOL},
    {INPUT3_COMPRESSOR_AMOUNT, FLOAT},
    {INPUT3_EXT_FX_MUTE, BOOL},
    {INPUT3_EXT_FX_SENDS, FLOAT},
    {INPUT4_GAIN, FLOAT},
    {INPUT4_VOLUME, FLOAT},
    {INPUT4_MUTE, BOOL},
    {INPUT4_CLIP_OL_PRE, BOOL},
    {INPUT4_CLIP_OL_POST, BOOL},
    {INPUT4_EFFECT_1_MUTE, BOOL},
    {INPUT4_EFFECT_1_AMOUNT, FLOAT},
    {INPUT4_EFFECT_2_MUTE, BOOL},
    {INPUT4_EFFECT_2_AMOUNT, FLOAT},
    {INPUT4_EQ_ENABLE, BOOL},
    {INPUT4_EQ_LOW_GAIN, FLOAT},
    {INPUT4_EQ_MID_GAIN, FLOAT},
    {INPUT4_EQ_HIGH_GAIN, FLOAT},
    {INPUT4_COMPRESSOR_ENABLE, BOOL},
    {INPUT4_COMPRESSOR_AMOUNT, FLOAT},
    {INPUT4_EXT_FX_MUTE, BOOL},
    {INPUT4_EXT_FX_SENDS, FLOAT},
    {STEREO_INPUT1_VOLUME, FLOAT},
    {STEREO_INPUT1_MUTE, BOOL},
    {STEREO_INPUT1_CLIP_OL_PRE, BOOL},
    {STEREO_INPUT1_CLIP_OL_POST, BOOL},
    {STEREO_INPUT1_EQ_ENABLE, BOOL},
    {STEREO_INPUT1_EQ_LOW_GAIN, FLOAT},
    {STEREO_INPUT1_EQ_MID_GAIN, FLOAT},
    {STEREO_INPUT1_EQ_HIGH_GAIN, FLOAT},
    {EFFECT11_TYPEID, UINT8},
    {EFFECT12_TYPEID, UINT8},
    {EFFECT13_TYPEID, UINT8},
    {EFFECT14_TYPEID, UINT8},
    {EFFECT21_TYPEID, UINT8},
    {EFFECT22_TYPEID, UINT8},
    {MAIN_HEADPHONE_GAIN, FLOAT},
    {MAIN_MASTER_GAIN, FLOAT},
    {MAIN_MUTE, BOOL},
    {MAIN_CLIP_OL, BOOL},
    {LOOPER_LEVEL, FLOAT},
    {LOOPER_STATE, UINT8},
    {FX_BYPASS, BOOL}
};

and then based on the great suggestion of kenb4, I was planning on doing something like this:

void handlePacket(uint8_t *raw_packet) {
  switch (raw_packet[3]) { // packet_type
    case ENTITY:
      switch (entity_type_mapping[raw_packet[5]]) { // entity_type_mapping[entity_id]
        case BOOL:
          {
            // ...
          }
        case UINT8:
          {
            // ...
          }
        case FLOAT:
          {
            // ...
          }
      }
      break;
    default:
      break;
  }
}

But I'm more than open to suggestions on how to do this better. Evidently I have a lot to learn when it comes to C++ and best-practices.

My entire proof-of-concept sketch looks like this at the moment:

#include <Arduino.h>
#include <unordered_map>

enum packet_type : uint8_t {
    ACK = 0x01,
    ENTITY = 0x03,
    UNKNOWN_04 = 0x04,
    UNKNOWN_05 = 0x05,
    DATA_REQUEST = 0x06,
    UNKNOWN_15 = 0x15,
    UNKNOWN_16 = 0x16,
    TUNER_TOGGLE = 0x18,
    TUNER_FEEDBACK = 0x19,
    SNAPSHOT = 0x1A,
    SD_CARD_EVENT = 0x1E,
    HEARTBEAT = 0xFF
};

enum entity_data_type : uint8_t {
   BOOL = 0,
   UINT8 = 1,
   FLOAT = 2,
};

enum entity_id : uint8_t {
    FRONT_LED = 0,
    FEEDBACK_ELIM = 1,
    AMP_PA_MODE = 2,
    LOCATION_MODE = 3,
    SELECTED_CHAN = 4,
    INPUT1_GAIN = 5,
    INPUT1_VOLUME = 6,
    INPUT1_MUTE = 7,
    INPUT1_CLIP_OL_PRE = 8,
    INPUT1_CLIP_OL_POST = 9,
    INPUT1_EFFECT_1_MUTE = 10,
    INPUT1_EFFECT_1_AMOUNT = 11,
    INPUT1_EFFECT_2_MUTE = 12,
    INPUT1_EFFECT_2_AMOUNT = 13,
    INPUT1_EQ_ENABLE = 14,
    INPUT1_EQ_LOW_GAIN = 15,
    INPUT1_EQ_MID_GAIN = 16,
    INPUT1_EQ_HIGH_GAIN = 17,
    INPUT1_COMPRESSOR_ENABLE = 18,
    INPUT1_COMPRESSOR_AMOUNT = 19,
    INPUT1_EXT_FX_MUTE = 20,
    INPUT1_EXT_FX_SENDS = 21,
    INPUT2_GAIN = 22,
    INPUT2_VOLUME = 23,
    INPUT2_MUTE = 24,
    INPUT2_CLIP_OL_PRE = 25,
    INPUT2_CLIP_OL_POST = 26,
    INPUT2_EFFECT_1_MUTE = 27,
    INPUT2_EFFECT_1_AMOUNT = 28,
    INPUT2_EFFECT_2_MUTE = 29,
    INPUT2_EFFECT_2_AMOUNT = 30,
    INPUT2_EQ_ENABLE = 31,
    INPUT2_EQ_LOW_GAIN = 32,
    INPUT2_EQ_MID_GAIN = 33,
    INPUT2_EQ_HIGH_GAIN = 34,
    INPUT2_COMPRESSOR_ENABLE = 35,
    INPUT2_COMPRESSOR_AMOUNT = 36,
    INPUT2_EXT_FX_MUTE = 37,
    INPUT2_EXT_FX_SENDS = 38,
    INPUT3_GAIN = 39,
    INPUT3_VOLUME = 40,
    INPUT3_MUTE = 41,
    INPUT3_CLIP_OL_PRE = 42,
    INPUT3_CLIP_OL_POST = 43,
    INPUT3_EFFECT_1_MUTE = 44,
    INPUT3_EFFECT_1_AMOUNT = 45,
    INPUT3_EFFECT_2_MUTE = 46,
    INPUT3_EFFECT_2_AMOUNT = 47,
    INPUT3_EQ_ENABLE = 48,
    INPUT3_EQ_LOW_GAIN = 49,
    INPUT3_EQ_MID_GAIN = 50,
    INPUT3_EQ_HIGH_GAIN = 51,
    INPUT3_COMPRESSOR_ENABLE = 52,
    INPUT3_COMPRESSOR_AMOUNT = 53,
    INPUT3_EXT_FX_MUTE = 54,
    INPUT3_EXT_FX_SENDS = 55,
    INPUT4_GAIN = 56,
    INPUT4_VOLUME = 57,
    INPUT4_MUTE = 58,
    INPUT4_CLIP_OL_PRE = 59,
    INPUT4_CLIP_OL_POST = 60,
    INPUT4_EFFECT_1_MUTE = 61,
    INPUT4_EFFECT_1_AMOUNT = 62,
    INPUT4_EFFECT_2_MUTE = 63,
    INPUT4_EFFECT_2_AMOUNT = 64,
    INPUT4_EQ_ENABLE = 65,
    INPUT4_EQ_LOW_GAIN = 66,
    INPUT4_EQ_MID_GAIN = 67,
    INPUT4_EQ_HIGH_GAIN = 68,
    INPUT4_COMPRESSOR_ENABLE = 69,
    INPUT4_COMPRESSOR_AMOUNT = 70,
    INPUT4_EXT_FX_MUTE = 71,
    INPUT4_EXT_FX_SENDS = 72,
    STEREO_INPUT1_VOLUME = 73,
    STEREO_INPUT1_MUTE = 74,
    STEREO_INPUT1_CLIP_OL_PRE = 75,
    STEREO_INPUT1_CLIP_OL_POST = 76,
    STEREO_INPUT1_EQ_ENABLE = 77,
    STEREO_INPUT1_EQ_LOW_GAIN = 78,
    STEREO_INPUT1_EQ_MID_GAIN = 79,
    STEREO_INPUT1_EQ_HIGH_GAIN = 80,
    EFFECT11_TYPEID = 81,
    EFFECT12_TYPEID = 82,
    EFFECT13_TYPEID = 83,
    EFFECT14_TYPEID = 84,
    EFFECT21_TYPEID = 85,
    EFFECT22_TYPEID = 86,
    MAIN_HEADPHONE_GAIN = 87,
    MAIN_MASTER_GAIN = 88,
    MAIN_MUTE = 89,
    MAIN_CLIP_OL = 90,
    LOOPER_LEVEL = 91,
    LOOPER_STATE = 92,
    FX_BYPASS = 93
};

std::unordered_map<entity_id, entity_data_type> entity_type_mapping = {
    {FRONT_LED, BOOL},
    {FEEDBACK_ELIM, UINT8},
    {AMP_PA_MODE, BOOL},
    {LOCATION_MODE, BOOL},
    {SELECTED_CHAN, UINT8},
    {INPUT1_GAIN, FLOAT},
    {INPUT1_VOLUME, FLOAT},
    {INPUT1_MUTE, BOOL},
    {INPUT1_CLIP_OL_PRE, BOOL},
    {INPUT1_CLIP_OL_POST, BOOL},
    {INPUT1_EFFECT_1_MUTE, BOOL},
    {INPUT1_EFFECT_1_AMOUNT, FLOAT},
    {INPUT1_EFFECT_2_MUTE, BOOL},
    {INPUT1_EFFECT_2_AMOUNT, FLOAT},
    {INPUT1_EQ_ENABLE, BOOL},
    {INPUT1_EQ_LOW_GAIN, FLOAT},
    {INPUT1_EQ_MID_GAIN, FLOAT},
    {INPUT1_EQ_HIGH_GAIN, FLOAT},
    {INPUT1_COMPRESSOR_ENABLE, BOOL},
    {INPUT1_COMPRESSOR_AMOUNT, FLOAT},
    {INPUT1_EXT_FX_MUTE, BOOL},
    {INPUT1_EXT_FX_SENDS, FLOAT},
    {INPUT2_GAIN, FLOAT},
    {INPUT2_VOLUME, FLOAT},
    {INPUT2_MUTE, BOOL},
    {INPUT2_CLIP_OL_PRE, BOOL},
    {INPUT2_CLIP_OL_POST, BOOL},
    {INPUT2_EFFECT_1_MUTE, BOOL},
    {INPUT2_EFFECT_1_AMOUNT, FLOAT},
    {INPUT2_EFFECT_2_MUTE, BOOL},
    {INPUT2_EFFECT_2_AMOUNT, FLOAT},
    {INPUT2_EQ_ENABLE, BOOL},
    {INPUT2_EQ_LOW_GAIN, FLOAT},
    {INPUT2_EQ_MID_GAIN, FLOAT},
    {INPUT2_EQ_HIGH_GAIN, FLOAT},
    {INPUT2_COMPRESSOR_ENABLE, BOOL},
    {INPUT2_COMPRESSOR_AMOUNT, FLOAT},
    {INPUT2_EXT_FX_MUTE, BOOL},
    {INPUT2_EXT_FX_SENDS, FLOAT},
    {INPUT3_GAIN, FLOAT},
    {INPUT3_VOLUME, FLOAT},
    {INPUT3_MUTE, BOOL},
    {INPUT3_CLIP_OL_PRE, BOOL},
    {INPUT3_CLIP_OL_POST, BOOL},
    {INPUT3_EFFECT_1_MUTE, BOOL},
    {INPUT3_EFFECT_1_AMOUNT, FLOAT},
    {INPUT3_EFFECT_2_MUTE, BOOL},
    {INPUT3_EFFECT_2_AMOUNT, FLOAT},
    {INPUT3_EQ_ENABLE, BOOL},
    {INPUT3_EQ_LOW_GAIN, FLOAT},
    {INPUT3_EQ_MID_GAIN, FLOAT},
    {INPUT3_EQ_HIGH_GAIN, FLOAT},
    {INPUT3_COMPRESSOR_ENABLE, BOOL},
    {INPUT3_COMPRESSOR_AMOUNT, FLOAT},
    {INPUT3_EXT_FX_MUTE, BOOL},
    {INPUT3_EXT_FX_SENDS, FLOAT},
    {INPUT4_GAIN, FLOAT},
    {INPUT4_VOLUME, FLOAT},
    {INPUT4_MUTE, BOOL},
    {INPUT4_CLIP_OL_PRE, BOOL},
    {INPUT4_CLIP_OL_POST, BOOL},
    {INPUT4_EFFECT_1_MUTE, BOOL},
    {INPUT4_EFFECT_1_AMOUNT, FLOAT},
    {INPUT4_EFFECT_2_MUTE, BOOL},
    {INPUT4_EFFECT_2_AMOUNT, FLOAT},
    {INPUT4_EQ_ENABLE, BOOL},
    {INPUT4_EQ_LOW_GAIN, FLOAT},
    {INPUT4_EQ_MID_GAIN, FLOAT},
    {INPUT4_EQ_HIGH_GAIN, FLOAT},
    {INPUT4_COMPRESSOR_ENABLE, BOOL},
    {INPUT4_COMPRESSOR_AMOUNT, FLOAT},
    {INPUT4_EXT_FX_MUTE, BOOL},
    {INPUT4_EXT_FX_SENDS, FLOAT},
    {STEREO_INPUT1_VOLUME, FLOAT},
    {STEREO_INPUT1_MUTE, BOOL},
    {STEREO_INPUT1_CLIP_OL_PRE, BOOL},
    {STEREO_INPUT1_CLIP_OL_POST, BOOL},
    {STEREO_INPUT1_EQ_ENABLE, BOOL},
    {STEREO_INPUT1_EQ_LOW_GAIN, FLOAT},
    {STEREO_INPUT1_EQ_MID_GAIN, FLOAT},
    {STEREO_INPUT1_EQ_HIGH_GAIN, FLOAT},
    {EFFECT11_TYPEID, UINT8},
    {EFFECT12_TYPEID, UINT8},
    {EFFECT13_TYPEID, UINT8},
    {EFFECT14_TYPEID, UINT8},
    {EFFECT21_TYPEID, UINT8},
    {EFFECT22_TYPEID, UINT8},
    {MAIN_HEADPHONE_GAIN, FLOAT},
    {MAIN_MASTER_GAIN, FLOAT},
    {MAIN_MUTE, BOOL},
    {MAIN_CLIP_OL, BOOL},
    {LOOPER_LEVEL, FLOAT},
    {LOOPER_STATE, UINT8},
    {FX_BYPASS, BOOL}
};

template <typename T>
void printRawPacket(const char* message, T *packet) {
    Serial.print(message);
    Serial.print(": ");
    auto rawBytes = reinterpret_cast<uint8_t *>(packet);
    for (int i = 4 /* skip vtable */; i < sizeof(T); i++) {
        Serial.printf("%02X ", rawBytes[i]);
    }
    Serial.println();
}

template <uint8_t type, typename T>
struct Packet {
    uint8_t start_seq[2] {0xBE, 0xEF};
    uint8_t body_size {sizeof(T) - 1};
    uint8_t packet_type {type};
    T p;
    uint8_t end_seq[2] {0xEF, 0xBE};

    void importP(const uint8_t *raw_packet) {
        memcpy(&p, raw_packet + 4, sizeof(T));
    }
    void exportP(uint8_t *raw_packet) {
        memcpy(raw_packet + 4, &p, sizeof(T));
    }
    virtual void intercept() = 0;
    void handle(uint8_t *raw_packet) {
        // printRawPacket("before", this);
        importP(raw_packet);
        // printRawPacket("import", this);
        intercept();
        // printRawPacket("export", this);
        exportP(raw_packet);
    }
} __attribute__((packed));

struct StateOnlyNoBody {
    uint8_t state;
} __attribute__((packed));

// Example: BE EF 00 1E 02 EF BE
typedef Packet<SD_CARD_EVENT, StateOnlyNoBody> SdCardEventPacket;

// Example: BE EF 01 18 01 02 EF BE
struct ToggleTunerBody {
    uint8_t state;
    uint8_t channel;
} __attribute__((packed));

typedef Packet<TUNER_TOGGLE, ToggleTunerBody> ToggleTunerPacket;

// Example: BE EF 01 1A 01 04 EF BE
struct SnapshotBody {
    uint8_t action;
    uint8_t slot;
} __attribute__((packed));

typedef Packet<SNAPSHOT, SnapshotBody> SnapshotPacket;

template <uint8_t id, typename T>
struct EntityBody {
    uint8_t header_byte {0};
    uint8_t entity_id {id};
    uint8_t reserved[3] {0};
    T value;
} __attribute__((packed));

// Example: BE EF 05 03 00 59 00 00 00 01 EF BE
typedef Packet<ENTITY, EntityBody<BOOL, bool>> BoolEntityPacket;

// Example: BE EF 05 03 00 5C 00 00 00 04 EF BE
typedef Packet<ENTITY, EntityBody<UINT8, uint8_t>> Uint8EntityPacket;

// Example: BE EF 08 03 00 58 00 00 00 00 00 C0 40 EF BE
typedef Packet<ENTITY, EntityBody<FLOAT, float>> FloatEntityPacket;

struct QuarterFloatEntityPacket : Packet<ENTITY, EntityBody<FLOAT, float>> {
    void intercept() override {
        p.value *= 0.25;
    }
};

struct InvertBoolEntityPacket : Packet<ENTITY, EntityBody<BOOL, bool>> {
    void intercept() override {
        p.value = !p.value;
    }
};

struct IncrementUint8EntityPacket : Packet<ENTITY, EntityBody<UINT8, uint8_t>> {
    void intercept() override {
        p.value++;
    }
};

void handlePacket(uint8_t *raw_packet) {
  packet_type packetType = static_cast<packet_type>(raw_packet[3]);
  switch (packetType) {
    case ENTITY:
      entity_id entityId = static_cast<entity_id>(raw_packet[5]);
      entity_data_type entityType = entity_type_mapping[entityId];
      Serial.printf("PacketType: %02X, EntityId: %02X, EntityType: %02X\n", packetType, entityId, entityType);
      switch (entityType) {
        case BOOL:
          {
            Serial.println("BoolEntityPacket");
            InvertBoolEntityPacket boolPacket;
            boolPacket.handle(raw_packet);
            break;
          }
        case UINT8:
          {
            Serial.println("Uint8EntityPacket");
            IncrementUint8EntityPacket uint8Packet;
            uint8Packet.handle(raw_packet);
            break;
          }
        case FLOAT:
          {
            Serial.println("FloatEntityPacket");
            QuarterFloatEntityPacket floatPacket;
            floatPacket.handle(raw_packet);
            break;
          }
      }
  }
}

void setup() {
  Serial.begin(115200);
  delay(2000);
  
  // Example packet
  uint8_t raw_packet[] = {
    // ------ HEADER ------
    0xBE, 0xEF, // start sequence
    0x08,       // body size uint8_t
    0x03,       // type uint8_t
    0x00,       // unknown
    // ------ HEADER ------

    // ------ BODY ------
    0x58,                   // 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 ------
  };


  Serial.println("Original raw_packet: ");
  for (int i = 0; i < sizeof(raw_packet); i++) {
    Serial.printf("%02X ", raw_packet[i]);
  }
  Serial.println();

  handlePacket(raw_packet);

  Serial.println("Modified raw_packet: ");
  for (int i = 0; i < sizeof(raw_packet); i++) {
    Serial.printf("%02X ", raw_packet[i]);
  }
  Serial.println();
}

void loop() {
  delay(10);
}

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.