Introduction

Protocol Buffer

Protocol Buffer is a tool developed by Google for serializing structured data which can then be used to transmit data from one medium to another. For example, sensor readings captured from a microcontroller could be sent to the cloud for logging and diagnostics after encoding the data in a serialized format for easier transmission of data bytes.

Protocol buffer can serialize data from variety of languages such as Java, Python, Objective-C C++, Dart, Go, Ruby, and C# along with running on any platform.

Nanopb

Google's Protocol Buffer Tool can generate data structures for C++ and not for C. Since microcontrollers have limited RAM and code memory, the embedded industry does not prefer to program in C++, thus making Google's Protocol Buffer tool less suitable. Nanopb provides a C based library for encoding and decoding messages in Google's Protocol Buffers format.

Why do we need Protocol Buffers (or Nanopb)?

Consider the scenario where we need to transmit CAN message frames from vehicle to the cloud for data logging and analysis. The data of a CAN frame can be held in the following C Data structure:

typedef struct {
  // 8 bytes:
  uint32_t message_id;    ///< Message ID of the CAN bus message
  uint32_t timestamp_ms;  ///< The receive timestamp(in microseconds) of the message

  // 2 bytes:
  uint8_t dlc;         ///< Data length code, 0-8 bytes
  uint8_t bus_id : 4;  ///< Identifies which CAN bus this message belongs to
  uint8_t ide : 1;     ///< ID Extended
  uint8_t rtr : 1;     ///< Remote Transmission Request

  // Usually has 8 bytes.
  uint8_t data[8];  //* byte data
} can_message_s;

For sending the data held in the data structure above to the cloud, a popular choice is using BSD Sockets. After establishing connection with the cloud and obtaining the socket file descriptor sockfd we can use send Socket API to send our data.

/**
 * @param sockfd Specifies the socket file descriptor.
 * @param buf Points to the buffer containing the message to send.
 * @param len Specifies the length of the message in bytes.
 *
 * @return number of bytes sent.
 */
ssize_t send(int sockfd, const void *buf, size_t len, int flags);

Since the send API requires an array of bytes to be transferred, we need a way to convert our CAN based C data structure to serialized data bytes. This is where Protocol Buffers (or Nanopb) comes into play. Protocol Buffers will serialize the CAN data structure as an array of bytes in an efficient manner which can then be transferred to the cloud server using Socket send API.

Getting started

Start with reading the following materials which are very useful in getting up to speed about protobufs:

  1. Google's Documentation: A great tutorial with simple examples and common API explanation.

  2. Nanopb Documentation

Installation and Setup

For Sibros Employees:

Sibros internal repository integrates Nanopb library with Bazel. Custom cc_nanopb_library Bazel rule is used to auto-generate C structures from .proto file in the form of an header(.h) file. Refer to the Bazel BUILD file in the Nanopb examples to generate the headers(.h) and source files(.c).

For others:

To use Nanopb, protocol compiler needs to be installed. Install Python3 and Python3-Pip package:

$ apt-get install python3 python3-pip

Install protobuf and grpcio-tools packages need by Nanopb:

$ pip3 install protobuf grpcio-tools

Protocol Buffers messages are defined in a proto file as follows:

//foo.proto

message Foo {
   required int id = 1;
}

This .proto file is used to generate the Nanopb headers(.h) and source files(.c) using the python script provided by Nanopb.

$ git clone https://github.com/nanopb/nanopb.git
$ cd nanopb
$ python3 generator/nanopb_generator.py /path/to/foo.proto

Implementation and Examples

Note: The examples should work out-of-the-box (if bazel targets are correctly defined by the user) for Sibro's Employees since all Bazel rules and BUILD files are setup. However, others need to create a Makefile to build and link all the source files (and also Nanopb library) for creating a binary target. I will add a Makefile for all the examples soon.

Example - 1

In this example, a single CAN message with a single byte of data is serialized to protobuf format using Nanopb.

Proto Files

Proto files are used to structure the protocol buffer data using protocol buffer language. We need to define the CAN message and its contents inside a proto file as follows:

// ex1/ex1.proto
message proto_can_message {
  required int32 message_id = 1; // Standard 11-bit

  required int32 bus_id = 2;     // The bus number which received the CAN message
  
  required int64 timestamp_ms = 3;
  required int32 data_byte = 4; // only 1 byte of data (CAN frame data bytes are usually 8 but modified here for simplification)
}

