cprotocol-buffersnanopb

Decoding oneof Nanopb


I am currently working with oneof properties. I am able to encode them without issue, however, decoding seems to be an issue. I fail to understand why it doesn't work.

My proto file looks like this:

syntax = "proto2";

message stringCallback{

    required string name = 1;
    required string surname = 2;
    required int32 age = 3;
    repeated sint32 values = 4;
    repeated metrics metric_data = 5;
    
    oneof payload {
        int32 i_val = 6;
        float f_val = 7;
        string msg = 8;
    }
}

message metrics{
    required int32 id = 1;
    required string type = 2;
    required sint32 value = 3;
}

Whenever I send from a C# application a message containing in the payload an integer, there is no issue decoding. I get the right int value and which_payload value as well.

I then tried to send a float. This resulted in a value which was not corresponding to the value I send through C# (which makes sense because I use the "standard decoder" which means that it treats it as an int instead of a fixed32) I don't know how to tell the decoder to decode this value with the fixed32 option.

And finally sending a payload which contains a message resulted in an incorrect which_payload value (Type None: 0) and no corresponding message. Even though before the decode function I assigned a callback function to decode a string (works perfectly).

Additional info:

This is the byte array which I send to the Nanopb code containing a string in the payload:

0A 04 4B 65 65 73 12 07 76 61 6E 20 44 61 6D 18 34 20 02 20 04 20 06 20 08 20 0A 20 0C 20 0E 20 10 20 12 20 14 20 16 20 18 2A 0C 08 01 12 06 53 65 6E 73 6F 72 18 04 2A 0A 08 02 12 04 44 61 74 61 18 0A 2A 0E 08 03 12 08 57 69 72 65 6C 65 73 73 18 0E 2A 0D 08 04 12 07 54 65 73 74 69 6E 67 18 04 2A 10 08 05 12 0A 46 72 6F 6E 74 20 64 6F 6F 72 18 0A 2A 1B 08 06 12 15 54 68 69 73 20 69 73 20 61 20 72 61 6E 64 6F 6D 20 6E 61 6D 65 18 0E 42 10 48 65 6C 6C 6F 20 66 72 6F 6D 20 6F 6E 65 6F 66

I get from an online decoder the correct decoded variables but not on Nanopb. What am I supposed to do?

EDIT:

As per request, my decoder test function:

void test_decode(byte* payload, unsigned int length){
  
  IntArray I_array = {0, 0};
  MetricArray M_array = {0,0};

  char name[MAX_STRING_LENGTH];
  char surname[MAX_STRING_LENGTH];
  char msg[MAX_STRING_LENGTH];

  stringCallback message = stringCallback_init_zero;

  message.name.funcs.decode = String_decode;
  message.name.arg = &name;

  message.surname.funcs.decode = String_decode;
  message.surname.arg = &surname;

  message.values.funcs.decode = IntArray_decode;
  message.values.arg = &I_array;

  message.metric_data.funcs.decode = MetricArray_decode;
  message.metric_data.arg = &M_array;

  message.payload.msg.arg = &msg;
  message.payload.msg.funcs.decode = String_decode;

  pb_istream_t istream = pb_istream_from_buffer(payload, length);

  if(!pb_decode(&istream, stringCallback_fields, &message)){
    Serial.println("Total decoding failed!");
    return;
  }

  Serial.println(name);
  Serial.println(surname);
  Serial.println(message.age);
  Serial.println(I_array.count);
  Serial.println(M_array.count);
  Serial.println();
  MetricArray_print(&M_array);

  Serial.println();
  Serial.println("Oneof: ");
  Serial.println(message.which_payload);
}

Solution

  • After some searching and testing I found a workaround!

    I look at these examples, and figured that there are possibilities of having callbacks in oneof fields. There is a possibility to add a Nanopb option to the proto file which tells the message that it should generate a callback. The option:

    option (nanopb_msgopt).submsg_callback = true;
    

    By adding this line to the proto file it will generate (to my understanding) a set-up callback. This callback gets called at the start of decoding the message. This gives us the opportunity to setup different callbacks for different fields in the oneof depending on the type we are decoding.

    The problem before with oneof is that it was impossible to assign callback functions to the fields of the oneof type since they all share the same memory space. But by having this set-up callback we can actually have a look at what we are receiving and assign a callback accordingly.

    However, there is a catch. I found out that this option will not work on anything other than messages in the oneof. This means no direct callback type can be assigned to a oneof field!

    The workaround I used is to encapsulate the wanted callback in a message. This will trigger the set-up callback which then can be used to assign the correct callback function in the oneof.

    EXAMPLE:

    Lets take this proto file:

    syntax = "proto2";
    
    message callback{
    
        required string name = 1;
        required string surname = 2;
        required int32 age = 3;
        repeated sint32 values = 4;
        repeated metrics metric_data = 5;
        
        oneof payload {
            int32 i_val = 6;
            float f_val = 7;
            string msg = 8;
            payloadmsg p_msg = 9;
            payloadmsg2 p_msg2 = 10;
        }
    }
    
    message metrics{
        required int32 id = 1;
        required string type = 2;
        required sint32 value = 3;
    }
    
    message payloadmsg{
        required int32 id = 1;
        required string type = 2;
        required string msg = 3;
        repeated sint32 values = 4;
    }
    
    message payloadmsg2{
        required int32 id = 1;
        required string type = 2;
        required string msg = 3;
        repeated sint32 values = 4;
    }
    

    I gave the strings in this proto file a maximum length which removed their callback type and exchanged it for a character array.

    For testing purposes I didn't assign a size to the integer arrays in the payloadmsg messages.

    The goal is to decode the arrays stored in the payloadmsg messages depending on the oneof type received (payloadmsg or payloadmsg2)

    Then the set-up callback has to be created:

    bool Oneof_decode(pb_istream_t *stream, const pb_field_t *field, void** arg){
    
      Callback* msg = (Callback*)field->message;
    
      switch(field->tag){
        case stringCallback_p_msg_tag:{
          Serial.println("Oneof type: p_msg detected!");
          payloadmsg* p_message = (payloadmsg*)field->pData;
          IntArray* array = (IntArray*)*arg;
          p_message->values.arg = array;
          p_message->values.funcs.decode = IntArray_decode;
          break;
        }
        case stringCallback_p_msg2_tag:{
          Serial.println("Oneof type: p_msg2 detected!");
          payloadmsg2* p_message2 = (payloadmsg2*)field->pData;
          IntArray* array = (IntArray*)*arg;
          p_message2->values.arg = array;
          p_message2->values.funcs.decode = IntArray_decode;
          break;
        }
      }
      return true;
    }
    

    We look at the tag we receive while decoding to see which type of callback has to be assigned. This is done by accessing the field. By using a switch case we can decide, according to the field tag, what decode callback function to assign. We pass the arguments and decoder functions like normal, after that everything is set-up correctly.

    Finally we assign this callback set-up to the compiler generated variable called cb_payload. This variable will be added to your message struct when you put in the option in the protofile. This is how to assign the set-up callback:

      message.cb_payload.arg = &I_array;
      message.cb_payload.funcs.decode = Oneof_decode;
    
      pb_istream_t istream = pb_istream_from_buffer(payload, length);
    
      if(!pb_decode(&istream, stringCallback_fields, &message)){
        Serial.println("Total decoding failed!");
        return;
      }
    

    I pass my own IntArray struct to the cb_payload function as an argument to then pass it on to the correctly assigned decoder.

    The result is that whenever I decode payloadmsg or payloadmsg2 the correct decoder gets assigned and the right values are being decoded into the structs.