How to Use Nanopb with Arduino and ESP32

How to Use Nanopb with Arduino and ESP32

Arduino sketches and protobuf might sound like they belong to different decades, yet the pairing makes perfect sense once a project outgrows comma separated text. Your ESP32 sensor node needs to send readings to a server that expects structured data, your BLE payload budget is tight, and suddenly nanopb on Arduino stops being exotic and starts being obvious. This guide shows you how to use nanopb with Arduino and ESP32 from start to finish: installing the library, defining a message, generating code, and encoding a SensorMessage you can ship over Serial, WiFi, or BLE. The same workflow applies to most Arduino compatible boards, with the ESP32 as our running example.

Why nanopb fits the Arduino world

Arduino boards, and ESP32 boards especially, live under real constraints. RAM is limited, the heap is shared with WiFi and Bluetooth stacks that fragment it enthusiastically, and every byte you push through a BLE characteristic costs airtime and battery. Nanopb was designed for exactly this world. Its runtime is a few small C files, it needs no dynamic allocation by default, and its wire format is dramatically smaller than JSON or hand formatted text.

There is also an interoperability dividend. The bytes your ESP32 produces are standard protobuf, so the receiving side can be a Python script, a Go service, or a cloud endpoint using ordinary protobuf tooling. Your little board speaks the same language as the big servers, with a shared .proto file as the contract.

Install nanopb from the Library Manager

The friendliest route is the Arduino Library Manager, where nanopb is published as an Arduino library. Open the Library Manager in the Arduino IDE, search for nanopb, and install it. That gives your sketches the nanopb runtime: the encoder, decoder, and common code, ready to include.

PlatformIO users get the same result through the PlatformIO library registry: add nanopb to your project’s library dependencies in platformio.ini and it joins the build automatically. Either way, remember the division of labor. The library on the board is only the runtime half. The generator half, which turns .proto files into C code, runs on your computer, so install that too using the release package or pip, as the install guide on this site describes.

Define the message and generate the code

Start on your computer with a small message suited to a sensor node:

syntax = "proto3"; message SensorMessage { uint32 device_id = 1; float temperature = 2; string label = 3; }

Beside it, write the .options file that gives the label a fixed size, for example SensorMessage.label max_size:16. Then run the nanopb generator to produce sensor.pb.c and sensor.pb.h. Copy those two generated files into your sketch folder or your PlatformIO project’s source directory, next to your main code. Inside the sketch, include the generated header and you are ready to encode.

Encode a reading in your sketch

The sketch side code looks like ordinary embedded C, because it is:

SensorMessage msg = SensorMessage_init_zero; msg.device_id = 7; msg.temperature = 21.5f; strcpy(msg.label, "roof"); uint8_t buffer[64]; pb_ostream_t stream = pb_ostream_from_buffer(buffer, sizeof(buffer)); if (pb_encode(&stream, SensorMessage_fields, &msg)) { size_t length = stream.bytes_written; }

Inside that if block, buffer holds a standard protobuf message and length says how many bytes are valid. From there the transport is your choice. Write the bytes to Serial for a tethered prototype, send them as a UDP or TCP payload over WiFi on the ESP32, or push them through a BLE characteristic. The nanopb part is done; the rest is ordinary Arduino networking.

Check the pb_encode() return value in real sketches, not just tutorials. A false almost always means the buffer was smaller than the message, and on a board you cannot attach a debugger to easily, an ignored failure becomes a very long evening.

Decode on the board, decode on the host

Plenty of Arduino projects only encode, sending telemetry up to a host that decodes with regular protobuf in Python or another desktop language. That asymmetry is fine and common, and it keeps the board’s job minimal. When the board must also receive commands, decoding on device is just as small:

pb_istream_t stream = pb_istream_from_buffer(buffer, length); SensorMessage msg = SensorMessage_init_zero; bool ok = pb_decode(&stream, SensorMessage_fields, &msg);

One warning that matters on device: pass pb_decode() the descriptor of the message type you actually received. Decoding SensorMessage bytes with a different message’s descriptor returns false or nonsense, and the bug can look like flaky radio behavior until you spot it. Keep message framing, even just a one byte type tag of your own, so the receiver always knows which descriptor to use.

ESP32 specific notes

The ESP32 runs ESP-IDF under the hood of its Arduino core, and nanopb is also available as an ESP-IDF component for projects that grow out of the Arduino layer. Knowing that path exists is useful: a prototype built in Arduino can carry its .proto files, its .options file, and its generated code straight into an ESP-IDF production project without redesigning the protocol.

On the ESP32, prefer static fields, the nanopb default, and size them honestly in .options. The WiFi and Bluetooth stacks already put the heap under pressure, and static messages keep nanopb entirely out of that fight. Your message memory becomes a fixed, known cost, visible in the generated header.

A complete mini project shape

Put together, a tidy ESP32 telemetry sketch has four moving parts: the library runtime from the Library Manager, the generated sensor.pb.c and sensor.pb.h in the sketch, a loop that reads the sensor and fills the struct, and a transport that ships stream.bytes_written bytes after each successful encode. Regenerate the message code on your computer whenever the .proto file changes, and keep the .proto and .options files in the project repo so the whole contract is versioned together.

Power and payload habits for battery boards

On a battery powered ESP32 node, the radio is the budget that matters, and message size is radio time. Protobuf already shrinks your payload compared with text, and a few habits shrink it further. Send readings as numbers, not formatted strings: a float temperature costs four bytes, its text form costs more and needs parsing at the other end. Batch several readings into one message with a repeated field, sized with max_count in your .options file, instead of waking the radio for every sample. And send on change where the application allows it, because the cheapest packet is the one you never transmit.

None of this requires heroics. It is the natural result of a typed contract: when your fields have real types and declared sizes, waste becomes visible in the .proto file itself, and visible waste tends to get fixed.

Conclusion

Nanopb on Arduino and ESP32 is a small addition with an outsized payoff: structured, compact, standard protobuf messages from boards that used to mumble comma separated text. Install the library, generate code from a .proto file with honest sizes in .options, encode with pb_encode(), and ship the bytes over whatever transport your project already uses. When something misbehaves, the fixes guide on this site maps the common nanopb errors to their causes.

Read next:

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top