Metadata-Version: 2.4
Name: softlora
Version: 0.5.0
Summary: LoRa satellite receiver - sync, demodulate, and decode
Author-email: LibreCube <info@librecube.org>, Hamza Hassan <hamza.mohammed.hasan@gmail.com>
License: MIT License
Project-URL: Homepage, https://gitlab.com/librecube/prototypes/gsoc-lora-sat-receiver
Project-URL: Repository, https://gitlab.com/librecube/prototypes/gsoc-lora-sat-receiver
Project-URL: Bug Tracker, https://gitlab.com/librecube/prototypes/gsoc-lora-sat-receiver/-/issues
Project-URL: Documentation, https://librecube.gitlab.io/prototypes/gsoc-lora-sat-receiver/
Keywords: lora,satellite,sdr,chirp-spread-spectrum,gnuradio
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Science/Research
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Typing :: Typed
Classifier: Topic :: Communications :: Ham Radio
Classifier: Topic :: Scientific/Engineering :: Information Analysis
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: numpy
Requires-Dist: scipy
Provides-Extra: docs
Requires-Dist: mkdocs; extra == "docs"
Requires-Dist: mkdocs-material; extra == "docs"
Requires-Dist: mkdocstrings[python]; extra == "docs"
Provides-Extra: dev
Requires-Dist: build; extra == "dev"
Requires-Dist: twine; extra == "dev"
Requires-Dist: pytest; extra == "dev"
Dynamic: license-file

# SoftLoRa

[![Google Summer of Code 2026](https://img.shields.io/badge/Google%20Summer%20of%20Code-2026-fbb040?logo=google&logoColor=white)]()

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](https://librecube.org/).


## Install

```bash
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](#receive-from-an-sdr).

## Decode a recording

```python
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:

```python
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/`](examples/README.md), for three
common radios -- set `RADIO` to the one you have:

```bash
python examples/sdr_live.py                   # RTL-SDR, Airspy or HackRF
```

It is a thin script over [`softlora.sdr.LiveReceiver`](softlora/sdr.py), which
owns the radio, the reader thread and the frequency correction:

```python
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))
```

```
436.13125 MHz  sf=8  bw=125k  fs=1000k (8x bw, decim 3)  offset=0k   Ctrl-C to stop

[16:04:38] t=  22.43s  snr=+11.7dB  cfo=     -8Hz
  len=15  cr=4/5  crc=on
  hex  736f66746c6f726120626561636f6e
  text 'softlora beacon'
```

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`](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**:

```bash
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`](softlora/packet.py) objects. The
fields you will usually want:

```python
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.

<div align="center">

| <img src="docs/images/lora_packet.png" width="600"> |
|:---:|
| *A real LoRa packet received from the Polytech Universe-3 (PU-3) satellite (SF8, 62.5 kHz bandwidth). The payload is truncated for display.* |

</div>

## Going further

| Topic | Where |
|---|---|
| Runnable examples | [`examples/`](examples/README.md) |
| Packets without a header | [Quick start](docs/quickstart.md) |
| Tuning the decoder (`DecoderSettings`) | [The decode pipeline](docs/guide/pipeline.md) |
| Rescuing weak packets with Chase decoding | [Chase guide](docs/guide/chase.md) |
| Live receive off RTL-SDR / Airspy / HackRF | [`examples/README.md`](examples/README.md#live-sdr-receive) |
| Live SDR chain via GNU Radio | [`examples/README.md`](examples/README.md#live-sdr-via-gnu-radio) |
| Doppler and carrier offset for satellites | [Ground-station use](docs/guide/satellites.md) |
| Benchmarks across SF 7–12 | [Performance](docs/guide/performance.md) |
| How synchronization works | [Sync algorithm](docs/guide/sync-algorithm.md) |
| Full API reference | [Documentation site](https://gsoc-lora-sat-receiver-7e5f4f.gitlab.io/) |

## References

[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](https://arxiv.org/abs/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](LICENSE) and [CONTRIBUTORS.txt](CONTRIBUTORS.txt).
