Nanopb
- 1 Introduction
- 2 Getting started
- 3 Installation and Setup
- 3.1 For Sibros Employees:
- 3.2 For others:
- 4 Implementation and Examples
- 5 External Resources
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:
Google's Documentation: A great tutorial with simple examples and common API explanation.
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);
}
pb_ostream_t pb_ostream_from_buffer(pb_byte_t *buf, size_t bufsize)
constructs an output stream for writing data bytes into a memory buffer. It uses an internal callback function that stores the pointer in stream state field.pb_ostream_t stream
will hold the output stream buffer.
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;
Visit Nanopb Docs for more reference on
pb_istream_from_buffer
.Visit Nanopb Docs for more reference on
pb_decode
.
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:
Union of Function pointers: This member holds a callback function for processing variable length member of a message.
void *arg
: It is used to pass a pointer to the data structure which is processed by the callback function. If the function pointer isNULL
, the corresponding message field will be skipped.
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:
proto_msg->data_byte.arg = can_msg
: Thecan_msg
can now be passed as an argument to callback function and it's variable amount of data bytes (8 in this case) can be encoded.proto_msg->data_byte.funcs.encode = &callback_encode_can_bytes
: Will hold the function pointer for the callback funtion. The callback function definition is described below.
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;
}
The
.proto
file definesproto_can_message
message as before.version_info
message is used to encode the release version information in integer data format.proto_message
wrapsversion_info
message, multiplecan_msgs
andsw_version
(ie. software version) in string format.proto_can_message can_msgs
member ofproto_message
has been declared asrepeated
. This allows us to encoded muliple CAN frames within a single proto message.
.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:
First callback function
callback_encode_can_messages
will encode multiple (variable amount of) CAN messagesSecond callback function
callback_encode_can_bytes
will encode multiple data bytes of the CAN message.sw_version
member is also defined as a char array of 6 bytes as defined in.option
file.
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
Related content
SIBROS TECHNOLOGIES, INC. CONFIDENTIAL