Pure-Python LoRa decoder and TinyGS SDR ground station (no ESP32)

Hi everyone,

Sharing a project I built this summer (GSoC 2026 with LibreCube) that should
interest colleagues here: a software-defined LoRa ground station for
the TinyGS network that runs entirely in Python. ( No more ESP based Boards)

The architecture is the same idea as SatNOGS: any Linux box + an SDR + an
antenna. An SDR streams IQ into a pure-Python LoRa decoder (softlora), and
a small station client (tinygs) publishes decoded packets to TinyGS over
MQTT-TLS.

It’s still in active testing, if anyone runs an omnidirectional VHF/UHF setup
and wants to try it on a LoRa satellite pass, I’d love feedback. My email is Hamza.mohammed.hasan[at]gmail[dot]com.

6 Likes

thats sound cool.

an in first time i read about librecube..

image

thank you for sharing

1 Like

poke @stefanp @jp7dvx

2 Likes

Hi Hamza (@studhamza), and thanks for the poke, @bali!

This is incredible timing! I was just wondering about setting up a TinyGS SDR ground station using my Linux environment and an RTL-SDR V4 instead of buying an additional hardware node, and then I found your post.

Thank you for sharing this Pure-Python LoRa decoder project (softlora). I’m very excited to check out your setup guide and test it out on upcoming satellite passes.

Thanks again for your great work! 73!

Hi @bali,

Thanks for the poke! Thanks to you, I noticed this amazing thread right away.

My current little hardware node is already working hard and keeping me satisfied, but it’s great timing because I was just wondering about setting up an SDR-based station using my Linux environment and RTL-SDR V4 instead of buying an additional node.

Thanks again for pointing me here! 73!

Hi @studhamza and everyone,

I’m currently setting up a pure-Python TinyGS SDR ground station following your great architecture (no ESP32, using a Linux box + SDR + softlora)!

Here is my setup:

  • Hardware: RTL-SDR Blog V4 (RTLSDRBlog, Blog V4 with R828D tuner)

  • Environment: Debian Linux (x86_64), Python 3.13, softlora

  • Pipeline command:

    Bash

    rtl_sdr -f 868100000 -s 2048000 -g 40 - | python3 receiver.py
    
    

Current Status & Question: I successfully piped the IQ stream into my Python script and initialized the LoRaDecoder with the following parameters:

Python

decoder = LoRaDecoder(bw=125000, fs=2048000, fc=868100000, sf=7)

However, when running the script, the decoder continuously outputs header parse attempts, resulting in errors like this:

Plaintext

header decode failed: Invalid header: payload_len=84, cr=6
header decode failed: Invalid header: checksum mismatch
header decode failed: checksum mismatch
header decode failed: Invalid header: payload_len=192, cr=7
...

I wanted to check if this behavior is expected when receiving ambient noise / before locking onto an actual satellite pass, or if I should fine-tune specific parameters (like DC offset, gain, or synchronization thresholds) to get clean decodes.

Any advice or feedback would be greatly appreciated!

73, JP7DVX / Yukio

Are there already active stations in TinyGS?

Hi @stefanp,

Ah, I might not have been clear—I’m actually right in the middle of troubleshooting!

I’m trying to get the softlora decoder running, but I keep getting checksum mismatch and header decode failures (as shown in my logs above). I’m still figuring out if it’s just ambient noise or if my setup parameters need tweaking.

So no active decoding yet, just a workstation trying to catch its first packet! :slight_smile:

73, JP7DVX

1 Like

OK - with the sdr it’s easy to find out, if the signal is ok, just record a few packets from a strong satellite with positive snr (Tianqi) and then check the recording with inspectrum, or as a different method, run a Heltec lora V3 board parallel on the same antenna (I think it’s worth investing $18, to avoid puzzling around for weeks) - then you see the signal quality and you can directly compare the performance.

Stefan/OE6ISP

1 Like

Hi @stefanp,

Thank you for the practical suggestions! Using inspectrum or comparing with a hardware board sounds like a solid approach to verify the RF path.

That said, @studhamza (Hamza), I’m really curious to hear your thoughts on this as well! When testing softlora with an SDR stream, are these persistent checksum mismatch or header decode failed errors expected when no valid sync word / strong packet is locked yet? Or is there a specific tuning trick in softlora we should look out for?

Thanks again to both of you!

73, JP7DVX

1 Like

Hi @JP7DVX,

I dont have much experience with soft-decoding lora, but the behaviour you are describing lets speculate, that the preamble is detected correctly and during header-decode things go off rails.

