softlora 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. softlora-0.3.0/CONTRIBUTORS.txt +4 -0
  2. softlora-0.3.0/LICENSE +19 -0
  3. softlora-0.3.0/MANIFEST.in +5 -0
  4. softlora-0.3.0/PKG-INFO +157 -0
  5. softlora-0.3.0/README.md +120 -0
  6. softlora-0.3.0/docs/api/chase.md +3 -0
  7. softlora-0.3.0/docs/api/chirp.md +3 -0
  8. softlora-0.3.0/docs/api/coding.md +3 -0
  9. softlora-0.3.0/docs/api/decoder.md +29 -0
  10. softlora-0.3.0/docs/api/io.md +3 -0
  11. softlora-0.3.0/docs/api/packet.md +3 -0
  12. softlora-0.3.0/docs/api/sync.md +3 -0
  13. softlora-0.3.0/docs/api/utils.md +3 -0
  14. softlora-0.3.0/docs/changelog.md +152 -0
  15. softlora-0.3.0/docs/guide/chase.md +254 -0
  16. softlora-0.3.0/docs/guide/formats.md +68 -0
  17. softlora-0.3.0/docs/guide/packet-semantics.md +60 -0
  18. softlora-0.3.0/docs/guide/performance.md +97 -0
  19. softlora-0.3.0/docs/guide/pipeline.md +267 -0
  20. softlora-0.3.0/docs/guide/satellites.md +78 -0
  21. softlora-0.3.0/docs/guide/streaming.md +96 -0
  22. softlora-0.3.0/docs/guide/sync-algorithm.md +157 -0
  23. softlora-0.3.0/docs/index.md +66 -0
  24. softlora-0.3.0/docs/installation.md +48 -0
  25. softlora-0.3.0/docs/quickstart.md +86 -0
  26. softlora-0.3.0/examples/README.md +60 -0
  27. softlora-0.3.0/examples/decode_file.py +36 -0
  28. softlora-0.3.0/examples/decode_ota_capture.py +15 -0
  29. softlora-0.3.0/examples/decoder_settings.py +61 -0
  30. softlora-0.3.0/examples/implicit_header.py +29 -0
  31. softlora-0.3.0/examples/sdr_live.py +141 -0
  32. softlora-0.3.0/examples/streaming.py +74 -0
  33. softlora-0.3.0/examples/tutorial_decode_iq.py +42 -0
  34. softlora-0.3.0/pyproject.toml +57 -0
  35. softlora-0.3.0/setup.cfg +4 -0
  36. softlora-0.3.0/softlora/__init__.py +22 -0
  37. softlora-0.3.0/softlora/chase.py +338 -0
  38. softlora-0.3.0/softlora/chirp.py +22 -0
  39. softlora-0.3.0/softlora/coding.py +474 -0
  40. softlora-0.3.0/softlora/decoder.py +1551 -0
  41. softlora-0.3.0/softlora/gr_frame_sync.py +619 -0
  42. softlora-0.3.0/softlora/io.py +244 -0
  43. softlora-0.3.0/softlora/packet.py +244 -0
  44. softlora-0.3.0/softlora/py.typed +0 -0
  45. softlora-0.3.0/softlora/sync.py +372 -0
  46. softlora-0.3.0/softlora/utils.py +166 -0
  47. softlora-0.3.0/softlora.egg-info/PKG-INFO +157 -0
  48. softlora-0.3.0/softlora.egg-info/SOURCES.txt +57 -0
  49. softlora-0.3.0/softlora.egg-info/dependency_links.txt +1 -0
  50. softlora-0.3.0/softlora.egg-info/requires.txt +12 -0
  51. softlora-0.3.0/softlora.egg-info/top_level.txt +1 -0
  52. softlora-0.3.0/tests/__init__.py +0 -0
  53. softlora-0.3.0/tests/test_chase.py +183 -0
  54. softlora-0.3.0/tests/test_decoder.py +61 -0
  55. softlora-0.3.0/tests/test_io.py +115 -0
  56. softlora-0.3.0/tests/test_packet.py +98 -0
  57. softlora-0.3.0/tests/test_stream.py +249 -0
  58. softlora-0.3.0/tests/test_sync_paper.py +238 -0
  59. softlora-0.3.0/tests/tx_helpers.py +144 -0
