Ground-station use
The receiver was built for decoding satellite LoRa downlinks from a ground station. This guide covers the operational workflow.
Typical pass workflow
from softlora import LoRaDecoder, load_iq, save_packets, sample_rate
fs = sample_rate("pass.wav") or 250_000
decoder = LoRaDecoder(sf=10, bw=125_000, fs=fs, fc=436.7e6)
# Offline: decode the whole pass recording
packets = decoder.decode_file("pass.wav")
# Keep only CRC-valid payloads, and persist them
good = [p for p in packets if p.crc_valid is True]
save_packets(good, "out/2026-06-05-2047z")
for p in good:
print(f"{p.time_start_sec:9.3f}s snr={p.snr_est:6.1f}dB {p.payload_text!r}")
Live tracking
For a live pass, feed chunks from an SDR (e.g. via GNU Radio File Sink into
a pipe) through decode_stream:
decoder = LoRaDecoder(sf=10, bw=125_000, fs=250_000, fc=437e6)
while chunk := read_sdr():
for p in decoder.decode_stream(chunk):
if p.crc_valid is True:
save_packets([p], "live/")
for p in decoder.flush():
if p.crc_valid is True:
save_packets([p], "live/")
Doppler
Satellite passes have large, slowly varying Doppler. The decoder re-estimates
the carrier-frequency offset per packet from its preamble, so per-packet
re-sync (which all decode paths do) naturally tracks slow Doppler drift across
a pass. Packet.freq_offset_hz reports the estimated offset for each packet:
for p in good:
doppler_ppm = p.freq_offset_hz / 437e6 * 1e6
print(f"t={p.time_start_sec:8.2f}s Doppler = {doppler_ppm:+.2f} ppm")
Intra-packet Doppler for very long packets on fast passes is out of scope.
SNR
Packet.snr_est is a per-packet estimate from the preamble (dB, relative to
the per-sample noise). Use it to log link margin over the pass, or to reject
marginal packets.
Choosing parameters
| Setting | What it means |
|---|---|
sf |
Spreading factor (7–12); satellites often use 9–12 |
bw |
Channel bandwidth (typically 125 kHz) |
fs |
Your capture rate; decoder resamples to bw |
fc |
Center frequency (used for Doppler ppm display) |
preamble_len |
Preamble upchirps (default 8); N_netid / N_sfd_down live in DecoderSettings |
implicit_header / payload_len / crc_enabled / code_rate |
Set when the mission uses no header |
Persistence
save_packets writes packet_XXXX.bin payloads, an append-only
metadata.jsonl (one JSON line per packet with SNR, offsets, timestamps and
positions, easy to parse or load into a database afterwards), and a readable
packets.txt detail-table summary for quick inspection.