According to my experience, even though the Lora signals are very robust, they are extremly sensitive to phase-issues, which can be caused by a jittery time-base or a poor SAW-filter. Unfortunately these problems are difficult to measure, but this comes into my mind, when I read the description of your problem. One more reason to connect a lora-hardware in parallel, which can be used to verify the integrity of the signal at least at the SDR-input (compared to other measuring equipment, the lora-board is an extremly powerfull and cheap measuring tool). In addition, with the lora board you have your own LoRa signal generator for testing and you can start with a strong, clean signal and work your way up to weaker and more noisy signals.

Stefan /OE6ISP

1 Like

Hi @stefanp,

Thanks for the detailed insights! That makes a lot of sense regarding phase sensitivity.

Just as a quick update before calling it a night: I actually tried switching over to the 400MHz band as well, but I got very similar header parse/checksum errors there too.

I’ll sleep on it and see what Hamza thinks when he has a moment. Thanks again for all the help today!

73, JP7DVX

1 Like

Could not sleep and fired it up again… :joy:

As you can see from the console log, it’s actually catching the signal and trying to parse it, but failing on checksum mismatch/header decode. It seems the signal is physically there, but the TLE/timing sync isn’t quite aligning for a full decode. Real-world RF never gets boring!

Hello Yukio,

Just came home from school. Actually this behavior was not intended, its a design flaw. I assume the decoder locks onto non existing packets.

Explanation

The decoder’s sync stage declares a preamble when three consecutive dechirped windows peak in the same FFT bin. That has a small but non-zero false-alarm rate on pure noise. Each false lock then synchronizes a non existing packet and tries to parse an explicit header, which fails its checksum, producing exactly the header decode failed warnings.

For IID circular Gaussian noise, the dechirped FFT bins |Y[k]| are IID Rayleigh, so the argmax bin is uniformly distributed over {0..N-1}. Every window picks a random “peak” bin. The detection condition “3 windows peak within ±1 bin” therefore happens by chance. For a window whose peak is bin b, the next two windows must land in {b−1, b, b+1}:

P(false preamble) ≈ (3/N) ^ (N_detect − 1) = (3/N)² for N_detect=3

Windows are examined at bw/N per second, so the false-alarm rate is:

rate ≈ (bw/N) · (3/N)² = bw · 9 / N³

In theory, at SF=7, BW=125khz, the false alarm rate is approximately 0.54.

A proposed solution I have in mind is adding a SNR gate before allowing the argmax bins to be counted as preamble peaks (should be far above noise).

In conclusion, even though these warnings are expected on ambient noise, they’re misleading and should be fixed.

Thank you for reporting! I’ll investigate this further and follow up with finding.

3 Likes

Hi Hamza,

Thanks for the detailed technical breakdown! My math/theory is a bit rusty, but I’m just relieved to know that my setup and config weren’t the culprit here. :joy:

I’ll halt the receiver for now and patiently wait for your updated version. Really appreciate you diving into this so quickly—keep up the great work!

73, Yukio / JP7DVX

Hello Yukio,

We fixed this behavior, you can run pip install softlora -U to upgrade to the latest release. Also make sure to check the examples directory at examples · main · LibreCube / Library / python-softlora · GitLab

Let me know if you face any more issues

1 Like

Hi Hamza,

Updated to v0.4.0 and ran a test on the 400MHz band.

Plaintext

Starting SoftLoRa TinyGS receiver pipeline (400MHz band)...
header decode failed: Invalid header: payload_len=174, cr=0
header decode failed: Invalid header: payload_len=0, cr=6
header decode failed: Invalid header: checksum mismatch
header decode failed: Invalid header: payload_len=218, cr=0
header decode failed: Invalid header: checksum mismatch
header decode failed: Invalid header: payload_len=123, cr=5
header decode failed: Invalid header: checksum mismatch

As you can see from the log, header decode failed and checksum mismatch messages are still continuously appearing while running.

Just wanted to share the current status. Thanks again for your work!

73, Yukio / JP7DVX

Hi Yukio,

I see, that is frustrating. Can you try running the rtl sdr example

Hamza

1 Like

Hi Hamza,

Thank you for your advice. I wanted to try running the example as you suggested, but I’ve hit a bit of a wall. Since I’m still struggling to fully understand how to navigate this and haven’t been able to use the AI assistant effectively to sort it out, I’m afraid I’ve reached my limit for now and have to step back.

Thanks again for your time and help!

73, Yukio / JP7DVX

1 Like

Hi Hamza,

Thanks for pointing me to the example!

I tried to check it out, but it looks like examples/sdr_live_rtl_sdr.py is no longer in the main branch due to the recent cleanup.

Could you let me know where the current live RTL-SDR example has been moved, or how I should run it with the latest version?

Thanks again for your support!

73, Yukio / JP7DVX