Overview
LoRaWAN (Long Range Wide Area Network) is a media access control (MAC) protocol built on top of the LoRa physical layer modulation, designed for low-power wide-area networks (LPWAN). It enables battery-powered IoT devices to communicate over distances of 2-15 km in urban areas and up to 40+ km in rural line-of-sight conditions, while consuming minimal power (devices can run for years on a single battery). LoRaWAN operates in unlicensed ISM bands (868 MHz in Europe, 915 MHz in US, 923 MHz in Asia) and uses a star-of-stars topology where end devices communicate through gateways to a central network server.
The LoRaWAN architecture consists of four key components: end devices (sensors/actuators), gateways (LoRa to IP bridges), a network server (manages routing, deduplication, and MAC commands), and application servers (process device data). The protocol supports three device classes: Class A (lowest power, uplink-initiated), Class B (scheduled receive windows), and Class C (always listening, highest power). Security is built-in with AES-128 encryption using separate network and application session keys (OTAA or ABP activation). Open-source network servers like ChirpStack and The Things Network (TTN) make it accessible for community and enterprise deployments.
Installation
ChirpStack (Network Server)
# Docker-based deployment
git clone https://github.com/chirpstack/chirpstack-docker.git
cd chirpstack-docker
# Configure region
# Edit docker-compose.yml and configuration/chirpstack.toml
# Start all services
docker compose up -d
# Access web UI at http://localhost:8080
# Default: admin / admin
The Things Network (TTN)
# TTN is a public community network
# Register at https://www.thethingsnetwork.org/
# Create application and register devices via web console
# Install TTN CLI
brew install TheThingsNetwork/lorawan-stack/ttn-lw-cli
# or
snap install ttn-lw-stack
# Login
ttn-lw-cli login
LoRa Gateway Setup (RAK/SX1301)
# Install packet forwarder
git clone https://github.com/Lora-net/lora_gateway.git
cd lora_gateway
make
git clone https://github.com/Lora-net/packet_forwarder.git
cd packet_forwarder
make
# Configure gateway
# Edit global_conf.json with your frequency plan and server address
// platformio.ini
// [env:lora_sensor]
// platform = espressif32
// board = heltec_wifi_lora_32_V3
// framework = arduino
// lib_deps =
// mcci-catena/MCCI LoRaWAN LMIC library@^4.1.1
#include <lmic.h>
#include <hal/hal.h>
// OTAA keys (from TTN/ChirpStack console)
static const u1_t PROGMEM APPEUI[8] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
static const u1_t PROGMEM DEVEUI[8] = { 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08 };
static const u1_t PROGMEM APPKEY[16] = { /* 16 bytes */ };
void os_getArtEui(u1_t* buf) { memcpy_P(buf, APPEUI, 8); }
void os_getDevEui(u1_t* buf) { memcpy_P(buf, DEVEUI, 8); }
void os_getDevKey(u1_t* buf) { memcpy_P(buf, APPKEY, 16); }
LoRaWAN Concepts
Device Classes
| Class | Description | Power | Latency |
|---|
| A | Uplink-initiated, 2 short RX windows | Lowest | High |
| B | Scheduled receive windows (beacons) | Medium | Medium |
| C | Continuous receive window | Highest | Lowest |
Activation Methods
| Method | Description |
|---|
| OTAA | Over-The-Air Activation (recommended) |
| Device negotiates session keys dynamically |
| Keys: AppEUI + DevEUI + AppKey |
| ABP | Activation By Personalization |
| Pre-provisioned session keys (less secure) |
| Keys: DevAddr + NwkSKey + AppSKey |
Frequency Plans
| Region | Frequency | Channels | Max Payload |
|---|
| EU868 | 868 MHz | 8+ | 222 bytes |
| US915 | 915 MHz | 72 | 222 bytes |
| AU915 | 915 MHz | 72 | 222 bytes |
| AS923 | 923 MHz | 8+ | 222 bytes |
| CN470 | 470 MHz | 96 | 222 bytes |
| IN865 | 865 MHz | 3+ | 222 bytes |
| KR920 | 920 MHz | 8+ | 222 bytes |
Spreading Factors
| SF | Data Rate | Range | Time on Air | Max Payload |
|---|
| SF7 | 5.47 kbps | Short | 36 ms | 222 bytes |
| SF8 | 3.12 kbps | Medium | 72 ms | 222 bytes |
| SF9 | 1.76 kbps | Medium | 144 ms | 115 bytes |
| SF10 | 980 bps | Long | 288 ms | 51 bytes |
| SF11 | 440 bps | Longer | 577 ms | 51 bytes |
| SF12 | 250 bps | Longest | 1155 ms | 51 bytes |
ChirpStack Configuration
Application Setup
# Create application via API
curl -X POST http://localhost:8080/api/applications \
-H "Grpc-Metadata-Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"application": {
"name": "my-sensors",
"description": "Environmental sensors",
"tenantId": "52f14cd4-c6f1-4fbd-8f87-4025e1d49242"
}
}'
Device Profile
{
"deviceProfile": {
"name": "class-a-otaa",
"tenantId": "52f14cd4-c6f1-4fbd-8f87-4025e1d49242",
"region": "EU868",
"macVersion": "LORAWAN_1_0_3",
"regParamsRevision": "A",
"supportsOtaa": true,
"supportsClassB": false,
"supportsClassC": false,
"adrAlgorithmId": "default"
}
}
Register Device
# Register device via API
curl -X POST http://localhost:8080/api/devices \
-H "Grpc-Metadata-Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"device": {
"devEui": "0102030405060708",
"name": "temp-sensor-01",
"applicationId": "app-uuid",
"deviceProfileId": "profile-uuid",
"description": "Office temperature sensor"
}
}'
# Set OTAA keys
curl -X POST http://localhost:8080/api/devices/0102030405060708/keys \
-H "Grpc-Metadata-Authorization: Bearer $API_KEY" \
-d '{
"deviceKeys": {
"devEui": "0102030405060708",
"nwkKey": "00112233445566778899aabbccddeeff"
}
}'
TTN CLI Commands
# Login
ttn-lw-cli login
# Create application
ttn-lw-cli applications create my-app --user-id my-user
# Register device (OTAA)
ttn-lw-cli end-devices create my-app my-device \
--dev-eui 0102030405060708 \
--app-eui 0000000000000000 \
--app-key 00112233445566778899AABBCCDDEEFF \
--lorawan-version 1.0.3 \
--lorawan-phy-version 1.0.3-a \
--frequency-plan-id EU_863_870
# List devices
ttn-lw-cli end-devices list my-app
# Get device info
ttn-lw-cli end-devices get my-app my-device
# Subscribe to uplinks
ttn-lw-cli events subscribe --application-id my-app
# Send downlink
ttn-lw-cli end-devices downlink push my-app my-device \
--frm-payload "AQID" --f-port 1
Payload Encoding/Decoding
// Decoder function (TTN/ChirpStack)
function decodeUplink(input) {
var data = {};
var i = 0;
while (i < input.bytes.length) {
var channel = input.bytes[i++];
var type = input.bytes[i++];
switch (type) {
case 0x67: // Temperature
data['temperature_' + channel] =
((input.bytes[i++] << 8) | input.bytes[i++]) / 10;
break;
case 0x68: // Humidity
data['humidity_' + channel] = input.bytes[i++] / 2;
break;
case 0x73: // Barometer
data['pressure_' + channel] =
((input.bytes[i++] << 8) | input.bytes[i++]) / 10;
break;
case 0x88: // GPS
data['latitude_' + channel] =
((input.bytes[i++] << 16) | (input.bytes[i++] << 8) | input.bytes[i++]) / 10000;
data['longitude_' + channel] =
((input.bytes[i++] << 16) | (input.bytes[i++] << 8) | input.bytes[i++]) / 10000;
data['altitude_' + channel] =
((input.bytes[i++] << 16) | (input.bytes[i++] << 8) | input.bytes[i++]) / 100;
break;
}
}
return { data: data };
}
Custom Binary Encoding
// Efficient binary encoder (device side - C)
// Pack temperature (int16, 0.01 resolution) + humidity (uint8, 0.5 resolution)
// bytes[0-1] = temperature * 100
// bytes[2] = humidity * 2
// Decoder (server side - JS)
function decodeUplink(input) {
var temp = (input.bytes[0] << 8 | input.bytes[1]);
if (temp > 32767) temp -= 65536;
return {
data: {
temperature: temp / 100,
humidity: input.bytes[2] / 2,
battery: input.bytes[3]
}
};
}
Advanced Usage
Adaptive Data Rate (ADR)
ADR automatically optimizes:
- Spreading factor (lower = faster, less range)
- Transmit power (lower = less battery usage)
- Channel selection
ADR is recommended for stationary devices.
Disable for mobile devices:
LMIC_setAdrMode(0); // Disable ADR
Multicast
# Create multicast group (ChirpStack)
curl -X POST http://localhost:8080/api/multicast-groups \
-H "Grpc-Metadata-Authorization: Bearer $API_KEY" \
-d '{
"multicastGroup": {
"name": "firmware-update-group",
"applicationId": "app-uuid",
"region": "EU868",
"mcAddr": "01020304",
"mcNwkSKey": "00112233445566778899aabbccddeeff",
"mcAppSKey": "ffeeddccbbaa99887766554433221100",
"groupType": "CLASS_C"
}
}'
FUOTA (Firmware Update Over The Air)
# LoRaWAN FUOTA uses multicast + fragmented data transport
# Requires Class B or C capable devices
# ChirpStack supports FUOTA via the FUOTA server component
Troubleshooting
| Issue | Solution |
|---|
| Device won’t join (OTAA) | Verify DevEUI/AppKey match, check frequency plan |
| No uplinks received | Check gateway connectivity, antenna connection |
| Downlinks not delivered | Class A: wait for next uplink; check RX windows |
| Poor range | Check antenna, raise gateway, reduce SF |
| High packet loss | Check duty cycle limits, reduce payload size |
| ADR not optimizing | Need 20+ uplinks for ADR to activate |
| Duplicate messages | Normal with multiple gateways; server deduplicates |
| Join accept timeout | Check RX1/RX2 delay settings match server |
# Monitor gateway traffic
# ChirpStack: Gateway > Frames tab shows all LoRa frames
# Check gateway status
curl http://localhost:8080/api/gateways/$GATEWAY_ID \
-H "Grpc-Metadata-Authorization: Bearer $API_KEY"
# View device events
curl "http://localhost:8080/api/devices/$DEV_EUI/events?limit=20" \
-H "Grpc-Metadata-Authorization: Bearer $API_KEY"
# TTN: check gateway status
ttn-lw-cli gateways get my-gateway --connection-stats
# Airtime calculator
# https://www.thethingsnetwork.org/airtime-calculator
# Input: SF, bandwidth, payload size, coding rate
Duty Cycle Limits
EU868 duty cycle restrictions:
- Band 1 (868.0-868.6 MHz): 1% duty cycle
- Band 2 (868.7-869.2 MHz): 0.1% duty cycle
- Band 3 (869.4-869.65 MHz): 10% duty cycle
US915: No duty cycle (FCC dwell time limits instead)
- Max 400ms dwell time per channel