In the proto file a message to be transmitted is defined using the message keyword, followed by the message name (proto_can_message in the exampe above). The proto message can have multiple data members such as message_id, bus_id, timestamp_ms, data_byte as above.

If required keyword is used before the declaration of the message's member, the member has to be initialized. Alternatively option keyword can also be used which makes initializing the data member optional.

The number after equality operator is the tag of the message member which are used to match fields when serializing and deserializing the data. For Eg. the tag number for timestamp_ms is 1.

For more information about proto message, visit Google's proto file documentation.

Compiling proto files and generation of C header(.h) file

For Bazel users, we create an instance (or target) of cc_nanopb_library rule, which when executed will compile the proto file and generate the (.h) file.

# ex1/BUILD

load("@rules_proto//proto:defs.bzl", "proto_library")
load("//embedded/infrastructure/bazel/rules:nanopb.bzl", "cc_nanopb_library")

proto_library(
    name = "ex1_proto",
    srcs = ["ex1.proto"],
)

cc_nanopb_library(
    name = "ex1_c_proto",
    deps = [
        ":ex1_proto",
    ],
)

Building the target we obtain the Nanopb generated file.

$ bazel build //shared/temp_work/nanopb-examples/ex1:ex1_c_proto

bazel-bin/shared/temp_work/nanopb-examples/ex1/ex1.npb.h

Non-Bazel users, can generate the header (.h) file using the python script provided by Nanopb:

$ cd nanopb
$ python3 generator/nanopb_generator.py /path/to/nanopb-examples/ex1/ex1.proto

The Nanopb generated header (.h) file will create an proto message equivalent C structure as follows:

/* Struct definitions */
typedef struct _proto_can_message {
    int32_t message_id;
    int32_t bus_id;
    int64_t timestamp_ms;
    int32_t data_byte;
/* @@protoc_insertion_point(struct:proto_can_message) */
} proto_can_message;

Refer to the complete Nanopb generated header.

Encoding data using Nanopb

We start with creating our .c and .h files for defining encoding data operation by creating encode_packet function. We have to include the Nanopb headers and the generated header files.

// ex1/ex1.c

/* Main Module Header */
#include "ex1.h"

/* Standard Includes */
#include <stdio.h>

/* Nanopb Includes */
#include "pb_decode.h"
#include "pb_encode.h"

/**
 * @param proto_msg Generated proto_can_message
 * @param packet Data structure to hold encoded data bytes
 * @param can_msg CAN message data structure to encode
 * 
 * @return number of bytes encoded, or 0 upon error.
 */
size_t encode_packet(proto_can_message* proto_msg, serial_data_packet_s* packet, can_message_s* can_msg);

Note: serial_data_packet_s* packet holds the memory buffer for Nanopb encoded data bytes:

typedef struct encoded_packet {
  uint8_t* buffer;
  uint16_t max_buffer_size;
  size_t bytes_written;
} serial_data_packet_s;

Now we can populate the Nanopb generated data strucutre proto_msg with our CAN message to be serialized as follows:

size_t encode_packet(proto_can_message* proto_msg, serial_data_packet_s* packet, can_message_s* can_msg) {

  /* Fill the proto packet */
  proto_msg->timestamp_ms = can_msg->timestamp_ms;
  proto_msg->bus_id = can_msg->bus_id;
  proto_msg->message_id = can_msg->message_id;
  proto_msg->data_byte = can_msg->data;

}

We then create an output stream for writing the data to the buffer:

size_t encode_packet(proto_can_message* proto_msg, serial_data_packet_s* packet, can_message_s* can_msg) {
  /* Fill the proto packet */
  ...
  pb_ostream_t stream = pb_ostream_from_buffer(encoded_packet->buffer, encoded_packet->encoded_packet_size);

}  

The Protobuf message is then encoded to a serialized format using Nanopb's pb_encode() API. The number of bytes encoded is obtained from stream.bytes_written data member.

size_t encode_packet(proto_can_message* proto_msg, serial_data_packet_s* packet, can_message_s* can_msg) {
  size_t number_bytes_encoded = 0;

  /* Fill the proto packet */
  ...

  pb_ostream_t stream = pb_ostream_from_buffer(packet->buffer, packet->max_buffer_size);

  if (pb_encode(&stream, proto_can_message_fields, proto_msg)) {
    printf("Encoding Success!!!\n");

    number_bytes_encoded = stream.bytes_written;
    packet->bytes_written = stream.bytes_written;
  }

  return number_bytes_encoded;
}

