Decode LoRa packets from radio recordings and live SDR streams, in pure Python.
Point it at an IQ recording and it finds the packets, corrects the frequency and
timing errors introduced by the radio link, and gives you back the payload.
No GNU Radio or C++ needed — just numpy and scipy.
It was built for satellite downlinks, where signals are weak and Doppler-shifted,
and is tested on synthetic data, over-the-air captures and real satellite passes.
Developed for Google Summer of Code 2026 with LibreCube.
Install
pip install softlora
Python 3.9+. The decoder needs only numpy and scipy.
Receiving live off an SDR additionally needs the SoapySDR bindings, which are
a system package rather than a pip one — see
Receive from an SDR.
Decode a recording
from softlora import LoRaDecoder
decoder = LoRaDecoder(sf=10, bw=125_000, fs=125_000, fc=437e6)
for packet in decoder.decode_file("recording.wav"):
print(packet.payload_text, packet.crc_valid)
The four arguments describe the signal you are decoding:
Argument
Meaning
sf
Spreading factor, 7–12
bw
LoRa bandwidth in Hz
fs
Sample rate of your recording in Hz
fc
Center frequency in Hz
.wav, .cfile, .dat and .bin files are supported. A recording may hold
several packets, so you always get a list back.
Decode a live stream
Feed IQ chunks as they arrive from an SDR. Packets are returned as soon as they
are complete, even when one spans two chunks:
decoder = LoRaDecoder(sf=10, bw=125_000, fs=250_000, fc=437e6)
while receiving:
for packet in decoder.decode_stream(read_iq_from_sdr(8192)):
print(packet.payload_text)
for packet in decoder.flush(): # decode whatever is left in the buffer
print(packet)
Receive from an SDR
One ready-to-run receiver, in examples/, for three
common radios – set RADIO to the one you have:
python examples/sdr_live.py # RTL-SDR, Airspy or HackRF
It is a thin script over softlora.sdr.LiveReceiver, which
owns the radio, the reader thread and the frequency correction:
from softlora.sdr import LiveReceiver, format_packet
with LiveReceiver(436.13125e6, sf=8, radio='airspy') as rx:
for packet in rx:
print(format_packet(packet))
It tunes off-channel to dodge the radio’s DC spike, decimates to a rate the
decoder likes, and folds each packet’s residual frequency error back into the
software shift — so the receiver calibrates itself out of its own crystal
error while it runs. RECORD keeps the IQ for offline replay.
That capture is the bundled
examples/arduino/lora_beacon sketch, which
beacons one LoRa packet every 3 s from an STM32 + SX1262 at exactly these
defaults — a transmitter to test against when you have only one radio.
softlora.sdr needs SoapySDR, which is not on PyPI:
sudo apt install soapysdr-tools python3-soapysdr
sudo apt install soapysdr-module-rtlsdr # or -airspy, or -hackrf
python3 -m venv --system-site-packages .venv
Reading the result
Every decode path returns Packet objects. The
fields you will usually want:
packet.payload_bytes # the data
packet.payload_text # the same data decoded as UTF-8
packet.crc_valid # True when the payload passed its checksum
packet.snr_est # signal-to-noise estimate in dB
packet.ok # a packet was found and demodulated
ok and crc_valid answer different questions. ok=True, crc_valid=False
means a packet arrived but was corrupted on the way. Filter on
packet.crc_valid is True when you only want trustworthy payloads.
A LoRa packet
Each packet starts with a preamble of plain upchirps, then a sync word, then a
2.25-symbol start-of-frame delimiter (SFD) made of downchirps, and finally the
header, payload and CRC. All four regions are visible below.
A real LoRa packet received from the Polytech Universe-3 (PU-3) satellite (SF8, 62.5 kHz bandwidth). The payload is truncated for display.
[1] M. Xhonneux, O. Afisiadis, D. Bol, and J. Louveaux, “A Low-Complexity LoRa
Synchronization Algorithm Robust to Sampling Time Offsets,” IEEE Internet of
Things Journal, 2021. arXiv:1912.11344
[2] J. Tapparel, O. Afisiadis, P. Mayoraz, A. Balatsoukas-Stimming, and A. Burg,
“An Open-Source LoRa Physical Layer Prototype on GNU Radio,” SPAWC, 2020.
SoftLoRa
Decode LoRa packets from radio recordings and live SDR streams, in pure Python.
Point it at an IQ recording and it finds the packets, corrects the frequency and timing errors introduced by the radio link, and gives you back the payload. No GNU Radio or C++ needed — just
numpyandscipy.It was built for satellite downlinks, where signals are weak and Doppler-shifted, and is tested on synthetic data, over-the-air captures and real satellite passes.
Developed for Google Summer of Code 2026 with LibreCube.
Install
Python 3.9+. The decoder needs only
numpyandscipy.Receiving live off an SDR additionally needs the SoapySDR bindings, which are a system package rather than a pip one — see Receive from an SDR.
Decode a recording
The four arguments describe the signal you are decoding:
sfbwfsfc.wav,.cfile,.datand.binfiles are supported. A recording may hold several packets, so you always get a list back.Decode a live stream
Feed IQ chunks as they arrive from an SDR. Packets are returned as soon as they are complete, even when one spans two chunks:
Receive from an SDR
One ready-to-run receiver, in
examples/, for three common radios – setRADIOto the one you have:It is a thin script over
softlora.sdr.LiveReceiver, which owns the radio, the reader thread and the frequency correction:It tunes off-channel to dodge the radio’s DC spike, decimates to a rate the decoder likes, and folds each packet’s residual frequency error back into the software shift — so the receiver calibrates itself out of its own crystal error while it runs.
RECORDkeeps the IQ for offline replay.That capture is the bundled
examples/arduino/lora_beaconsketch, which beacons one LoRa packet every 3 s from an STM32 + SX1262 at exactly these defaults — a transmitter to test against when you have only one radio.softlora.sdrneeds SoapySDR, which is not on PyPI:Reading the result
Every decode path returns
Packetobjects. The fields you will usually want:okandcrc_validanswer different questions.ok=True, crc_valid=Falsemeans a packet arrived but was corrupted on the way. Filter onpacket.crc_valid is Truewhen you only want trustworthy payloads.A LoRa packet
Each packet starts with a preamble of plain upchirps, then a sync word, then a 2.25-symbol start-of-frame delimiter (SFD) made of downchirps, and finally the header, payload and CRC. All four regions are visible below.
Going further
examples/DecoderSettings)examples/README.mdexamples/README.mdReferences
[1] M. Xhonneux, O. Afisiadis, D. Bol, and J. Louveaux, “A Low-Complexity LoRa Synchronization Algorithm Robust to Sampling Time Offsets,” IEEE Internet of Things Journal, 2021. arXiv:1912.11344
[2] J. Tapparel, O. Afisiadis, P. Mayoraz, A. Balatsoukas-Stimming, and A. Burg, “An Open-Source LoRa Physical Layer Prototype on GNU Radio,” SPAWC, 2020.
License
MIT — see LICENSE and CONTRIBUTORS.txt.