Garden133 radio communication
Posted on July 3, 2025 • 10 min read • 1,957 wordsRadio link and protocol design for the Garden133 system.

Garden133 project sensor devices sample soil moisture levels in the yard and garden. They communicate this information to our home automation system, where it can be used to avoid unnecessary watering.
Introducing Garden133 gives a project overview, and I’ve also written about the project hardware. This post is about how the sensor units send information to a server inside the home. They use LoRa1 radios to send packets of data, using a custom protocol I designed for this project, via a base station also created for this project.
Requirements
The sensor units of the system live in the garden or yard and are battery-powered. This means:
- They should have a low-power design to preserve battery energy. This means that radio communication should not require much power.
- They need to work outside the range of our home WiFi.
However, soil moisture levels do not need to be sampled very often, and the level is just a single number. So there is no need for substantial communication bandwidth. We need to transmit a little bit of data, over longer distances, using little power. This is exactly what LoRa is designed for.
LoRa radio
The name LoRa stands for “long range”. It is a physical proprietary radio communication technique specialized for long range, low bandwidth, low power radio communication for Internet of Things (IoT) applications.
LoRa is nearly the opposite of WiFi, which tries to maximize bandwidth, typically over shorter distances, with less concern about power efficiency. WiFi is great for streaming video. LoRa can’t do that at all.
In the words of Vit Prajzler,
LoRa is a digital spread spectrum modulation, more specifically, a chirp spread spectrum modulation.
Instead of Amplitude Modulation (e.g., AM radio) or Frequency Modulation (e.g., FM radio), LoRa uses a “chirp modulation” which sends very distinct signals which vary in frequency and are easy to detect and decode amidst a lot of noise. This allows it to be work even with low power signals over large distances with a bunch of “stuff” (trees, houses, cars, etc…) in the direct path between the transmitter and receiver.
LoRa is often used for transport of the LoRaWAN protocol. LoRaWAN is overkill for the perspective of Garden133, however, which needs only simple point-to-point communication. Since LoRa transports data in packets, I defined my own data format for the packet contents.
I first started experimenting with LoRa by going through this post from Random Nerd Tutorials. The HopeRF RFM95W modules they document were fairly inexpensive (about $4 on AliExpress), and work well.