pb_encode() function encodes the contents of a proto message C structure and writes it to output stream. Visit Nanopb Docs for more information on pb_encode().

proto_can_message_fields argument to the pb_encode() function is an auto-generated Struct field which will encoding specification for Nanopb. Visit Nanopb Docs for more information on proto_can_message_fields. Also visit the auto-generated Nanopb header file for reference.

At this point data bytes are encoded in a serialized format and stored in the serial_data_packet_s memory structure.

Decoding

To verify the integrity of encoded data, we decode the encoded packet. First, we create a stream that reads from the encoded buffer.

pb_istream_t stream = pb_istream_from_buffer(packet->buffer, packet->bytes_written);

Then we decode the encoded message by using Nanopb's decoding API such that the proto message (ie. proto_can_message* proto_msg) is re-populated.

pb_decode(&stream, proto_can_message_fields, proto_msg)

Finally, we re-populate the CAN message using the newly populated proto_msg ie. Copy decoded data to CAN message.

  can_msg->message_id = proto_msg->message_id;
  can_msg->bus_id = proto_msg->bus_id;
  can_msg->timestamp_ms = proto_msg->timestamp_ms;
  can_msg->data = proto_msg->data_byte;

Example - 2 : Using Callbacks for Nanopb

A CAN message usually has a 8 byte data field (if using CAN-FD the data field is 64 bytes). Thus we modify the CAN message in the proto file such that the CAN proto message can encode 8 bytes of data at once.

// ex2/ex2.proto
message proto_can_message {
  required int32 message_id = 1; // Standard 11-bit
  required int32 bus_id = 2;     // The bus number which received the CAN message  
  required int64 timestamp_ms = 3;
  required bytes data_byte = 4; // Varaible number of bytes
}

Note: The int32 data type for data_byte member has changed to bytes data type, which is used to allocate variable-length storage for message fields. Refer the Scalar Value Types section for more context on data types.

Compiling proto files and generation of C header(.h) file

On compiling the .proto file again and generating the Nanopb header (ex2.npb.h) file the Proto message C structure proto_can_message is modified as follows:

/* Struct definitions */
typedef struct _proto_can_message {
    int64_t timestamp_ms;
    int32_t bus_id;
    int32_t message_id;
    pb_callback_t data_byte;
/* @@protoc_insertion_point(struct:proto_can_message) */
} proto_can_message;

Note the data type of data_byte is changed from to int32_t to pb_callback_t type. Nanopb callbacks are explained next.

Callbacks

Callbacks are used when the members of a message have variable length and storage is not statically allocated for it. For example if a proto message (in a .proto file) contains a string member such as string name, instead of generating char *name Nanopb generates the variable name of pb_callback_t (ie. 'pb_callback_t name') datatype. This allows the user to allocate variable name with any number of chars using a custom callback function.

Thus, members of a Proto message are generated as pb_callback_t datatypes for variable-length arrays/strings and repeated member messages(This is demonstrated below).

The pb_callback_t structure is defined as follows:

typedef struct _pb_callback_t pb_callback_t;
struct _pb_callback_t {
    union {
        bool (*decode)(pb_istream_t *stream, const pb_field_iter_t *field, void **arg);
        bool (*encode)(pb_ostream_t *stream, const pb_field_iter_t *field, void * const *arg);
    } funcs;

    void *arg;
};

The pb_callback_t structure consists of two members:

For more information on callbacks in Nanopb visit here.

Encoding a callback message

Let's encode a single CAN message instantiated as follows:

can_message_s can_msgs_to_encode = {.message_id = 111U, .timestamp_ms = 5U, .dlc = 8U, .bus_id = 2};
memset(can_msgs_to_encode.data, 0x0E, sizeof(can_msgs_to_encode.data));

Here, the data field of CAN message has 8 bytes, and each byte is set to the value 0x0E.

Inside the encode_packet function proto_can_message *proto_msg (ie. The generated Proto C Structure) is populated as before from the can_message_s *can_msg. Since data_bytes member is of pb_callback_t type it is populated as follows:

