LoRa for long range
Kilometres from a coin cell, through buildings, with no infrastructure at all. The price is bandwidth measured in bytes per minute and a legal duty-cycle limit that decides your design before your code does.
One dial, two effects
Design for the duty cycle first
Work out how often the device must report, multiply by the airtime at the spreading factor your range needs, and check it against 1% of an hour. If it does not fit, the answer is not a better antenna — it is fewer, smaller messages.
A packed binary struct of six bytes at SF9 is about 60 ms on air. The same reading as JSON is four times that, for no extra information.
LoRa and LoRaWAN are not the same thing
LoRa is two radios talking to each other. Matching settings, a sync word, done. This is what most projects want.
LoRaWAN adds gateways, a network server, join procedures and keys, so your device can reach the internet through infrastructure somebody else runs. Much more capable, much more to set up, and unnecessary if the receiver is your own board on your own hill.
The code
The ESP32 has no LoRa radio - this is an SX1276 or SX1262 module on SPI. Frequency, spreading factor and sync word must match at both ends or the packets simply do not exist for the receiver.
#include <SPI.h>
#include <LoRa.h>
typedef struct { uint8_t id; int16_t temp; uint16_t mv; } Packet;
void setup() {
Serial.begin(115200);
LoRa.setPins(18, 14, 26); // CS, reset, DIO0
if (!LoRa.begin(868E6)) { // check your region
Serial.println("no radio");
return;
}
LoRa.setSpreadingFactor(9); // 7 fast and near, 12 slow and far
LoRa.setSignalBandwidth(125E3);
LoRa.setSyncWord(0x12); // must match the receiver
Packet p = { 3, 2140, 3820 };
LoRa.beginPacket();
LoRa.write((uint8_t *)&p, sizeof(p));
LoRa.endPacket();
}
void loop() {}868 MHz in Europe, 915 MHz in North America, 433 MHz in some regions. Transmitting on the wrong band is illegal, not merely unreliable.
The receiving side. RSSI and SNR are worth logging from the first day - they tell you how much margin you have long before a link starts failing.
from machine import SPI, Pin
from sx127x import SX127x # any of the community drivers
import struct
spi = SPI(2, baudrate=10_000_000, sck=Pin(5), mosi=Pin(27), miso=Pin(19))
lora = SX127x(spi, cs=Pin(18), reset=Pin(14),
frequency=868E6, spreading_factor=9, sync_word=0x12)
while True:
pkt = lora.receive()
if pkt:
node, temp, mv = struct.unpack('<BhH', pkt)
print(node, temp / 100, mv, 'rssi', lora.rssi())Send binary, not text. A JSON object is four times the airtime of the same numbers packed into a struct, and airtime is the resource you are rationing.
When it does not work
Frequency, spreading factor, bandwidth and sync word all have to match. Any one of them wrong gives silence rather than errors - LoRa has no way to report a packet it did not decode.
Antenna. A module transmitting without one can damage itself, and a coil of wire at the wrong length is close to no antenna. Get the length right for your band and keep it clear of the board.
You hit the duty-cycle limit and the stack is enforcing it, or the local regulator is. 1% at 868 MHz means roughly 36 seconds of transmission per hour.
That is LoRaWAN, not LoRa - gateways, join keys and a network server. Two boards talking directly is much simpler and covers most projects.
Every radio on the last five pages was chosen for battery life. The next chapter is where that battery life actually comes from.
Deep sleep and wake sources →Edit this page — content/esp32/lora-for-long-range.mdx
Discuss this article
Ask about this page. The answer stays here, on the page it belongs to, for whoever hits the same wall next.