To work with LoRaWAN on a PLC, you need four things: a LoRa radio on the PLC, a LoRaWAN stack (in the PLC program or inside the radio module), a gateway in range, and a network server such as The Things Network that knows the device keys and forwards the data to your application. The PLC sends small packets, the gateway receives them, and the network server checks, decrypts and delivers them.
This guide follows the full path with an M-Duino from the LoRa family (Arduino PLC) and the MCCI LoRaWAN LMIC library: the choice between OTAA and ABP, the device registration in The Things Network, the pin mapping, the send cycle, the airtime limits and the payload decoding. The sections on activation, airtime and payloads apply to any LoRaWAN node, also if you don't program Arduino.
How does a PLC send data over LoRaWAN?
LoRa is the radio modulation. LoRaWAN is the network protocol on top of it: it defines the MAC layer (Media Access Control), the device activation, the security and the roles of gateways and servers. A LoRaWAN network is a star of stars: end devices talk to gateways by radio, and gateways talk to one network server over IP.
- 1 · PLCM-Duino LoRaReads its inputs, builds a small payload and sends it by radio. Here the LMIC library runs the LoRaWAN stack on the PLC.
- 2 · RadioGatewayReceives every LoRa packet in range on its channels and forwards it over Ethernet, Wi-Fi or 4G.
- 3 · NetworkNetwork serverThe Things Network checks the message integrity code and the frame counter, and removes duplicates from several gateways.
- 4 · ApplicationApplication serverDecrypts the payload and runs your payload formatter to turn bytes into values.
- 5 · Your systemSCADA, databaseGets the decoded data through an integration, for example MQTT or a webhook.
Example One possible path. Uplinks go from left to right. A downlink can only reach a Class A device in the two short receive windows after each uplink.
Every LoRaWAN 1.0.x session uses two AES-128 keys. The network session key (NwkSKey) protects the message integrity between the device and the network server. The application session key (AppSKey) encrypts the payload from the device to the application server, so gateways and the network server only see an encrypted payload.
Range depends on antenna height, terrain, obstacles and spreading factor. It goes from hundreds of metres inside buildings to several kilometres in open terrain. A higher spreading factor reaches further, but each packet takes longer on air.
OTAA or ABP: choose the activation before you write code
A LoRaWAN device needs session keys before it can send. It gets them in one of two ways. Over-The-Air Activation (OTAA) creates new session keys in a join procedure. Activation By Personalization (ABP) stores fixed session keys in the device. The keys you copy into the sketch, and the sketch itself (ttn-otaa or ttn-abp), depend on this choice.
| OTAA | ABP | |
|---|---|---|
| Values stored in the PLC | DevEUI, JoinEUI (also called AppEUI) and the root key AppKey | DevAddr, NwkSKey and AppSKey |
| Join procedure | Yes. The device sends a join request and the network answers with a join accept | No. The device can send at once |
| Session keys | New ones derived from the AppKey at every join | The same for the whole life of the device |
| Network settings | The join accept can carry the channel list and the receive settings | You set the channels and receive settings in the code |
| Frame counters after a PLC restart | A new join starts a new session with new counters | Counters restart at 0 unless you store them, and the network drops frames it has already seen |
| Good for | Installed devices and production | Short lab tests |
For a PLC in the field, use OTAA. The AppKey never travels over the air, and the device can join again after a power cut or a network change. If you use ABP, every device needs its own unique DevAddr and keys, and you must keep the frame counters in non volatile memory.
Hardware and software for the M-Duino LoRa example
- A PLC from the M-Duino LoRa family with an antenna for your band. Our example uses an M-Duino 21+ LoRa. The family is based on the Arduino Mega 2560 (ATmega2560, 256 KB flash, 8 KB SRAM), see the M-Duino LoRa family datasheet.
- Arduino IDE with the Industrial Shields boards package. In Tools, choose the board M-Duino LoRa family and your model. Follow the first steps guide if the boards are not installed yet.
- The MCCI LoRaWAN LMIC library, Industrial Shields version, from GitHub (version 6.0.1-is1 at the time of writing). It supports LoRaWAN 1.0.2 and 1.0.3. The region and the radio are set in
project_config/lmic_project_config.h: the default isCFG_eu868. For North America, change it toCFG_us915. - An account on The Things Network and a gateway in range, your own or a community gateway.
Register the PLC as an end device in The Things Network
The network server must know your device before the first join. In The Things Network console:
- Create an application. The name is free.
- Click "+ Register end device" and choose "Enter end device specifics manually".
- Frequency plan: "Europe 863-870 MHz (SF9 for RX2 - recommended)" for EU868. Choose the plan of your region if you are outside Europe.
- LoRaWAN version: "LoRaWAN Specification 1.0.2". Regional Parameters version: "RP001 Regional Parameters 1.0.2".
- JoinEUI (AppEUI): use the value from your module supplier if you have one. If not, all zeros is accepted.
- Generate the DevEUI and the AppKey, and register the device.
Then you can set the device location and a payload formatter (see below). The console shows each key with a byte order toggle (msb or lsb) and a C style array view. You'll need it in the next step.
LMIC pin mapping on M-Duino LoRa and Ardbox LoRa
Open the example from File, Examples, MCCI LoRaWAN LMIC library, and choose ttn-otaa or ttn-abp. Both sketches share the same structure. They differ in the keys at the top and in the setup.
The library talks to the LoRa radio over SPI. You must tell it which Arduino pins drive the radio chip select (NSS) and reset:
- M-Duino LoRa family: chip select on pin 12, reset on pin 13.
- Ardbox LoRa family: chip select on pin 23, reset on pin 3. Don't use pin 3 for anything else in your program, for example as an interrupt input.
The Industrial Shields version of the library doesn't need the DIO lines of the radio, so they stay as LMIC_UNUSED_PIN. Without LMIC_USE_INTERRUPTS, the library reads the radio interrupt flags over SPI every time os_runloop_once() runs. That's why your loop() must call it often and never block.
// Pin mapping for the M-Duino LoRa family
const lmic_pinmap lmic_pins = {
.nss = 12, // radio chip select
.rxtx = LMIC_UNUSED_PIN,
.rst = 13, // radio reset
.dio = {LMIC_UNUSED_PIN, LMIC_UNUSED_PIN, LMIC_UNUSED_PIN},
};
Pin 13 is the radio reset on the M-Duino LoRa. Some generic Arduino examples blink pin 13 as a status LED. Don't copy that code here, because it resets the radio.
Device keys in the sketch: OTAA and ABP
OTAA keys
Replace FILLMEIN with the values from the console. In the LMIC library, the DevEUI and the JoinEUI go in little-endian order (least significant byte first, "lsb" in the console). The AppKey goes in big-endian order ("msb" in the console).
static const u1_t PROGMEM APPEUI[8] = { FILLMEIN }; // JoinEUI, lsb
static const u1_t PROGMEM DEVEUI[8] = { FILLMEIN }; // DevEUI, lsb
static const u1_t PROGMEM APPKEY[16] = { FILLMEIN }; // AppKey, msb
With OTAA, the first call to do_send() starts the join. After the join accept, The Things Network sends the extra EU868 channels to the device, so you don't need to set them in the code.
ABP keys
For ABP, register the device with ABP activation in the console and copy the DevAddr, the NwkSKey and the AppSKey. Both keys go in big-endian order. The DevAddr is a 32-bit number, for example 0x260B1234.
static const PROGMEM u1_t NWKSKEY[16] = { FILLMEIN }; // msb
static const u1_t PROGMEM APPSKEY[16] = { FILLMEIN }; // msb
static const u4_t DEVADDR = FILLMEIN; // unique for every device
An ABP device starts with only the three default EU868 channels. The ttn-abp example adds the other channels of The Things Network after LMIC_setSession(), sets the RX2 data rate to SF9 and the uplink to SF7 at 14 dBm:
#if defined(CFG_eu868)
LMIC_setupChannel(0, 868100000, DR_RANGE_MAP(DR_SF12, DR_SF7), BAND_CENTI); // g-band
LMIC_setupChannel(1, 868300000, DR_RANGE_MAP(DR_SF12, DR_SF7B), BAND_CENTI); // g-band
LMIC_setupChannel(2, 868500000, DR_RANGE_MAP(DR_SF12, DR_SF7), BAND_CENTI); // g-band
LMIC_setupChannel(3, 867100000, DR_RANGE_MAP(DR_SF12, DR_SF7), BAND_CENTI); // g-band
LMIC_setupChannel(4, 867300000, DR_RANGE_MAP(DR_SF12, DR_SF7), BAND_CENTI); // g-band
LMIC_setupChannel(5, 867500000, DR_RANGE_MAP(DR_SF12, DR_SF7), BAND_CENTI); // g-band
LMIC_setupChannel(6, 867700000, DR_RANGE_MAP(DR_SF12, DR_SF7), BAND_CENTI); // g-band
LMIC_setupChannel(7, 867900000, DR_RANGE_MAP(DR_SF12, DR_SF7), BAND_CENTI); // g-band
LMIC_setupChannel(8, 868800000, DR_RANGE_MAP(DR_FSK, DR_FSK), BAND_MILLI); // g2-band
#else
# error Region not supported
#endif
LMIC_setLinkCheckMode(0);
LMIC.dn2Dr = DR_SF9; // The Things Network uses SF9 for RX2
LMIC_setDrTxpow(DR_SF7, 14); // uplink data rate and power
The LMIC send cycle: do_send, onEvent and os_runloop_once
The example sends the text "Hello, world!" (13 bytes) every TX_INTERVAL seconds. Three functions make the cycle work.
do_send() queues one uplink on port 1, unless a transmission is still pending:
static uint8_t mydata[] = "Hello, world!";
static osjob_t sendjob;
// Schedule TX every this many seconds (might become longer due to duty
// cycle limitations).
const unsigned TX_INTERVAL = 60;
void do_send(osjob_t* j){
// Check if there is not a current TX/RX job running
if (LMIC.opmode & OP_TXRXPEND) {
Serial.println(F("OP_TXRXPEND, not sending"));
} else {
// Prepare upstream data transmission at the next possible time.
LMIC_setTxData2(1, mydata, sizeof(mydata)-1, 0);
Serial.println(F("Packet queued"));
}
// Next TX is scheduled after TX_COMPLETE event.
}
onEvent() is not only for debugging. When the event EV_TXCOMPLETE arrives, it schedules the next do_send(). Without it, the PLC sends one packet and stops. This is a shortened version of the event handler in the library example:
void onEvent(ev_t ev) {
switch (ev) {
case EV_JOINED:
Serial.println(F("EV_JOINED"));
LMIC_setLinkCheckMode(0);
break;
case EV_TXCOMPLETE:
Serial.println(F("EV_TXCOMPLETE (includes waiting for RX windows)"));
if (LMIC.dataLen) {
Serial.print(F("Received "));
Serial.print(LMIC.dataLen);
Serial.println(F(" bytes of payload"));
}
// Schedule next transmission
os_setTimedCallback(&sendjob, os_getTime()+sec2osticks(TX_INTERVAL), do_send);
break;
default:
break;
}
}
os_runloop_once() runs the LMIC scheduler. Call it in every loop() and read your PLC inputs with millis() timers, not with delay():
void setup() {
Serial.begin(9600);
os_init(); // LMIC init
LMIC_reset(); // reset the MAC state
do_send(&sendjob); // first send (starts the join with OTAA)
}
void loop() {
os_runloop_once();
}
Airtime, duty cycle and The Things Network fair use policy
Every LoRa packet occupies the channel for a time that grows with the payload size and the spreading factor. Two limits apply to that airtime:
- Regulatory duty cycle. In the EU863-870 band, the sub-bands used by LoRaWAN channels have a 1% duty cycle (ETSI EN 300 220-2). After 1 s of transmission, the device must stay silent for 99 s on that sub-band.
- The Things Network fair use policy. On the community network, each node gets 30 s of uplink airtime per 24 hours and at most 10 downlink messages per 24 hours.
Calculation Airtime of the example message ("Hello, world!", 13 bytes of payload plus 13 bytes of LoRaWAN overhead), 125 kHz bandwidth, coding rate 4/5, 8 preamble symbols, explicit header and CRC:
| Spreading factor | Airtime per uplink | Airtime per day, 1 uplink every 60 s | Uplinks per day inside 30 s |
|---|---|---|---|
| SF7 | 61.7 ms | 88.8 s | 486 (one every 178 s) |
| SF9 | 205.8 ms | 296.4 s | 145 (one every 596 s) |
| SF10 | 411.6 ms | 592.8 s | 72 (one every 1200 s) |
| SF12 | 1646.6 ms | 2371.1 s | 18 (one every 4800 s) |
So the 60 s interval of the example is fine for a bench test, but on The Things Network it is too frequent for a device that runs every day. For a PLC in the field, send less and send smaller: a binary payload of a few bytes instead of text, and an interval that fits your spreading factor. LoRaWAN carries process data and alarms. It is not a link for fast control loops.
Payload formatter for The Things Network
The network delivers raw bytes. A payload formatter in the console turns them into fields. For this text example, choose the custom JavaScript formatter for uplinks and paste this code. It converts each byte into a character:
function decodeUplink(input) {
if (input.bytes && input.bytes.length > 0) {
const decodedChars = [];
for (let i = 0; i < input.bytes.length; i++) {
const charValue = String.fromCharCode(input.bytes[i]);
decodedChars.push(charValue);
}
return {
data: {
decodedChars: decodedChars.join('')
},
warnings: [],
errors: []
};
} else {
return {
data: {},
warnings: ["No bytes in the input"],
errors: []
};
}
}
For real PLC data, pack the values into bits and bytes in the sketch and decode them with shifts and masks in the formatter. The LoRaWAN payload bit packing example puts 11 digital inputs, 2 fault flags and a 10-bit analog value into 4 bytes.
First uplink: what to check and common errors
Open the Serial Monitor. With OTAA you should see EV_JOINING, then EV_JOINED, then Packet queued and EV_TXCOMPLETE once per interval. In the console, the Live data tab of the device shows the join accept and each uplink. With the formatter above, the decoded field shows "Hello, world!".
| Symptom | Usual cause | What to do |
|---|---|---|
Join requests in the console, but no EV_JOINED | Wrong byte order of DevEUI or JoinEUI, wrong AppKey | Copy DevEUI and JoinEUI as lsb, AppKey as msb |
| Nothing in the console | No gateway in range, or a frequency plan that doesn't match lmic_project_config.h | Check gateway coverage and use the same region in the library and in the console |
| Join accepted in the console, but the PLC doesn't receive it | The receive window opens too late or too early | Add LMIC_setClockError() after LMIC_reset() to widen the receive windows. See the Timing section of the library documentation |
| ABP device: uplinks ignored after a restart | The frame counter restarted at 0 | Store the counter, or for a lab test only, enable "Resets frame counters" in the device MAC settings |
| Fewer uplinks than expected | Duty cycle limits in the library | Use a longer interval or a lower spreading factor |
LoRaWAN on ESP32 PLC and Raspberry PLC
The network side is the same for every PLC: the same activation, keys, frequency plans, airtime limits and payload formatters. The device side changes. On the M-Duino LoRa family, the LMIC library runs the LoRaWAN stack on the PLC microcontroller. On the ESP32 PLC family, the LoRa expansion board uses a Microchip RN2483 module (EU and Asia) or RN2903 (USA and Australia), which has the LoRaWAN stack inside and takes ASCII commands over UART, according to the ESP32 PLC family datasheet. Your program sends commands like "join" and "send", and the module does the rest.
ESP32 PLC and Raspberry PLC models offer a LoRa communication board in an EU868 or a US902 version. See the ESP32 PLC family and the Raspberry PLC family, and these guides:
- How to work with LoRaWAN and an ESP32 PLC
- RN2483 LoRa library for ESP32 based PLCs
- LoRaWAN on Raspberry Pi
- FreeRTOS tasks on an ESP32 PLC with a LoRaWAN watchdog, with an hourly OTAA re-join (PLC Solution Library)
- Dual pump station with LoRaWAN telemetry on an ESP32 PLC (PLC Solution Library)
See LoRaWAN telemetry at network scale. The remote site telemetry simulator runs a town water network with ESP32 PLC RTUs over 4G and LoRaWAN. Cut a link and see store and forward, alarms and night flow leak detection.
Run the telemetry simulatorLoRa links in PLC case studies
These case studies use LoRa to bring PLC data from places without a cable:
- Concrete batching plant: an M-Duino doses each material and sends the batch records to a server by LoRa, for production, billing and failure forecasts.
- Geothermal heat pump: an M-Duino watches pressure and temperature, and LoRa is one of the links to remote sites.
- Autonomous solar panels: one PLC per group of panels reports to a master PLC over LoRa.
FAQ about LoRaWAN and PLCs
What is the difference between LoRa and LoRaWAN?
LoRa is the radio modulation that carries the bits. LoRaWAN is the network protocol on top of LoRa: device activation, AES-128 security, frame counters, gateways and network servers. Two PLCs can talk point to point with LoRa only. To use The Things Network or another LoRaWAN network, the PLC needs a LoRaWAN stack.
Should I use OTAA or ABP for an industrial PLC?
Use OTAA. It creates new session keys at every join, keeps the root key off the air and recovers after a restart. ABP is useful for short lab tests, but it needs unique fixed keys per device and frame counters stored in non volatile memory.
Why doesn't my PLC join The Things Network?
Check the byte order first: in the LMIC library, DevEUI and JoinEUI are lsb and the AppKey is msb. Then check that the region in lmic_project_config.h matches the frequency plan in the console, and that a gateway is in range.
How often can a PLC send data over LoRaWAN on The Things Network?
The fair use policy gives each node 30 s of uplink airtime per day. A 13-byte payload at SF7 takes 61.7 ms, so about 486 uplinks per day. At SF12 it takes 1646.6 ms, so about 18 per day.
Can a LoRaWAN PLC receive commands?
Yes, as downlinks. A Class A device listens only in two short receive windows after each uplink, so a command waits for the next uplink. The Things Network allows 10 downlinks per node per day. Use downlinks for set points and configuration, not for fast control.
Which frequency plan do I need?
The one of the country where the PLC is installed: EU868 in Europe and US915 in North America, for example. The radio hardware, the library region and the console frequency plan must all match. Industrial Shields LoRa boards come in versions for different bands, for example EU868 and US902.