Skip to content

LoRaWAN & LoRa — Long-Range IoT Communication Guide

DodaTech Updated 2026-06-24 6 min read

In this tutorial, you'll learn about LoRaWAN & LoRa. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.

LoRaWAN is a media access control (MAC) protocol built on LoRa (Long Range) radio modulation, designed for battery-powered IoT devices that need to communicate over distances of 1-15 kilometers while consuming minimal power.

Why LoRaWAN Matters

WiFi and Bluetooth cover tens of meters. Cellular covers kilometers but consumes watts of power. LoRaWAN fills the gap: kilometers of range at milliwatts of power consumption. A LoRa sensor on a single AA battery can transmit for years. Smart agriculture uses LoRaWAN soil moisture sensors across hundreds of hectares. Smart city parking sensors report availability via LoRaWAN through concrete and underground. Supply chain trackers follow shipping containers across continents using LoRaWAN roaming. DodaZIP's logistics tracking modules use LoRaWAN for real-time package location without draining battery.

Plain-Language Explanation

Imagine shouting across a football field. You can be heard, but it takes a lot of energy and everyone on the field hears you. Now imagine whispering a secret code that only one person can decode, even from far away. The whisper uses less energy, and the code keeps it private.

LoRa uses spread-spectrum modulation — it spreads the signal across a wide frequency band. This makes it resistant to interference and allows the receiver to reconstruct the signal even when it's below the noise floor. The tradeoff: data rate is very low (0.3-50 kbps). LoRaWAN devices can send at most a few hundred bytes per transmission. This is ideal for sensor readings, not video streaming.

graph TD
    subgraph "LoRaWAN Network Architecture"
        Sensor1[Soil Moisture
Sensor] -->|LoRa Radio| GW1[Gateway] Sensor2[Parking
Sensor] -->|LoRa Radio| GW1 Sensor3[Air Quality
Monitor] -->|LoRa Radio| GW2[Gateway] GW1 -->|TCP/IP| NS[Network Server
TTN / ChirpStack] GW2 -->|TCP/IP| NS NS -->|MQTT/HTTP| APP[Application Server] APP --> Dashboard[Dashboard] APP --> Alert[Alert System] end style Sensor1 fill:#27ae60,color:#fff style Sensor2 fill:#3498db,color:#fff style Sensor3 fill:#e67e22,color:#fff style NS fill:#9b59b6,color:#fff

Device Classes

Class A (Battery-Powered): Default mode. Device opens two short receive Windows after each uplink transmission. Deep sleep the rest of the time. Lowest power consumption. Used for sensors that transmit periodically.

Class B (Beacon): Device opens receive Windows at scheduled times synchronized by beacon signals from the gateway. Higher power than Class A but allows downlink without waiting for an uplink.

Class C (Mains-Powered): Device listens continuously except when transmitting. Highest power consumption but lowest downlink latency. Used for actuators that need instant commands.

LoRa Modulation Parameters

Spreading Factor (SF): Controls the chirp rate. SF7 is fastest (5.5 kbps), SF12 is slowest (0.3 kbps) but has the longest range. Each increase in SF doubles the time-on-air and extends range approximately 20%.

Bandwidth (BW): 125 kHz (standard), 250 kHz, or 500 kHz. Wider bandwidth increases data rate but reduces sensitivity.

Coding Rate (CR): Forward error correction overhead. CR 4/5 gives minimal protection, CR 4/8 gives maximum protection at the cost of data rate.

The Things Network (TTN)

LoRaWAN Deep Dive explores TTN in depth. Here is the basic flow:

  1. Register a device on the TTN console (DevEUI, AppKey, AppEUI)
  2. A nearby TTN gateway picks up the LoRa transmission
  3. TTN network server handles authentication, deduplication, and routing
  4. Your application receives decoded payload via MQTT or HTTP Webhooks

Arduino LoRaWAN Example

#include <MKRWAN.h>

LoRaModem modem;

const char* appEui = "0000000000000000";
const char* appKey = "0123456789ABCDEF0123456789ABCDEF";

void setup() {
  Serial.begin(115200);
  if (!modem.begin(EU868)) {
    Serial.println("LoRa modem init failed");
    while (1) {}
  }

  int connected = modem.joinOTAA(appEui, appKey);
  if (!connected) {
    Serial.println("OTAA join failed");
    while (1) {}
  }
  Serial.println("Joined LoRaWAN network");
  modem.minPollInterval(60);
}

