Skip to content

Changelog

Unreleased

Added

  • README packet spectrogramscripts/plot_pktA_spectrogram.py renders the PU-3 packet from the packet-A sample recording as a single labelled spectrogram strip (preamble, sync, SFD down-chirps, header + payload + CRC), used as README Figure 1.
  • gr-lora-sdr streamingdecode_stream / flush now work with sync_algorithm='gr-lora-sdr': a new _GrFrameStream scanner runs the native-rate dechirp run detection incrementally over arbitrary-sized chunks (bounded memory) and decodes each candidate as soon as its window is buffered. decode_file / decode_iq reuse the same scanner, so both sync algorithms now stream.

Changed

  • save_packets prettier output — besides packet_XXXX.bin payloads and the append-only metadata.jsonl, save_packets now writes a readable packets.txt summary with each packet's Packet.__str__ detail table.

Removed

  • Chase diagnosis fields dropped from Packetchase_used, chase_attempts and chase_stage are no longer exposed on the result type; Chase remains a decoder mode and its outcome is read from ok / crc_valid as before. The chase_flip_frac / chase_attempts_avg metrics were removed from perf/awgn_chase_vs_hard.py accordingly.

v0.3.0 (2026-08-23)

Added

  • Automatic carrier offset estimationdecode_file / decode_iq / decode accept carrier_offset_hz='auto', which scans the recording for preamble runs and resolves the offset from the preamble upchirp and the first SFD downchirp (the timing term cancels, so the estimate is unambiguous over ±fs/2). See LoRaDecoder.estimate_carrier_offset.
  • DecoderSettings.sfo_ppm — optional clock-offset override for the gr-lora-sdr sync: a fixed ppm value, None (derive from residual CFO), or 'auto' (default) to sweep sfo_ppm_search and accept the first frame whose CRC validates.
  • DecoderSettings.max_os_factor — the gr-lora-sdr sync band-limits and decimates high-oversampled input down to at most this fs/bw before running the native-rate frame_sync port.
  • Header checksum verification — explicit headers are now validated with the LoRa 5-bit checksum (verify_header_checksum) before their fields are trusted.
  • Sub-sample timing refinement — when the hard decode of a fully buffered packet fails, the xhonneux sync probes timing_search sub-sample alignments and the surrounding symbol positions before giving up.

Changed

  • sync.py now implements Algorithm 1 of Xhonneux et al. exactly:
  • Fractional CFO from the last two upchirp pairs (Eq. 14) instead of an average over all preamble pairs.
  • Stage 2 uses the first post-detection window for s_tilde_up and the paper's M_tilde = N - s_tilde_up.
  • Stage 3 demodulates the single final upchirp (U7) and single first SFD downchirp (D1) instead of averaged windows.
  • Integer offsets via Eq. 17/18, with the integer STO unwrapped into [-N/2, N/2) so a slightly negative STO no longer pushes the payload a full symbol early.
  • gr-lora-sdr sync no longer returns failed candidatesdecode_file / decode_iq with sync_algorithm='gr-lora-sdr' now drop runs whose frame never synchronized (false-positive preamble detections on noise), matching the xhonneux path which already filtered them.
  • Packet.__str__ renders a detail tableprint(packet) shows an aligned key/value table with the full payload hex (wrapped at 32 bytes per line), full payload text, positions, SNR, frequency offset and CRC status. __repr__ keeps the compact one-line summary for lists and logs.
  • calc_payload_sym_num now uses the SF-dependent explicit-header spare nibbles (sf - 7) instead of a fixed 3, fixing symbol-count undercounts for SF < 10 (gr-lora_sdr packets at SF 7-9 previously dewhitened one nibble short).
  • apply_time_shift fractional delays are zero-padded so the FFT wrap cannot alias into the signal.
  • Short preambles — stage 3 now demodulates the final preamble upchirp from l + min(4, N_preamble_up - N_detect) instead of the paper's fixed U7 (l+4), so preambles with only 6 upchirps (6 up + 2 sync + 2.25 SFD) decode correctly when configured with N_preamble_up=6.
  • Header checksum fix — the c0 checksum bit used bit0(n1) where gr-lora_sdr's algorithm uses bit3(n2); valid headers were being rejected.
  • decode_file small-chunk performance — the sub-sample timing probe is only run once a packet is fully buffered, removing the per-chunk repeat cost while a packet is still streaming in.
  • Packaging — license and authorship follow the LibreCube convention (MIT, CONTRIBUTORS.txt); wheel and source distribution rebuilt for 0.3.0.

Fixed

  • Explicit-header checksum validation (see Changed).

Tests

  • Reference OTA packet-start positions updated for the unwrapped-STO sync.
  • tests/test_sync_paper.py — estimator identities (Eq. 19 = Eq. 20 at M_hat=0), Eq. 17/18 round-trips, noiseless fractional CFO/STO recovery, and end-to-end decode with injected integer/fractional CFO and STO.
  • scripts/gr_ref.py, scripts/gr_packet_test.py — generate packets with gr-lora_sdr's TX (SF 7-10, CR 1/2/4, with/without CRC, with/without offsets) and verify the payload against the transmitted ground truth and gr-lora_sdr's RX chain.
  • scripts/ota_test.py — decode the OTA capture dataset/lora_capture2.dat (4× "Hi from Shayan") at the reference positions and cross-check the payload with gr-lora_sdr's RX.
  • docs/guide/sync-algorithm.md — equation-by-equation mapping of the algorithm, the preamble-length generalization, and the N/2 disambiguation.

v0.2.0 (2026-08-06)

Breaking: the decoder now returns Packet objects on every path, and the streaming API replaces the old detect_packets.

Added

  • Packet result type — stable schema with ok / error / crc_valid, snr_est, freq_offset_hz, sample positions and diagnostics; to_dict() for JSON serialization.
  • LoRaDecoder.decode_stream / flush / reset — live chunked decoding; packets may straddle chunk boundaries and memory stays bounded.
  • decode_file streams from disk — new iter_iq_chunks reader (raw float32/int16 + stereo WAV) keeps memory bounded for multi-GB files.
  • estimate_snr — preamble-based SNR estimator (previously documented but never implemented).
  • load_iq / save_packets / sample_rate I/O helpers.
  • Stateful linear-phase FIR decimator, preamble gate, and pending/truncation handling in the streaming scan.

Removed

  • detect_packets (replaced by decode_iq) and the multi_packet module.

Changed

  • full_decode returns a Packet instead of a dict.
  • Constructor gains implicit-header, chase and streaming tuning parameters.
  • decode_file / decode_iq always return list[Packet].

Tests

  • Streaming equivalence across chunk sizes, packet splits across chunk boundaries, synthetic implicit-header round-trips at SF 7–12, SNR estimator, and I/O helpers (46 tests).

v0.1.0 (initial)

  • Pure-Python LoRa receiver with the 3-stage Xhonneux et al. synchronizer.
  • GNU Radio TX/RX flowgraphs and AWGN performance sweeps.