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:
Protocol Buffers messages are defined in a proto file
as follows:
This .proto
file is used to generate the Nanopb headers(.h) and source files(.c) using the python script provided by Nanopb.
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:
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.
Building the target we obtain the Nanopb generated file.
Non-Bazel users, can generate the header (.h) file using the python script provided by Nanopb:
The Nanopb generated header (.h) file will create an proto message equivalent C structure as follows:
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.
Note: serial_data_packet_s* packet
holds the memory buffer for Nanopb encoded data bytes:
Now we can populate the Nanopb generated data strucutre proto_msg
with our CAN message to be serialized as follows:
We then create an output stream for writing the data to the buffer:
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.
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.
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.
Finally, we re-populate the CAN message using the newly populated proto_msg
ie. Copy decoded data to CAN message.
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.
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:
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:
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:
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.
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.
The strucutre of the callback function is as follows:
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.
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.
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:
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:
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.
Building the Nanopb target we generate the (ex3.npb.h) file.
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:
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.
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.
The callback function callback_encode_can_messages
is called internally by pb_encode
function.
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.
Nanopb's pb_encode_submessage()
function will internally call callback_encode_can_bytes
callback function to encoded individual CAN data bytes.
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.
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.
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.
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.
External Resources
SIBROS TECHNOLOGIES, INC. CONFIDENTIAL