Message Format
All communication in Sensor Net uses a single Protocol Buffer (protobuf) message type called SensorReport. This page explains what protobuf is, why Sensor Net uses it, and what each field in the message means.
What Is Protocol Buffers?
Section titled “What Is Protocol Buffers?”Protocol Buffers (protobuf) is a language-neutral binary serialization format developed by Google. You define your message fields in a .proto file using a simple syntax, and a code generator produces fast encoder and decoder code in your target language.
Protobuf encodes data as compact binary bytes — much smaller than JSON or plain text. This is important for Sensor Net because LoRa packets should be as small as possible to minimize airtime and maximize reliability.
Why Not Raw Bytes?
Section titled “Why Not Raw Bytes?”You could manually pack sensor data into a fixed byte layout (like a C struct), but protobuf offers several advantages:
- Self-describing and versioned. Adding a new field to the message does not break nodes running older firmware. Unknown fields are simply skipped.
- Handles encoding details. Protobuf manages endianness, variable-length integers, and optional fields automatically.
- Cross-language. The same
.protofile can generate code for C, Python, Go, Rust, and many other languages. This means the monitor application (or any other tool) can decode the data without reimplementing a custom parser.
Nanopb — Protobuf for Microcontrollers
Section titled “Nanopb — Protobuf for Microcontrollers”Standard protobuf libraries are too large for microcontrollers with limited RAM. Sensor Net uses Nanopb, a C implementation of protobuf designed for embedded systems. Nanopb generates plain C structs and encode/decode functions with no dynamic memory allocation.
The generated code lives in firmware/node/lib/proto/:
messages.pb.h— struct definitionsmessages.pb.c— encode and decode functions
The SensorReport Message
Section titled “The SensorReport Message”The message definition in proto/messages.proto:
syntax = "proto3";
enum SensorType { SENSOR_UNKNOWN = 0; SENSOR_TEMPERATURE = 1; SENSOR_PRESSURE = 2;}
message SensorReport { uint32 node_id = 1; uint32 uptime_s = 2; uint32 tx_count = 3; SensorType sensor_type = 4; float value = 5; string firmware_version = 6; uint32 msg_id = 7; uint32 hops = 8;}Field Details
Section titled “Field Details”| Field | Type | Example | Description |
|---|---|---|---|
node_id | uint32 | 0xABCD1234 | Lower 32 bits of the ESP32’s factory MAC address. Unique per chip. |
uptime_s | uint32 | 3600 | Seconds since the node last booted. Resets to 0 on reboot. |
tx_count | uint32 | 720 | Total number of reports this node has sent. Useful for detecting missed packets. |
sensor_type | enum | SENSOR_TEMPERATURE | What kind of sensor produced this reading. |
value | float | 23.45 | The actual measurement (degrees C or hPa). |
firmware_version | string | "2.0.0" | Encoded as a max 16-byte fixed string (set by a nanopb option). |
msg_id | uint32 | 0xABCD00A3 | Unique packet identifier for deduplication. Computed as node_id XOR sequence_counter. |
hops | uint32 | 1 | How many mesh relay hops this packet has traveled. 0 means received directly. |
Message ID Generation
Section titled “Message ID Generation”The msg_id is generated by XOR-ing the node’s ID with a monotonically increasing sequence counter:
return nodeId ^ (++_seqCounter);This guarantees uniqueness because:
- Two different nodes with the same sequence number XOR with different node IDs, producing different msg_ids.
- The same node generates different msg_ids on each call because the counter increments.
Wire Size
Section titled “Wire Size”Protobuf uses variable-length integer encoding and skips fields with zero/default values. A typical temperature report with all 8 fields encodes in roughly 22 to 30 bytes. This compares favorably to a raw C struct of 28 bytes, but with the added benefits of self-description and forward compatibility.
The Nanopb Options File
Section titled “The Nanopb Options File”The file proto/messages.options contains Nanopb-specific constraints:
SensorReport.firmware_version max_size:16This tells Nanopb to allocate a fixed 16-byte char array for the firmware version string instead of using dynamic memory. This is essential for microcontroller environments where heap allocation should be avoided.
Encoding and Decoding in Firmware
Section titled “Encoding and Decoding in Firmware”In the firmware, encoding a report looks like:
SensorReport report = SensorReport_init_zero;report.node_id = nodeId;report.uptime_s = millis() / 1000;report.tx_count = txCount;report.sensor_type = SensorType_SENSOR_TEMPERATURE;report.value = sensorValue;// ... set other fields ...
uint8_t buffer[128];pb_ostream_t stream = pb_ostream_from_buffer(buffer, sizeof(buffer));pb_encode(&stream, SensorReport_fields, &report);// stream.bytes_written contains the encoded lengthDecoding an incoming packet is the reverse:
SensorReport report = SensorReport_init_zero;pb_istream_t stream = pb_istream_from_buffer(data, length);pb_decode(&stream, SensorReport_fields, &report);// report.node_id, report.value, etc. are now populatedRegenerating the Code
Section titled “Regenerating the Code”If you modify messages.proto (for example, to add a new sensor type or field), regenerate the C bindings:
pip install nanopb./scripts/generate_proto.shThe updated .pb.h and .pb.c files are placed in firmware/node/lib/proto/.