There are other options, including modules that integrate ESP32 and LoRa hardware. But as I eventually designed my own board and I really wanted to optimize the power system, I was happy to do the integration myself.
LoRa settings
I’m in the US, so I try to follow FCC standards for LoRaWAN communications. I don’t know whether this is necessary for LoRa radios that are not part of a LoRaWAN network, but I figure it is at least friendly to follow the guidelines. In the US, the “dwell time” for packets should not exceed 400 ms (see Regional Limitations of RF Use in LoRaWAN). This limits the size of packets which can be sent.
Packets used by the Garden133 system tend to be around 100 bytes. The LoRaWAN Airtime Calculator can be used to see what LoRa parameters are compatible with a given payload size. The Garden133 software checks whether the size of packets it wants to send are legal based on the selected spreading factor and bandwidth, which will be explained below.
Because I am in the US, I use the 915MHz band for communication. A LoRa spreading factor in the range 7-12 can be selected. A greater spreading factor results in a lower communication bit rate, but more reliable communication and longer range. Communication bandwidth can be either 125kHz or 500kHz. A higher bandwidth allows you to send more data in the same amount of time. If you choose a large spreading factor and a low bandwidth, it may not be possible to transmit any payload data in under 400 ms.
I found that if I chose a spreading factor of 11 and a bandwidth of 500kHz, I can send the packets used by Garden133 within the 400 ms dwell time, and they are received reliably. Other countries use different frequency bands, and there will be different regulations for how the radios can be used.
With the RFM95W radio module, you can also select a “Sync word” on the sender and receiver side, and the result is supposed to be that the receiver only receives packets from a sender with the same Sync word. So far, I have not gotten this feature to work.
I did an informal test of the range of the radio in my neighborhood. With the LoRa base station inside my house (on the ground floor, before I moved it to the attic), the maximum range of the radio link was around 150m, in a semi-urban neighborhood with many homes and trees.
Protocol
Packet format
I tried to make a very general packet format. It serves a number of functions at the cost of some space in the packets. A packet:
- Starts with a 4-byte “magic” sequence (“og3p”), so a receiver can verify it is parsing the kind of packet it expects.
- Contains a format version number, so the format can evolve the format over time without packets being misinterpreted.
- Has a two-byte sequence number so the receiver can know if packets have been dropped and how many were missed.
- Can contain multiple messages. Each message has a type-ID to indicate the kind of data the packet contains.
This packet format is not specific to LoRa, and turned-out to be a bit heavy-weight based on the size of packets I found I can actually send. It adds an overhead of 16-20 bytes for a single message, which is a fair chunk of the bytes that can be transmitted. I may replace this with an alternative, lighter-weight, single-message packet format.
Code to read and write these packets are part of the og3 library.
Message format
Inside the packet, I defined a message format for the actual information to be transmitted. The message format has two main jobs:
- To describe the device and the sensor readings it provides.
- To transmit the sensor readings.
The fact that the satellite sensor can describe itself means that a new kind of sensor device can be added to an existing network with no manual registration. Simply turn on the device within range of the base station, and it will automatically appear in Home Assistant.
The device description part of the message includes a:
- Unique 32-bit ID number (hashed from the ESP32 WiFi MAC address, which should be different for every individual ESP32 module),
- 32-bit manufacturer code (massive overkill, because I’m the only “manufacturer” using this protocol at this time),
- Device name (a string up to 15 characters), and
- Hardware and software version numbers.
For each sensor reading, the description includes:
- A name (string) for the reading,
- An ID number for the reading,
- An id-code for the type of reading,
- Whether reading is an integer or floating-point value, and
- The measurement units.
After the device description has been sent, a message with sensor reading need only include:
- The device ID number, and
- For each sensor reading,
- The ID number
- The numerical value.
So messages with only sensor values are very compact.
Protocol Buffer encoding
To translate the message information into the actual bytes to be transmitted, I chose to use Protocol Buffer2 serialization. This is a very widely used method, developed by Google. There are a number of implementations which are suitable for use in firmware, including Nanopb, which is what I used for Garden133.
To use Protobuf encoding, you write a definition for your message in a special “.proto”
file, and then compile it into support code in the language you want.
For this project, I used Nanopb to compile the Protobuf message definition into C code
which I can include into the satellite and base station firmware.
This code is distributed in the
og3x-satellite
GitHub project.
The actual message definition file is
satellite.proto
.
Here is a snippet:
// Submessage for communicating basic information about the device with the given id.
message Device {
// The device ID is a unique value found by hashing the device WiFi MAC address.
uint32 id = 1;
uint32 manufacturer = 2;
string name = 3 [ (nanopb).max_length = 15 ];
Version hardware_version = 4;
Version software_version = 5;
}
message Packet {
uint32 device_id = 1;
Device device = 2;
repeated FloatSensorReading reading = 3 [ (nanopb).max_length = 80, (nanopb).max_count = 8 ];
repeated IntSensorReading i_reading = 4 [ (nanopb).max_length = 80, (nanopb).max_count = 8 ];
repeated Sensor sensor = 5 [ (nanopb).max_length = 120, (nanopb).max_count = 8 ];
}
The device
and sensor
fields are only populated when the device description is being
communicated.
If you follow some guidelines in how you define and update Protobuf messages, you can preserve forward and backward compatibility of messages while writing new versions of firmware.
Satellite startup and broadcast strategy
When a satellite unit first starts, it sends its full device description. Because of the size limitation on individual LoRa packets, it does this via a sequence of packets. The sensor packs as much information as can fit into each packet, while fitting within the 400 ms maximum dwell time. It pauses 15 seconds after sending each of these start-up packets.
Garden133 currently provides 10 sensor readings. With the LoRa settings I am currently using, it takes 4 packets to fully describe the device.
After this startup communication, the device repeatedly sleeps for 5 minutes then wakes and sends a short packet with all the actual sensor readings.
The communication is all one-way: the base station never transmits to the satellites. The satellite doesn’t know whether the base station actually received all the device/sensor description packets. For this reason, and because the base station could be reset at any time (e.g., power interruption or firmware update), the device updates its self-description every hour. It does this by adding one sensor description in turn to each sensor value packet. The device description is sent along with the first sensor description.
In this way, if you reset the base station, it should regain full knowledge of all the satellite sensor units within an hour and data will continue to flow. This is fine for a soil moisture sensor where values don’t change quickly and stakes are low. For other kinds of sensor, a more aggressive strategy could be more appropriate.
Conclusion
The Garden133 project uses a LoRa radio link with a custom protocol to provide reliable, low-power communication from sensor units in the yard and garden to a base station in the house. To satisfy FCC regulations, the sensor unit determines the maximum message length that can be legally transmitted (in the US) given the chosen set of LoRa communication settings.
Sensor units can be added to the system simply by deploying them in the yard. With the sensor self-description capability of the protocol and the software in the base station, new units automatically show-up in Home Assistant.
References
LoRa (the name stands for “long range”) is a physical proprietary radio communication technique ( Wikipedia, Semtech). It is specialized for long range, low bandwidth, low power radio communication for Internet of Things (IoT) applications. ↩︎
Protocol Buffer or “Protobuf”, is Google’s language-neutral, platform-neutral, extensible mechanism for serializing structured data. Compared to json or XML, it is much more compact, but it is not human-readable in the same way. ↩︎