size_t encode_packet(proto_can_message *proto_msg, serial_data_packet_s *packet, can_message_s *can_msg) {

  /* Fill the proto packet */
  proto_msg->timestamp_ms = can_msg->timestamp_ms;
  proto_msg->bus_id = can_msg->bus_id;
  proto_msg->message_id = can_msg->message_id;
  // `pb_callback_t` member
  proto_msg->data_byte.arg = can_msg;
  proto_msg->data_byte.funcs.encode = &callback_encode_can_bytes;

  /* Create a stream that will write to our buffer. */
  pb_ostream_t stream = pb_ostream_from_buffer(packet->buffer, packet->max_buffer_size);
}

We then call Nanopb's pb_encode() function. The pb_encode() will internally call the callback registered above to encode all the data bytes inside the CAN message.

  if (!pb_encode(&stream, proto_can_message_fields, proto_msg)) {
    printf("Encoding failed: %s\n", PB_GET_ERROR(&stream));
    return 1;
  }

The strucutre of the callback function is as follows:

static bool callback_encode_can_bytes(pb_ostream_t *ostream, const pb_field_t *field, void *const *arg) {
  bool is_success = true;

  if ((NULL == arg) || (NULL == *arg)) {
    is_success = false;
  } else {
    const can_message_s *const can_message = (const can_message_s *)(*arg);
    const uint8_t *const can_data_bytes = (const uint8_t *)can_message->data;
    const size_t can_message_byte_count = can_message->dlc;

    if (!pb_encode_tag_for_field(ostream, field)) {
      is_success = false;
    }
    if (!pb_encode_string(ostream, can_data_bytes, can_message_byte_count)) {
      is_success = false;
    }
  }

  return is_success;
}

We deference the arg pointer which points to the array of CAN messages, get the count of CAN messages inside the array and encode all bytes at once using Nanopb's pb_encode_string API. pb_encode_tag_for_field starts a field in the Protocol Buffers binary format. More information here.

Decoding

Decoding the packet is similar to the first example however, we make use of callback structure to decoded multiple CAN bytes.

The callback_t structure members are assigned the pointer to callback function and CAN message data structure which will hold the decoded CAN message. The Nanopb's pb_decode API is then called.

bool decode_packet(proto_can_message *proto_msg, serial_data_packet_s *packet, can_message_s *can_msg) {
  ....

  /* Create a stream that reads from the buffer. */
  pb_istream_t stream = pb_istream_from_buffer(packet->buffer, packet->bytes_written);

  proto_msg->data_byte.arg = can_msg;
  proto_msg->data_byte.funcs.decode = &callback_decode_can_bytes;

  pb_decode(&stream, proto_can_message_fields, proto_msg);
  ....
}

The Callback to decode multiple CAN bytes reads all the bytes at once using pb_read API. Here, pb_byte_t pb_bytes is essentially a byte array. istream->bytes_left argument to pb_read API informs the Nanopb how many bytes are left to be read.

static bool callback_decode_can_bytes(pb_istream_t *istream, const pb_field_t *field, void **arg) {
  bool is_success = true;

  can_message_s *can_message = (can_message_s *)(*arg);

  // Read 8 bytes
  pb_byte_t pb_bytes[8 + 1] = {0};

  // With `pb_read` NanoPB API, we can directly read the bytes for string or byte types
  if (!pb_read(istream, pb_bytes, istream->bytes_left)) {
    is_success = false;
    printf("pb_read() failed while reading CAN data bytes");
  }
  // Copy decoded stream to CAN message data bytes
  memcpy(can_message->data, pb_bytes, sizeof(can_message->data));

  return is_success;
}

Example - 3 : Nested Callback Structures

Consider a scenario when we need to send multiple CAN frames, each frame containing multiple data bytes and some other information (for eg. software version information) to the cloud in the form of encoded data bytes.

For this we need to define the following .proto file:

// ex3/ex3.proto

syntax = "proto2";

message proto_can_message {
  required int32 message_id = 1; // Standard 11-bit
  required int32 bus_id = 2;     // The bus number which received the CAN message 
  required int64 timestamp_ms = 3;
  required bytes data_byte = 4; // Usually 8 data bytes
}

message version_info{
  required int32 major_version = 1;
  required int32 minor_version = 2;
}

message proto_message{
  required string sw_version = 1;
  required version_info release_version = 2;
  repeated proto_can_message can_msgs = 3;
}

.options file

Using generator options, we can set maximum sizes for fields in order to allocate them statically. The preferred way to do this is to create an .options file with the same name as your .proto file:

