Raspberry Pi

Raspberry Pi LoRa MQTT Receiver: Forward SX127x Packets to the Cloud

Receive point-to-point LoRa packets on a Raspberry Pi with an RFM95 or SX127x HAT, match radio settings with your senders and republish every packet as JSON to an MQTT broker.

Raspberry Pi with a LoRa radio bonnet and antenna receiving long-range packets, illustrating raspberry pi lora mqtt
Run this on your own MQTT brokerGet a private, managed MQTT broker on the Free plan and start from ready-made code samples.
Try it free Code samples No card required

A Raspberry Pi LoRa MQTT receiver is the simplest way to collect data from sensors that are too far away for Wi-Fi: a field, a farm, a car park or the far end of a large site. In this tutorial you will connect an SX127x-based radio (RFM95W, RFM96W or a LoRa bonnet) to a Pi, receive point-to-point packets from your own senders and republish each one as JSON to an MQTT broker. You will start with broker.connectmq.com and finish with a production-ready setup.

Point-to-Point LoRa vs LoRaWAN

LoRa is a radio modulation; LoRaWAN is a full network protocol built on top of it, with device registration, encryption keys, a network server and multi-channel gateways. If you already run a LoRaWAN network, your network server publishes uplinks to MQTT and you want the ChirpStack MQTT integration instead.

Point-to-point LoRa skips all of that. Your sender (often an ESP32 or Arduino with an SX1276 module) transmits a small packet on a fixed frequency with fixed settings, and a single-channel receiver hears it. It is easy to understand and fully under your control, which makes it a good fit for hobby projects, prototypes and private sensor networks where you own both ends. The trade-off is that you handle addressing, integrity checks and duty cycle limits yourself.

Hardware

Item Notes
Raspberry Pi Any model with the 40-pin header; a Zero 2 W is enough
SX127x radio Adafruit LoRa Radio Bonnet, RFM95W breakout or similar SPI module
Antenna Tuned for your band; never transmit without one
Sender node ESP32, Arduino or a second Pi with a matching radio
Frequency 868 MHz in Europe, 915 MHz in North America, 433 MHz in some regions; follow local rules

Wiring for a breakout follows the SPI pins: SCK to GPIO 11, MOSI to GPIO 10, MISO to GPIO 9, CS to CE1 (GPIO 7), RESET to GPIO 25, plus 3.3 V and ground. The Adafruit bonnet uses exactly those pins, which is why the code below defaults to them.

Install the Libraries

Enable SPI with sudo raspi-config (Interface Options, SPI), then create an environment with Adafruit Blinka, which provides the board and busio modules on Linux, and the RFM9x driver:

sudo raspi-config nonint do_spi 0
ls /dev/spidev*
python3 -m venv ~/lora/.venv
source ~/lora/.venv/bin/activate
pip install adafruit-blinka adafruit-circuitpython-rfm9x "paho-mqtt>=2.0"

The Blinka installation guide on the Adafruit site lists extra steps for some Pi models, for example installing lgpio on newer releases. If import board fails, follow that guide before going further.

Match the Radio Settings

A LoRa receiver only hears packets sent with the same frequency, spreading factor, bandwidth, coding rate, sync word and preamble. A mismatch does not produce errors; it produces silence. Write the settings down once and use them on both sides.

Setting Example value Notes
Frequency 868.1 MHz Must be legal in your region
Spreading factor 7 Higher means longer range, slower airtime
Bandwidth 125 kHz Common default
Coding rate 4/5 Set as 5 in the driver
CRC On Enable on sender and receiver
Header Depends on sender See the RadioHead note below

The RadioHead header pitfall. The Adafruit RFM9x driver follows the RadioHead convention and, by default, strips a 4-byte header (to, from, id, flags) from received packets. Senders using the Arduino LoRa library by Sandeep Mistry do not add that header, so the first four bytes of your payload silently disappear. Call receive(with_header=True) when your sender does not use RadioHead, as the script below does.

The Raspberry Pi LoRa MQTT Script