@@ -0,0 +1,4 @@
1
+ # Contributors
2
+
3
+ Hamza <hamza.mohammed.hasan@gmail.com>
4
+ Shayan <shayan.majumder2@gmail.com>
softlora-0.3.0/LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ MIT License
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ SOFTWARE.
@@ -0,0 +1,5 @@
1
+ include CONTRIBUTORS.txt
2
+ recursive-include tests *.py
3
+ recursive-include examples *.py *.md
4
+ recursive-include docs *.md
5
+ recursive-include softlora *.py *.pyi py.typed
@@ -0,0 +1,157 @@
1
+ Metadata-Version: 2.4
2
+ Name: softlora
3
+ Version: 0.3.0
4
+ Summary: LoRa satellite receiver - sync, demodulate, and decode
5
+ Author-email: LibreCube <info@librecube.org>, Hamza Hassan <hamza.mohammed.hasan@gmail.com>
6
+ License: MIT License
7
+ Project-URL: Homepage, https://gitlab.com/librecube/prototypes/gsoc-lora-sat-receiver
8
+ Project-URL: Repository, https://gitlab.com/librecube/prototypes/gsoc-lora-sat-receiver
9
+ Project-URL: Bug Tracker, https://gitlab.com/librecube/prototypes/gsoc-lora-sat-receiver/-/issues
10
+ Project-URL: Documentation, https://librecube.gitlab.io/prototypes/gsoc-lora-sat-receiver/
11
+ Keywords: lora,satellite,sdr,chirp-spread-spectrum,gnuradio
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Science/Research
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.9
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Typing :: Typed
21
+ Classifier: Topic :: Communications :: Ham Radio
22
+ Classifier: Topic :: Scientific/Engineering :: Information Analysis
23
+ Requires-Python: >=3.9
24
+ Description-Content-Type: text/markdown
25
+ License-File: LICENSE
26
+ Requires-Dist: numpy
27
+ Requires-Dist: scipy
28
+ Provides-Extra: docs
29
+ Requires-Dist: mkdocs; extra == "docs"
30
+ Requires-Dist: mkdocs-material; extra == "docs"
31
+ Requires-Dist: mkdocstrings[python]; extra == "docs"
32
+ Provides-Extra: dev
33
+ Requires-Dist: build; extra == "dev"
34
+ Requires-Dist: twine; extra == "dev"
35
+ Requires-Dist: pytest; extra == "dev"
36
+ Dynamic: license-file
37
+
38
+ # SoftLoRa
39
+
40
+ [![Google Summer of Code 2026](https://img.shields.io/badge/Google%20Summer%20of%20Code-2026-fbb040?logo=google&logoColor=white)]()
41
+
42
+ Decode LoRa packets from radio recordings and live SDR streams, in pure Python.
43
+
44
+ Point it at an IQ recording and it finds the packets, corrects the frequency and
45
+ timing errors introduced by the radio link, and gives you back the payload.
46
+ No GNU Radio or C++ needed — just `numpy` and `scipy`.
47
+
48
+ It was built for satellite downlinks, where signals are weak and Doppler-shifted,
49
+ and is tested on synthetic data, over-the-air captures and real satellite passes.
50
+
51
+ Developed for Google Summer of Code 2026 with [LibreCube](https://librecube.org/).
52
+
53
+
54
+ ## Install
55
+
56
+ ```bash
57
+ pip install softlora
58
+ ```
59
+
60
+ Python 3.9+.
61
+
62
+ ## Decode a recording
63
+
64
+ ```python
65
+ from softlora import LoRaDecoder
66
+
67
+ decoder = LoRaDecoder(sf=10, bw=125_000, fs=125_000, fc=437e6)
68
+
69
+ for packet in decoder.decode_file("recording.wav"):
70
+ print(packet.payload_text, packet.crc_valid)
71
+ ```
72
+
73
+ The four arguments describe the signal you are decoding:
74
+
75
+ | Argument | Meaning |
76
+ |---|---|
77
+ | `sf` | Spreading factor, 7–12 |
78
+ | `bw` | LoRa bandwidth in Hz |
79
+ | `fs` | Sample rate of *your recording* in Hz |
80
+ | `fc` | Center frequency in Hz |
81
+
82
+ `.wav`, `.cfile`, `.dat` and `.bin` files are supported. A recording may hold
83
+ several packets, so you always get a list back.
84
+
85
+ ## Decode a live stream
86
+
87
+ Feed IQ chunks as they arrive from an SDR. Packets are returned as soon as they
88
+ are complete, even when one spans two chunks:
89
+
90
+ ```python
91
+ decoder = LoRaDecoder(sf=10, bw=125_000, fs=250_000, fc=437e6)
92
+
93
+ while receiving:
94
+ for packet in decoder.decode_stream(read_iq_from_sdr(8192)):
95
+ print(packet.payload_text)
96
+
97
+ for packet in decoder.flush(): # decode whatever is left in the buffer
98
+ print(packet)
99
+ ```
100
+
101
+ ## Reading the result
102
+
103
+ Every decode path returns [`Packet`](softlora/packet.py) objects. The
104
+ fields you will usually want:
105
+
106
+ ```python
107
+ packet.payload_bytes # the data
108
+ packet.payload_text # the same data decoded as UTF-8
109
+ packet.crc_valid # True when the payload passed its checksum
110
+ packet.snr_est # signal-to-noise estimate in dB
111
+ packet.ok # a packet was found and demodulated
112
+ ```
113
+
114
+ `ok` and `crc_valid` answer different questions. `ok=True, crc_valid=False`
115
+ means a packet arrived but was corrupted on the way. Filter on
116
+ `packet.crc_valid is True` when you only want trustworthy payloads.
117
+
118
+ ## A LoRa packet
119
+
120
+ Each packet starts with a preamble of plain upchirps, then a sync word, then a
121
+ 2.25-symbol start-of-frame delimiter (SFD) made of downchirps, and finally the
122
+ header, payload and CRC. All four regions are visible below.
123
+
124
+ <div align="center">
125
+
126
+ | <img src="docs/images/lora_packet.png" width="600"> |
127
+ |:---:|
128
+ | *A real LoRa packet received from the Polytech Universe-3 (PU-3) satellite (SF8, 62.5 kHz bandwidth). The payload is truncated for display.* |
129
+
130
+ </div>
131
+
132
+ ## Going further
133
+
134
+ | Topic | Where |
135
+ |---|---|
136
+ | Runnable examples | [`examples/`](examples/README.md) |
137
+ | Packets without a header | [Quick start](docs/quickstart.md) |
138
+ | Tuning the decoder (`DecoderSettings`) | [The decode pipeline](docs/guide/pipeline.md) |
139
+ | Rescuing weak packets with Chase decoding | [Chase guide](docs/guide/chase.md) |
140
+ | Live SDR chain via GNU Radio | [`examples/README.md`](examples/README.md#live-sdr-via-gnu-radio) |
141
+ | Doppler and carrier offset for satellites | [Ground-station use](docs/guide/satellites.md) |
142
+ | Benchmarks across SF 7–12 | [Performance](docs/guide/performance.md) |
143
+ | How synchronization works | [Sync algorithm](docs/guide/sync-algorithm.md) |
144
+ | Full API reference | [Documentation site](https://gsoc-lora-sat-receiver-7e5f4f.gitlab.io/) |
145
+
146
+ ## References
147
+
148
+ [1] M. Xhonneux, O. Afisiadis, D. Bol, and J. Louveaux, "A Low-Complexity LoRa
149
+ Synchronization Algorithm Robust to Sampling Time Offsets," *IEEE Internet of
150
+ Things Journal*, 2021. [arXiv:1912.11344](https://arxiv.org/abs/1912.11344)
151
+
152
+ [2] J. Tapparel, O. Afisiadis, P. Mayoraz, A. Balatsoukas-Stimming, and A. Burg,
153
+ "An Open-Source LoRa Physical Layer Prototype on GNU Radio," *SPAWC*, 2020.
154
+
155
+ ## License
156
+
157
+ MIT — see [LICENSE](LICENSE) and [CONTRIBUTORS.txt](CONTRIBUTORS.txt).
@@ -0,0 +1,120 @@
1
+ # SoftLoRa
2
+
3
+ [![Google Summer of Code 2026](https://img.shields.io/badge/Google%20Summer%20of%20Code-2026-fbb040?logo=google&logoColor=white)]()
4
+
5
+ Decode LoRa packets from radio recordings and live SDR streams, in pure Python.
6
+
7
+ Point it at an IQ recording and it finds the packets, corrects the frequency and
8
+ timing errors introduced by the radio link, and gives you back the payload.
9
+ No GNU Radio or C++ needed — just `numpy` and `scipy`.
10
+
11
+ It was built for satellite downlinks, where signals are weak and Doppler-shifted,
12
+ and is tested on synthetic data, over-the-air captures and real satellite passes.
13
+
14
+ Developed for Google Summer of Code 2026 with [LibreCube](https://librecube.org/).
15
+
16
+
17
+ ## Install
18
+
19
+ ```bash
20
+ pip install softlora
21
+ ```
22
+
23
+ Python 3.9+.
24
+
25
+ ## Decode a recording
26
+
27
+ ```python
28
+ from softlora import LoRaDecoder
29
+
30
+ decoder = LoRaDecoder(sf=10, bw=125_000, fs=125_000, fc=437e6)
31
+
32
+ for packet in decoder.decode_file("recording.wav"):
33
+ print(packet.payload_text, packet.crc_valid)
34
+ ```
35
+
36
+ The four arguments describe the signal you are decoding:
37
+
38
+ | Argument | Meaning |
39
+ |---|---|
40
+ | `sf` | Spreading factor, 7–12 |
41
+ | `bw` | LoRa bandwidth in Hz |
42
+ | `fs` | Sample rate of *your recording* in Hz |
43
+ | `fc` | Center frequency in Hz |
44
+
45
+ `.wav`, `.cfile`, `.dat` and `.bin` files are supported. A recording may hold
46
+ several packets, so you always get a list back.
47
+
48
+ ## Decode a live stream
49
+
50
+ Feed IQ chunks as they arrive from an SDR. Packets are returned as soon as they
51
+ are complete, even when one spans two chunks:
52
+
53
+ ```python
54
+ decoder = LoRaDecoder(sf=10, bw=125_000, fs=250_000, fc=437e6)
55
+
56
+ while receiving:
57
+ for packet in decoder.decode_stream(read_iq_from_sdr(8192)):
58
+ print(packet.payload_text)
59
+
60
+ for packet in decoder.flush(): # decode whatever is left in the buffer
61
+ print(packet)
62
+ ```
63
+
64
+ ## Reading the result
65
+
66
+ Every decode path returns [`Packet`](softlora/packet.py) objects. The
67
+ fields you will usually want:
68
+
69
+ ```python
70
+ packet.payload_bytes # the data
71
+ packet.payload_text # the same data decoded as UTF-8
72
+ packet.crc_valid # True when the payload passed its checksum
73
+ packet.snr_est # signal-to-noise estimate in dB
74
+ packet.ok # a packet was found and demodulated
75
+ ```
76
+
77
+ `ok` and `crc_valid` answer different questions. `ok=True, crc_valid=False`
78
+ means a packet arrived but was corrupted on the way. Filter on
79
+ `packet.crc_valid is True` when you only want trustworthy payloads.
80
+
81
+ ## A LoRa packet
82
+
83
+ Each packet starts with a preamble of plain upchirps, then a sync word, then a
84
+ 2.25-symbol start-of-frame delimiter (SFD) made of downchirps, and finally the
85
+ header, payload and CRC. All four regions are visible below.
86
+
87
+ <div align="center">
88
+
89
+ | <img src="docs/images/lora_packet.png" width="600"> |
90
+ |:---:|
91
+ | *A real LoRa packet received from the Polytech Universe-3 (PU-3) satellite (SF8, 62.5 kHz bandwidth). The payload is truncated for display.* |
92
+
93
+ </div>
94
+
95
+ ## Going further
96
+
97
+ | Topic | Where |
98
+ |---|---|
99
+ | Runnable examples | [`examples/`](examples/README.md) |
100
+ | Packets without a header | [Quick start](docs/quickstart.md) |
101
+ | Tuning the decoder (`DecoderSettings`) | [The decode pipeline](docs/guide/pipeline.md) |
102
+ | Rescuing weak packets with Chase decoding | [Chase guide](docs/guide/chase.md) |
103
+ | Live SDR chain via GNU Radio | [`examples/README.md`](examples/README.md#live-sdr-via-gnu-radio) |
104
+ | Doppler and carrier offset for satellites | [Ground-station use](docs/guide/satellites.md) |
105
+ | Benchmarks across SF 7–12 | [Performance](docs/guide/performance.md) |
106
+ | How synchronization works | [Sync algorithm](docs/guide/sync-algorithm.md) |
107
+ | Full API reference | [Documentation site](https://gsoc-lora-sat-receiver-7e5f4f.gitlab.io/) |
108
+
109
+ ## References
110
+
111
+ [1] M. Xhonneux, O. Afisiadis, D. Bol, and J. Louveaux, "A Low-Complexity LoRa
112
+ Synchronization Algorithm Robust to Sampling Time Offsets," *IEEE Internet of
113
+ Things Journal*, 2021. [arXiv:1912.11344](https://arxiv.org/abs/1912.11344)
114
+
115
+ [2] J. Tapparel, O. Afisiadis, P. Mayoraz, A. Balatsoukas-Stimming, and A. Burg,
116
+ "An Open-Source LoRa Physical Layer Prototype on GNU Radio," *SPAWC*, 2020.
117
+
118
+ ## License
119
+
120
+ MIT — see [LICENSE](LICENSE) and [CONTRIBUTORS.txt](CONTRIBUTORS.txt).
@@ -0,0 +1,3 @@
1
+ # Chase
2
+
3
+ ::: softlora.chase
@@ -0,0 +1,3 @@
1
+ # Chirp
2
+
3
+ ::: softlora.chirp
@@ -0,0 +1,3 @@
1
+ # Coding
2
+
3
+ ::: softlora.coding
@@ -0,0 +1,29 @@
1
+ # LoRaDecoder
2
+
3
+ The main entry point: constructed once with all decode parameters, then used
4
+ to decode files, in-memory buffers, or live streams.
5
+
6
+ ::: softlora.decoder.LoRaDecoder
7
+ options:
8
+ show_docstring_parameters: true
9
+ members:
10
+ - decode
11
+ - decode_file
12
+ - decode_iq
13
+ - decode_stream
14
+ - estimate_carrier_offset
15
+ - flush
16
+ - reset
17
+ - sync
18
+ - demodulate
19
+
20
+ # DecoderSettings
21
+
22
+ Advanced/internal decoder parameters, passed to `LoRaDecoder` via
23
+ `settings=`. It isolates everything that is not a radio/tunable parameter:
24
+ the sync algorithm, the decode mode, Chase tuning, the streaming gates and
25
+ the gr-lora-sdr SFO search.
26
+
27
+ ::: softlora.decoder.DecoderSettings
28
+ options:
29
+ show_docstring_parameters: true
@@ -0,0 +1,3 @@
1
+ # I/O helpers
2
+
3
+ ::: softlora.io
@@ -0,0 +1,3 @@
1
+ # Packet
2
+
3
+ ::: softlora.packet
@@ -0,0 +1,3 @@
1
+ # Sync
2
+
3
+ ::: softlora.sync
@@ -0,0 +1,3 @@
1
+ # Utils
2
+
3
+ ::: softlora.utils
@@ -0,0 +1,152 @@
1
+ # Changelog
2
+
3
+ ## Unreleased
4
+
5
+ ### Added
6
+
7
+ - **README packet spectrogram** — `scripts/plot_pktA_spectrogram.py` renders the
8
+ PU-3 packet from the packet-A sample recording as a single labelled
9
+ spectrogram strip (preamble, sync, SFD down-chirps, header + payload + CRC),
10
+ used as README Figure 1.
11
+ - **`gr-lora-sdr` streaming** — `decode_stream` / `flush` now work with
12
+ `sync_algorithm='gr-lora-sdr'`: a new `_GrFrameStream` scanner runs the
13
+ native-rate dechirp run detection incrementally over arbitrary-sized chunks
14
+ (bounded memory) and decodes each candidate as soon as its window is
15
+ buffered. `decode_file` / `decode_iq` reuse the same scanner, so both sync
16
+ algorithms now stream.
17
+
18
+ ### Changed
19
+
20
+ - **`save_packets` prettier output** — besides `packet_XXXX.bin` payloads and
21
+ the append-only `metadata.jsonl`, `save_packets` now writes a readable
22
+ `packets.txt` summary with each packet's `Packet.__str__` detail table.
23
+
24
+ ### Removed
25
+
26
+ - **Chase diagnosis fields dropped from `Packet`** — `chase_used`,
27
+ `chase_attempts` and `chase_stage` are no longer exposed on the result type;
28
+ Chase remains a decoder mode and its outcome is read from `ok` / `crc_valid`
29
+ as before. The `chase_flip_frac` / `chase_attempts_avg` metrics were removed
30
+ from `perf/awgn_chase_vs_hard.py` accordingly.
31
+
32
+ ## v0.3.0 (2026-08-23)
33
+
34
+ ### Added
35
+
36
+ - **Automatic carrier offset estimation** — `decode_file` / `decode_iq` /
37
+ `decode` accept `carrier_offset_hz='auto'`, which scans the recording for
38
+ preamble runs and resolves the offset from the preamble upchirp and the
39
+ first SFD downchirp (the timing term cancels, so the estimate is
40
+ unambiguous over `±fs/2`). See `LoRaDecoder.estimate_carrier_offset`.
41
+ - **`DecoderSettings.sfo_ppm`** — optional clock-offset override for the
42
+ `gr-lora-sdr` sync: a fixed ppm value, `None` (derive from residual CFO), or
43
+ `'auto'` (default) to sweep `sfo_ppm_search` and accept the first frame
44
+ whose CRC validates.
45
+ - **`DecoderSettings.max_os_factor`** — the `gr-lora-sdr` sync band-limits and
46
+ decimates high-oversampled input down to at most this `fs/bw` before running
47
+ the native-rate `frame_sync` port.
48
+ - **Header checksum verification** — explicit headers are now validated with
49
+ the LoRa 5-bit checksum (`verify_header_checksum`) before their fields are
50
+ trusted.
51
+ - **Sub-sample timing refinement** — when the hard decode of a fully buffered
52
+ packet fails, the xhonneux sync probes `timing_search` sub-sample
53
+ alignments and the surrounding symbol positions before giving up.
54
+
55
+ ### Changed
56
+
57
+ - **`sync.py` now implements Algorithm 1 of Xhonneux et al. exactly**:
58
+ - Fractional CFO from the **last two** upchirp pairs (Eq. 14) instead of an
59
+ average over all preamble pairs.
60
+ - Stage 2 uses the **first** post-detection window for `s_tilde_up` and the
61
+ paper's `M_tilde = N - s_tilde_up`.
62
+ - Stage 3 demodulates the **single** final upchirp (U7) and **single** first
63
+ SFD downchirp (D1) instead of averaged windows.
64
+ - Integer offsets via Eq. 17/18, with the integer STO unwrapped into
65
+ `[-N/2, N/2)` so a slightly negative STO no longer pushes the payload a
66
+ full symbol early.
67
+ - **`gr-lora-sdr` sync no longer returns failed candidates** — `decode_file`
68
+ / `decode_iq` with `sync_algorithm='gr-lora-sdr'` now drop runs whose frame
69
+ never synchronized (false-positive preamble detections on noise), matching
70
+ the `xhonneux` path which already filtered them.
71
+ - **`Packet.__str__` renders a detail table** — `print(packet)` shows an
72
+ aligned key/value table with the full payload hex (wrapped at 32 bytes per
73
+ line), full payload text, positions, SNR, frequency offset and CRC status.
74
+ `__repr__` keeps the compact one-line summary for lists and logs.
75
+ - `calc_payload_sym_num` now uses the SF-dependent explicit-header spare
76
+ nibbles (`sf - 7`) instead of a fixed 3, fixing symbol-count undercounts
77
+ for SF < 10 (gr-lora_sdr packets at SF 7-9 previously dewhitened one
78
+ nibble short).
79
+ - `apply_time_shift` fractional delays are zero-padded so the FFT wrap cannot
80
+ alias into the signal.
81
+ - **Short preambles** — stage 3 now demodulates the final preamble upchirp
82
+ from `l + min(4, N_preamble_up - N_detect)` instead of the paper's fixed
83
+ U7 (`l+4`), so preambles with only 6 upchirps (6 up + 2 sync + 2.25 SFD)
84
+ decode correctly when configured with `N_preamble_up=6`.
85
+ - **Header checksum fix** — the `c0` checksum bit used `bit0(n1)` where
86
+ gr-lora_sdr's algorithm uses `bit3(n2)`; valid headers were being rejected.
87
+ - **`decode_file` small-chunk performance** — the sub-sample timing probe is
88
+ only run once a packet is fully buffered, removing the per-chunk repeat
89
+ cost while a packet is still streaming in.
90
+ - **Packaging** — license and authorship follow the LibreCube convention
91
+ (MIT, `CONTRIBUTORS.txt`); wheel and source distribution rebuilt for 0.3.0.
92
+
93
+ ### Fixed
94
+
95
+ - Explicit-header checksum validation (see Changed).
96
+
97
+ ### Tests
98
+
99
+ - Reference OTA packet-start positions updated for the unwrapped-STO sync.
100
+ - `tests/test_sync_paper.py` — estimator identities (Eq. 19 = Eq. 20 at
101
+ `M_hat=0`), Eq. 17/18 round-trips, noiseless fractional CFO/STO recovery,
102
+ and end-to-end decode with injected integer/fractional CFO and STO.
103
+ - `scripts/gr_ref.py`, `scripts/gr_packet_test.py` — generate packets with
104
+ gr-lora_sdr's TX (SF 7-10, CR 1/2/4, with/without CRC, with/without offsets)
105
+ and verify the payload against the transmitted ground truth and gr-lora_sdr's
106
+ RX chain.
107
+ - `scripts/ota_test.py` — decode the OTA capture `dataset/lora_capture2.dat`
108
+ (4× "Hi from Shayan") at the reference positions and cross-check the payload
109
+ with gr-lora_sdr's RX.
110
+ - `docs/guide/sync-algorithm.md` — equation-by-equation mapping of the
111
+ algorithm, the preamble-length generalization, and the `N/2` disambiguation.
112
+
113
+ ## v0.2.0 (2026-08-06)
114
+
115
+ Breaking: the decoder now returns `Packet` objects on every path, and the
116
+ streaming API replaces the old `detect_packets`.
117
+
118
+ ### Added
119
+
120
+ - **`Packet` result type** — stable schema with `ok` / `error` / `crc_valid`,
121
+ `snr_est`, `freq_offset_hz`, sample positions and diagnostics; `to_dict()`
122
+ for JSON serialization.
123
+ - **`LoRaDecoder.decode_stream` / `flush` / `reset`** — live chunked decoding;
124
+ packets may straddle chunk boundaries and memory stays bounded.
125
+ - **`decode_file` streams from disk** — new `iter_iq_chunks` reader
126
+ (raw float32/int16 + stereo WAV) keeps memory bounded for multi-GB files.
127
+ - **`estimate_snr`** — preamble-based SNR estimator (previously documented
128
+ but never implemented).
129
+ - **`load_iq` / `save_packets` / `sample_rate`** I/O helpers.
130
+ - Stateful linear-phase FIR decimator, preamble gate, and pending/truncation
131
+ handling in the streaming scan.
132
+
133
+ ### Removed
134
+
135
+ - `detect_packets` (replaced by `decode_iq`) and the `multi_packet` module.
136
+
137
+ ### Changed
138
+
139
+ - `full_decode` returns a `Packet` instead of a dict.
140
+ - Constructor gains implicit-header, chase and streaming tuning parameters.
141
+ - `decode_file` / `decode_iq` always return `list[Packet]`.
142
+
143
+ ### Tests
144
+
145
+ - Streaming equivalence across chunk sizes, packet splits across chunk
146
+ boundaries, synthetic implicit-header round-trips at SF 7–12, SNR estimator,
147
+ and I/O helpers (46 tests).
148
+
149
+ ## v0.1.0 (initial)
150
+
151
+ - Pure-Python LoRa receiver with the 3-stage Xhonneux et al. synchronizer.
152
+ - GNU Radio TX/RX flowgraphs and AWGN performance sweeps.