proto_message.sw_version  type:FT_STATIC
proto_message.sw_version  max_size:6 //Also includes space for `NULL` character

// Prevents mangling of names for global enums
*                                                                        long_names:false

In the .options file above we fix the size of proto_message.sw_version message member in the .proto file to 6 characters.

For more information on Proto .options file in Nanopb visit here.

Compiling proto files and generation of C header(.h) file

For Bazel users, we modify the BUILD file from the example-1 by adding ex3.options to the data attribute of proto_library rule.

# ex3/BUILD

proto_library(
    name = "ex3_proto",
    srcs = ["ex3.proto"],
    data = ["ex3.options"],
)

cc_nanopb_library(
    name = "ex3_c_proto",
    deps = [
        ":ex3_proto",
    ],
)

Building the Nanopb target we generate the (ex3.npb.h) file.

$ bazel build //shared/temp_work/nanopb-examples/ex3:ex3_c_proto

bazel-bin/shared/temp_work/nanopb-examples/ex3/ex3.npb.h
typedef struct _proto_can_message {
    int32_t message_id;
    int32_t bus_id;
    int64_t timestamp_ms;
    pb_callback_t data_byte;
/* @@protoc_insertion_point(struct:proto_can_message) */
} proto_can_message;

typedef struct _version_info {
    int32_t major_version;
    int32_t minor_version;
/* @@protoc_insertion_point(struct:version_info) */
} version_info;

typedef struct _proto_message {
    char sw_version[6];
    version_info release_version;
    pb_callback_t can_msgs;
/* @@protoc_insertion_point(struct:proto_message) */
} proto_message;

So we need to have 2 callback functions:

Encoding

First, we populate the software release information and sotware version string in the proto_message *proto_msg using the following function:

void populate_version_info(proto_message *proto_msg, int32_t major_version, int32_t minor_version,
                           const char *sw_version) {
  strcpy(proto_msg->sw_version, sw_version);
  proto_msg->release_version.major_version = major_version;
  proto_msg->release_version.minor_version = minor_version;
}

Then we start by passing N number of CAN messages to encode_packet function. The can_msg_count variable passes the number of CAN messages in the can_msg array.

size_t encode_packet(proto_message *proto_pkt, serial_data_packet_s *packet, can_message_s *can_msg,
                     size_t can_msg_count)

We register the array of CAN messages to be encoded to the arg member of pb_callback_t structure along with the callback_encode_can_messages callback function. We then pass the entire proto_pkt to the Nanopb's pb_encode API for encoding.

  proto_pkt->can_msgs.arg = (void *)(&encode_can_messages_callback_parameter);
  proto_pkt->can_msgs.funcs.encode = &callback_encode_can_messages;

  /* Now encode the message! and check of error */
  if (!pb_encode(&stream, proto_message_fields, proto_pkt)) {
    printf("Encoding failed: %s\n", PB_GET_ERROR(&stream));
    return 1;
  }

The callback function callback_encode_can_messages is called internally by pb_encode function.

bool callback_encode_can_messages(pb_ostream_t *ostream, const pb_field_t *field, void *const *arg)

We then deference the array of CAN messages passed into the void *const *arg argument and iteratively encode individual CAN messages. Since the pb_callback_t can_msgs is a member field of proto_message we use the Nanopb's pb_encode_submessage() API to encode individual CAN messages.

bool callback_encode_can_messages(pb_ostream_t *ostream, const pb_field_t *field, void *const *arg) {
  bool is_success = true;

  if ((NULL == arg) || (NULL == *arg)) {
    is_success = false;
  } else {
    const pb_callback_parameter_s *callback_parameter = (const pb_callback_parameter_s *)(*arg);
    can_message_s *can_msgs = callback_parameter->can_msgs;
    size_t msg_count = callback_parameter->can_msg_count;

    for (size_t count = 0; count < msg_count; count++) {
      proto_can_message proto_msg_can = proto_can_message_init_zero;

      proto_msg_can.timestamp_ms = can_msgs[count].timestamp_ms;
      proto_msg_can.bus_id = can_msgs[count].bus_id;
      proto_msg_can.message_id = can_msgs[count].message_id;

      proto_msg_can.data_byte.arg = (void *)(&can_msgs[count]);
      proto_msg_can.data_byte.funcs.encode = &callback_encode_can_bytes;

      /* This encodes the header for the field, based on the constant info from pb_field_t. */
      if (!pb_encode_tag_for_field(ostream, field)) {
        is_success = false;
      }

      /* This encodes the data for the field, based on our can_message structure. */
      if (!pb_encode_submessage(ostream, proto_can_message_fields, &proto_msg_can)) {
        is_success = false;
      }
    }
  }
  return is_success;
}