The sender in this example transmits a short text payload like n1,21.4,63,3.92 (node ID, temperature, humidity, battery volts). The receiver validates it, adds radio metadata and publishes to a per-node topic. Unknown or malformed packets go to a separate raw topic for debugging rather than being dropped silently.

import json
import os
import time
import uuid

import adafruit_rfm9x
import board
import busio
import digitalio
import paho.mqtt.client as mqtt

BROKER = os.getenv("MQTT_HOST", "broker.connectmq.com")
PORT = int(os.getenv("MQTT_PORT", "1883"))
USERNAME = os.getenv("MQTT_USERNAME")
PASSWORD = os.getenv("MQTT_PASSWORD")
CLIENT_ID = os.getenv("MQTT_CLIENT_ID", f"lora-rx-{uuid.uuid4().hex[:8]}")
PREFIX = os.getenv("TOPIC_PREFIX", "connectmq/demo/l9r3x5/lora")
FREQ_MHZ = float(os.getenv("LORA_FREQ", "868.1"))

spi = busio.SPI(board.SCK, MOSI=board.MOSI, MISO=board.MISO)
cs = digitalio.DigitalInOut(board.CE1)
reset = digitalio.DigitalInOut(board.D25)
rfm9x = adafruit_rfm9x.RFM9x(spi, cs, reset, FREQ_MHZ)
rfm9x.spreading_factor = 7
rfm9x.signal_bandwidth = 125000
rfm9x.coding_rate = 5
rfm9x.enable_crc = True


def on_connect(client, userdata, flags, reason_code, properties):
    print("MQTT connected:", reason_code)
    client.publish(f"{PREFIX}/receiver/status", "online", qos=1, retain=True)


client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id=CLIENT_ID)
if USERNAME:
    client.username_pw_set(USERNAME, PASSWORD)
if PORT == 8883:
    client.tls_set()
client.will_set(f"{PREFIX}/receiver/status", "offline", qos=1, retain=True)
client.reconnect_delay_set(1, 60)
client.on_connect = on_connect
client.connect_async(BROKER, PORT, keepalive=60)
client.loop_start()


def parse(text):
    parts = text.split(",")
    if len(parts) != 4 or not parts[0].startswith("n"):
        return None
    node, temp, hum, vbat = parts
    return node, {"temperature": float(temp), "humidity": float(hum), "vbat": float(vbat)}


print(f"listening on {FREQ_MHZ} MHz")
try:
    while True:
        packet = rfm9x.receive(timeout=5.0, with_header=True)
        if packet is None:
            continue
        meta = {"rssi": rfm9x.last_rssi, "snr": rfm9x.last_snr, "ts": int(time.time())}
        try:
            parsed = parse(packet.decode("ascii").strip())
        except (UnicodeDecodeError, ValueError):
            parsed = None
        if parsed:
            node, values = parsed
            client.publish(f"{PREFIX}/{node}", json.dumps({**values, **meta}), qos=1, retain=True)
            print(node, values, meta)
        else:
            client.publish(f"{PREFIX}/raw", json.dumps({"hex": packet.hex(), **meta}), qos=0)
except KeyboardInterrupt:
    pass
finally:
    client.publish(f"{PREFIX}/receiver/status", "offline", qos=1, retain=True)
    client.loop_stop()
    client.disconnect()

On an ESP32 sender using the Arduino LoRa library, the matching configuration is LoRa.begin(868.1E6), LoRa.setSpreadingFactor(7), LoRa.setSignalBandwidth(125E3), LoRa.setCodingRate4(5) and LoRa.enableCrc(). Then send with LoRa.beginPacket(); LoRa.print("n1,21.4,63,3.92"); LoRa.endPacket();.

Watch the results from any machine:

mosquitto_sub -h broker.connectmq.com -p 1883 -i lora-viewer-l9r3x5 -t "connectmq/demo/l9r3x5/lora/#" -v

Making Packets Robust

