Metadata-Version: 2.4
Name: lora-receiver
Version: 0.2.0
Summary: LoRa satellite receiver - sync, demodulate, and decode
Author: HamzaHassan
License-Expression: MIT
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: 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

# lora-receiver

A pure-Python LoRa satellite receiver implementing the 3-stage synchronization algorithm from [Xhonneux, Dallemagne, et al. (2021)](https://arxiv.org/abs/2106.03022).

## Features

- **Preamble detection** in complex baseband IQ recordings
- **3-stage synchronizer**: Carrier Frequency Offset (CFO) and Symbol Timing Offset (STO) estimation and correction
- **Demodulation** of LoRa symbols from corrected signals
- **Packet decoding** with both explicit headers (standard LoRa) and implicit headers (no header, forced parameters)
- **Multi-packet detection** — a sliding-window scan over a recording or a live sample stream
- **Streaming decode** — feed IQ chunks incrementally; packets may straddle chunk boundaries
- **CRC-16 verification** for payload integrity
- **Chase (soft-decision) decoding** as a fallback when the hard-decision decode fails

## Installation

```bash
pip install lora-receiver
```

Requires Python 3.9+.

## Quick Start

```python
from lora_receiver import LoRaDecoder

decoder = LoRaDecoder(sf=10, bw=125_000, fs=125_000, fc=437e6)

# Decode all packets in an IQ recording (.wav, .cfile, .dat, .bin)
packets = decoder.decode_file("recording.wav")
for p in packets:
    print(p.payload_text, "CRC:", p.crc_valid, "SNR:", p.snr_est)
```

A recording may hold several packets; `decode_file` and `decode_iq` always
return a `list[Packet]`. Decode an in-memory IQ array the same way:

```python
from lora_receiver import load_iq
iq = load_iq("recording.cfile")
packets = decoder.decode_iq(iq)
```

## Streaming decode

Feed IQ chunks from an SDR or a GNU Radio pipe; packets are returned as soon
as they are fully received, even when a packet spans chunk boundaries:

```python
decoder = LoRaDecoder(sf=10, bw=125_000, fs=250_000, fc=437e6)

while True:
    chunk = read_iq_from_sdr(8192)          # complex baseband samples
    if not chunk:
        break
    for p in decoder.decode_stream(chunk):
        print(f"{p.timestamp_sec:.0f} {p.payload_text!r} {p.crc_valid}")

for p in decoder.flush():                   # decode the remaining tail
    print(p)
```

`fs` is the rate of the chunks you feed; the decoder resamples internally to
the LoRa bandwidth. Call `flush()` at end-of-transmission and `reset()` to
start over.

## Packet result type

Every decode path returns [`Packet`](lora_receiver/packet.py) objects:

| Attribute | Description |
|---|---|
| `ok` | True once a packet was found and its payload demodulated/decoded without error (does *not* mean the CRC passed) |
| `error` | Reason, when `ok` is False |
| `payload_bytes` | Raw payload bytes |
| `payload_text` | Payload decoded as UTF-8 (NUL-stripped) |
| `crc_valid` | True/False, or None when the packet has no CRC |
| `snr_est` | SNR estimate in dB from the preamble |
| `freq_offset_hz` | Estimated carrier frequency offset |
| `mode` | `'explicit'` or `'implicit'` header |
| `chase_used`, `chase_attempts`, `chase_stage` | Chase fallback diagnostics |
| `sample_start`, `payload_start`, `sample_end` | Sample positions at the decoder bandwidth rate |
| `time_start_sec` | `sample_start / bw` |
| `packet_index` | Ordering within a decode run |
| `sync`, `header_info`, `data_symbols`, `total_syms_needed` | Raw internals for diagnostics |

`Packet.to_dict()` returns a JSON-serializable dict for logging/persistence.
Sample positions are absolute within the recording for `decode_file` /
`decode_iq` / `decode_stream`, and relative to the input buffer for a direct
`full_decode` call.

`ok` and `crc_valid` are independent: `ok` means *a packet was found and
demodulated*, `crc_valid` is the data-integrity verdict. Filter on
`p.crc_valid is True` for guaranteed-good payloads; `ok=True,
crc_valid=False` is a received-but-corrupted packet, and `ok=False` means
no valid packet (see `error` for the reason).

## Implicit headers

Configure the decoder with the forced parameters when packets carry no header:

```python
decoder = LoRaDecoder(sf=10, bw=125_000, fs=125_000, fc=437e6,
                      impl_header=True, forced_payload_len=19,
                      forced_has_crc=True, forced_cr=1)
```

## Chase decoding

When a packet's hard-decision symbols fail to decode (e.g. CRC-16 mismatch),
`LoRaDecoder` can retry with a Chase decoder over the per-symbol FFT spectra:

```python
decoder = LoRaDecoder(sf=10, bw=125_000, fs=125_000, fc=437e6, chase_fallback=True)
packets = decoder.decode_file("recording.wav")
for p in packets:
    if p.chase_used:
        print("Recovered by Chase:", p.chase_stage, p.payload_text)
```

Two stages are tried in order (see `lora_receiver/chase.py`):

1. **Symbol-level Chase** — each symbol's spectrum is reduced to its top-K
   bins; the top1/top2 power ratio is the reliability. The hard symbols are
   decoded first, then single, pair and triple symbol flips are tried among
   the least-reliable positions, ranked by `-log(power ratio)`.
2. **Bit-level Chase** — spectra are converted to per-bit LLRs; the least
   reliable bits are flipped one at a time, then in pairs.

`chase_kwargs` can be passed to the `LoRaDecoder` constructor to tune the
Chase parameters.

## LoRaDecoder API

```python
LoRaDecoder(sf, bw, fs, fc, N_detect=3, N_preamble_up=8, N_netid=2,
            N_sfd_down=2.25, impl_header=False, forced_payload_len=None,
            forced_has_crc=True, forced_cr=1, chase_fallback=False,
            chase_kwargs=None, gate_ratio=2.5, strong_ratio=6.0,
            max_packet_syms=600)
```

| Parameter | Description |
|---|---|
| `sf` | Spreading factor (7-12) |
| `bw` | Bandwidth in Hz |
| `fs` | Sampling rate in Hz |
| `fc` | Center frequency in Hz |
| `N_detect` | Preamble detection windows (default: 3) |
| `N_preamble_up` | Number of preamble upchirps (default: 8) |
| `N_netid` | Net ID upchirps (default: 2) |
| `N_sfd_down` | SFD downchirps (default: 2.25) |
| `gate_ratio` / `strong_ratio` | Streaming preamble-gate thresholds |
| `max_packet_syms` | Streaming give-up bound for unfinished packets |

### Methods

- `decode_file(path, chunk_samples=2**16)` -> `list[Packet]` — decode all packets in a recording, **streaming it from disk in chunks** so memory stays bounded regardless of file size
- `decode_iq(iq)` -> `list[Packet]` — decode all packets in an in-memory buffer
- `decode_stream(chunk)` -> `list[Packet]` — feed a live IQ chunk
- `flush()` -> `list[Packet]` — decode the remaining tail; resets streaming state
- `reset()` — clear streaming state
- `full_decode(iq)` -> `Packet` — decode a single packet from a buffer (low-level)
- `sync(iq)` -> `(synced, payload_start, params)` — run the 3-stage synchronizer

### Helpers

- `load_iq(path)` — load a recording fully into memory (stereo `.wav`, raw interleaved float32 `.cfile`/`.dat`/`.bin`, int16 `.dat`)
- `iter_iq_chunks(path, chunk_samples=2**16)` — stream the same formats as complex64 chunks without loading the file; used internally by `decode_file`
- `save_packets(packets, out_dir)` — write raw payload `.bin` files + `metadata.jsonl`
- `sample_rate(path)` — WAV sample rate, or None for raw IQ files
- `estimate_snr(signal, downchirp, N, ...)` — SNR estimator used by the decoder

## Dependencies

- `numpy` — array operations and FFT
- `scipy` — signal resampling, WAV reading

## License

MIT — see [LICENSE](LICENSE).