Nanopb's pb_encode_submessage() function will internally call callback_encode_can_bytes callback function to encoded individual CAN data bytes.

static bool callback_encode_can_bytes(pb_ostream_t *ostream, const pb_field_t *field, void *const *arg) {
  bool is_success = true;

  if ((NULL == arg) || (NULL == *arg)) {
    is_success = false;
  } else {
    const can_message_s *const can_messages = (const can_message_s *)(*arg);
    const uint8_t *const can_bytes = (const uint8_t *)can_messages->data;
    const size_t can_message_byte_count = can_messages->dlc;

    if (!pb_encode_tag_for_field(ostream, field)) {
      is_success = false;
    }
    if (!pb_encode_string(ostream, can_bytes, can_message_byte_count)) {
      is_success = false;
    }
  }

  return is_success;
}

Thus subsequently all the CAN messages are encoded.

Decoding

Now to decode the Array of CAN messages we reverse the encoding process. We pass an empty array of CAN message which will hold the decoded data from the serial_data_packet_s *packet data structure.

bool decode_packet(proto_message *proto_msg, serial_data_packet_s *packet, can_message_s *decoded_can_messages)

To decode the array of messages we register the callback decode_callback_can_messages to the pb_callback_t structure can_msgs and also pass the empty CAN message array to store the decoded CAN data. We then call Nanopb's pb_decode() API.

  proto_msg->can_msgs.arg = (void *)decoded_can_messages;
  proto_msg->can_msgs.funcs.decode = &decode_callback_can_messages;

  /* Now we are ready to decode the message and Check for errors... */
  if (!pb_decode(&stream, proto_message_fields, proto_msg)) {
    printf("Decoding failed: %s\n", PB_GET_ERROR(&stream));
    return 0;
  }

The decode_callback_can_messages is invoked for EACH repeated message. This callback uses Nanopb's pb_decode() function to decode a single CAN message.

static bool decode_callback_can_messages(pb_istream_t *istream, const pb_field_t *field, void **arg) {
  bool is_success = true;

  if ((NULL == arg) || (NULL == *arg)) {
    is_success = false;
  } else {
    can_message_s *can_msgs_decoded = (can_message_s *)(*arg);

    // Attempt to decode this proto message
    proto_can_message proto_can_message = proto_can_message_init_zero;

    // When the repeated data bytes are decoded, then invoke the callback: callback_decode_can_bytes
    proto_can_message.data_byte.arg = (void *)can_msgs_decoded;
    proto_can_message.data_byte.funcs.decode = callback_decode_can_bytes;

    if (!pb_decode(istream, proto_can_message_fields, &proto_can_message)) {
      is_success = false;
    }

    can_msgs_decoded->bus_id = proto_can_message.bus_id;
    can_msgs_decoded->message_id = proto_can_message.message_id;
    can_msgs_decoded->timestamp_ms = proto_can_message.timestamp_ms;

    /* Next time the callback is invoked, the argument pointer will point to the next message
     * Reference: 'protobuf.live_logs.arg = (void*) decoded_can_messages;' in the code below
     */
    *arg += sizeof(can_message_s);
  }
  return is_success;
}

decode_callback_can_messages() function internally call the callback_decode_can_bytes callback as illustrated in the second example to decode the 8 bytes of CAN message's data field.

/**
 * With NanoPB API, we can directly read the bytes for string or byte types
 */
static bool callback_decode_can_bytes(pb_istream_t *istream, const pb_field_t *field, void **arg) {
  bool is_success = true;

  if ((NULL == arg) || (NULL == *arg)) {
    is_success = false;
  } else {
    can_message_s *can_messages = (can_message_s *)(*arg);

    // Read 8 bytes
    pb_byte_t pb_bytes[8 + 1] = {0};

    if (!pb_read(istream, pb_bytes, istream->bytes_left)) {
      is_success = false;
      printf("pb_read() failed while reading CAN data bytes");
    }
    // Copy decoded stream to CAN message data bytes
    memcpy(can_messages->data, pb_bytes, sizeof(can_messages->data));
  }

  return is_success;
}

External Resources