Skip to content

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.