Plain text is easy to debug but wastes airtime, and airtime is the scarce resource in LoRa. Once the link works, consider these improvements:

  1. Binary payloads. Pack values with fixed scaling, for example temperature as int16 in hundredths. A 4-field reading drops from around 16 bytes to 7, which shortens airtime and improves battery life.
  2. Node ID and sequence number. A sequence counter lets the receiver detect missed packets and duplicates, and it lets you report packet loss per node as its own metric.
  3. Integrity. The radio CRC catches corruption. If neighbours might transmit on the same settings, add a short identifier or an HMAC so you ignore packets that are not yours.
  4. Duty cycle. Many regions restrict how long a device may transmit per hour on a sub-band. Long spreading factors and frequent sends can exceed it quickly, so keep payloads small and intervals reasonable.
  5. Receiver health. Publish a heartbeat from the receiver and track the last-seen time per node, so a dead sender and a dead receiver look different on your dashboard.

The SNR and RSSI values published with every packet are your best antenna-placement tool. Positive SNR means a comfortable link; values approaching the sensitivity limit for your spreading factor mean you are near the edge of range.

Running It Unattended

A receiver on a barn wall or a mast must survive power cuts and network drops. Run the script as a systemd service so it restarts automatically, store readings locally when the uplink is down with an offline MQTT buffer in SQLite, and follow the Raspberry Pi reliability tips for SD card wear and power supplies. For remote sites over cellular, MQTT is a good fit because the connection is outbound only and the protocol overhead is small.

Moving Off the Test Broker

The public MQTT test broker is perfect for getting the radio link right, but everything published there is visible to anyone. For a real deployment, create a broker on ConnectMQ, take the generated username, password and client ID from the portal and start the receiver with MQTT_PORT=8883 and those values in the environment. TLS uses a Let's Encrypt certificate, so tls_set() works with the Pi's default CA store. A cloud MQTT broker also lets several receivers at different sites publish into one topic tree that your dashboards and alert rules can read in one place.

New to MQTT on the Pi? The Raspberry Pi MQTT tutorial covers the fundamentals used here, and the Raspberry Pi IoT gateway guide shows how a LoRa receiver can sit alongside BLE, Modbus and other inputs on one device.

Send Your LoRa Data to ConnectMQ

Create a free Serverless broker at app.connectmq.com, with no credit card required, and you will have TLS credentials for your receiver in minutes. ConnectMQ's live topic explorer shows every node as it reports, and threshold alert rules can flag a temperature or battery value that crosses a limit.

Frequently Asked Questions

Can a single SX127x receiver act as a LoRaWAN gateway?

Not properly. LoRaWAN gateways listen on several channels and spreading factors at once using concentrator chips such as the SX1302. A single SX127x hears one configuration at a time, which suits point-to-point use.

What range can I expect?

Range depends heavily on antenna height, terrain and spreading factor. Line of sight with elevated antennas reaches far further than through buildings, so measure RSSI and SNR on site.

Why does my receiver get packets with missing bytes?

Almost always the RadioHead header mismatch. Use receive(with_header=True) when the sender does not add the 4-byte RadioHead header.

Can the Pi send commands back to the nodes?

Yes, the RFM9x driver can transmit. Subscribe to a command topic and call rfm9x.send(), but remember the nodes must be listening at that moment, which battery-powered senders usually are only briefly after transmitting.

Related articles

Pico W vs Pico 2 W for MQTT: RP2040 vs RP2350 Performance, Memory and TLS

The Pico 2 W keeps the Pico W's footprint and Wi-Fi chip but swaps the RP2040 for the faster RP2350. This comparison looks at what that means for MQTT devices: CPU, memory, TLS, security features, software support, and which board to buy for which project.

8 min read

Raspberry Pi RFID MQTT Access Logging with the RC522 Reader

Wire an RC522 RFID reader to a Raspberry Pi, read tags in Python, check them against a local allowlist and publish every access attempt as an MQTT event that loggers, dashboards and alerting tools can consume.

7 min read

All articles →

Your private MQTT broker is a few clicks away

Create an account, get your broker credentials and connect your first device — no servers to install, patch or scale.