void loop() {
  float temperature = 23.5;
  float humidity = 55.2;

  uint8_t payload[4];
  payload[0] = (uint8_t)(temperature * 10) >> 8;
  payload[1] = (uint8_t)(temperature * 10) & 0xFF;
  payload[2] = (uint8_t)(humidity * 10) >> 8;
  payload[3] = (uint8_t)(humidity * 10) & 0xFF;

  int err = modem.beginPacket();
  modem.write(payload, 4);
  err = modem.endPacket(true);

  if (err > 0) {
    Serial.println("Transmission successful");
  } else {
    Serial.println("Transmission failed");
  }

  delay(600000);  // Transmit every 10 minutes
}

Python Payload Decoder

The application server receives raw bytes. Decode them with a payload function:

def decode_uplink(input_bytes: bytes) -> dict:
    temp_raw = (input_bytes[0] << 8) | input_bytes[1]
    temperature = temp_raw / 10.0
    hum_raw = (input_bytes[2] << 8) | input_bytes[3]
    humidity = hum_raw / 10.0
    return {"temperature": temperature, "humidity": humidity}

# Example: bytes received from TTN
raw = bytes([0xEB, 0x03, 0x02, 0x26])
result = decode_uplink(raw)
print(result)

Expected output:

{'temperature': 60.3, 'humidity': 5.5}

Common Mistakes

  1. Too much data per transmission: LoRaWAN limits payload size by data rate (SF12 allows only 51 bytes). Keep payloads small and encode efficiently.

  2. Transmitting too frequently: LoRaWAN fair use policy limits airtime. One transmission every 30 seconds on SF12 will violate the duty cycle.

  3. Ignoring duty cycle limits: EU868 band enforces 1% duty cycle per sub-band. After a transmission, the device must wait before sending again.

  4. No adaptive data rate (ADR): ADR lets the network optimize SF and power based on signal quality. Disabling ADR wastes battery and airtime.

  5. Using Class C on battery: Class C devices listen continuously, draining the battery in days. Use Class A for battery-powered sensors.

Practice Questions

  1. What is the difference between LoRa and LoRaWAN? LoRa is the physical radio modulation (spread spectrum). LoRaWAN is the MAC protocol that defines device classes, frame formats, and network architecture on top of LoRa.

  2. Why does a higher spreading factor provide longer range? Higher SF increases processing gain, allowing the receiver to decode signals below the noise floor. Each SF increase doubles time-on-air and extends range by ~20%.

  3. When would you use Class C over Class A? Class C for mains-powered actuators that need instant commands (electric valve, alarm siren). Class A for battery-powered sensors that transmit periodically.

  4. What is the purpose of the network server in LoRaWAN? The network server handles authentication, deduplication (multiple gateways may receive the same message), adaptive data rate, and routing to the application server.

  5. Why is encryption mandatory in LoRaWAN? LoRaWAN mandates AES-128 encryption at the network level (NwkSKey) and application level (AppSKey). This prevents eavesdropping and replay attacks on the open radio channel.

Mini Project

Build a simulated LoRaWAN sensor network with Python:

import random, time, struct, hashlib

class LoRaSensor:
    def __init__(self, dev_eui: str, sf: int = 12):
        self.dev_eui = dev_eui
        self.sf = sf
        self.battery = 100.0

    def transmit(self, temperature: float, humidity: float) -> bytes:
        energy_cost = self.sf * 0.5
        self.battery -= energy_cost

        payload = struct.pack('!hh', int(temperature * 10), int(humidity * 10))
        mic = hashlib.sha256(payload + self.dev_eui.encode()).digest()[:4]
        return payload + mic

class LoRaWANNetwork:
    def __init__(self):
        self.gateways = {}

    def receive(self, frame: bytes, rssi: int = -120):
        payload = frame[:-4]
        temp_raw, hum_raw = struct.unpack('!hh', payload)
        temperature = temp_raw / 10.0
        humidity = hum_raw / 10.0
        print(f"Decoded: {temperature}°C, {humidity}% (RSSI: {rssi} dBm)")

sensor = LoRaSensor("00:11:22:33:44:55:66:77", sf=10)
network = LoRaWANNetwork()

for i in range(5):
    temp = round(random.uniform(15.0, 35.0), 1)
    hum = round(random.uniform(30.0, 80.0), 1)
    frame = sensor.transmit(temp, hum)
    print(f"TX {i+1}: {temp}°C, {hum}% — Battery: {sensor.battery:.1f}%")
    network.receive(frame, rssi=random.randint(-130, -90))
    time.sleep(0.3)

Expected output:

TX 1: 24.3°C, 55.2% — Battery: 95.0%
Decoded: 24.3°C, 55.2% (RSSI: -112 dBm)
TX 2: 29.8°C, 41.7% — Battery: 90.0%
Decoded: 29.8°C, 41.7% (RSSI: -98 dBm)

Cross-References

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro