Ryan Malloy 20abda421a Fix CFO estimation and timing for loopback tests
Two fixes for the frame sync timing bug reported by uart-agent:

1. CFO Overwritten by Timing Refinement
   - The _refine_symbol_boundary() returns a bin that reflects timing
     offset, not CFO. For aligned loopback signals, any timing shift k
     produces bin=k, incorrectly interpreted as CFO.
   - Fix: Keep CFO from state machine instead of overwriting.

2. SFD Correlation Noise Issues
   - For perfectly aligned signals, skip SFD correlation and use known
     frame structure offset (preamble_count + 4.25 symbols).
   - For real captures, use SFD correlation with adjusted search start.

Also updates SFD search start from (preamble_count + 1) to
(preamble_count + 3) for real captures to match existing decoder.

Loopback test: 50/50 seeds pass (100%)
Real SDR capture: All 10 bins match existing decoder
2026-02-07 04:28:39 -07:00

gr-rylr998

GNU Radio Out-of-Tree Module for RYLR998 LoRa Modems

Overview

gr-rylr998 provides complete TX/RX blocks for RYLR998 LoRa modems, implementing the full PHY layer compatible with the Semtech LoRa standard.

Key Features

  • Complete TX/RX chains for LoRa modulation/demodulation
  • NETWORKID support - Extracts and encodes RYLR998 NETWORKID from sync word
  • gr-lora_sdr compatible - Uses the same signal processing chain
  • Python-only - No C++ compilation required (GNU Radio 3.10+)
  • Well-documented - Educational comments throughout

Installation

From Source (Development)

cd gr-rylr998
pip install -e .

With CMake (GNU Radio Integration)

mkdir build && cd build
cmake ..
make
sudo make install

Quick Start

Python API

from rylr998 import PHYEncode, PHYDecode, FrameGen, FrameSync
import numpy as np

# === Transmit ===
encoder = PHYEncode(sf=9, cr=1, has_crc=True)
frame_gen = FrameGen(sf=9, networkid=18)

payload = b"Hello, LoRa!"
bins = encoder.encode(payload)
iq = frame_gen.generate_frame(bins)

# === Receive (loopback) ===
sync = FrameSync(sf=9)
result = sync.sync_from_samples(iq)

decoder = PHYDecode(sf=9)
# For loopback: use_grlora_gray=False, soft_decoding=True
# For real SDR captures: use defaults (True, False)
frame = decoder.decode(
    result.data_symbols,
    cfo_bin=int(result.cfo_bin),
    use_grlora_gray=False,
    soft_decoding=True
)

print(f"NETWORKID: {result.networkid}")
print(f"Payload: {frame.payload}")
print(f"CRC OK: {frame.crc_ok}")

Command Line

# Run loopback test
python examples/loopback_test.py

# Test all SF/CR combinations
python examples/loopback_test.py --all

# Generate TX frame
python examples/bladerf_tx.py --payload "Test" --output frame.raw

# Decode from file
python examples/bladerf_rx.py --input capture.raw --verbose

Block Inventory

Block Type Description
css_demod Demod FFT-based CSS demodulator
css_mod Mod Chirp generator
frame_sync Sync Preamble + NETWORKID detection
frame_gen Framing Preamble + sync word + SFD
phy_decode PHY Gray → deinterleave → Hamming → dewhiten
phy_encode PHY Whiten → Hamming → interleave → Gray
rylr998_rx Hier Complete RX chain
rylr998_tx Hier Complete TX chain

NETWORKID Mapping

The RYLR998 NETWORKID (0-255) maps directly to the LoRa sync word:

sync_bin_1 = (NETWORKID >> 4) * 8   # High nibble × 8
sync_bin_2 = (NETWORKID & 0x0F) * 8 # Low nibble × 8
NETWORKID Sync Bins Use
18 (0x12) [8, 16] Private networks
52 (0x34) [24, 32] LoRaWAN public

Directory Structure

gr-rylr998/
├── python/rylr998/     # Python module
│   ├── __init__.py
│   ├── networkid.py    # NETWORKID utilities
│   ├── css_demod.py    # CSS demodulator
│   ├── css_mod.py      # CSS modulator
│   ├── phy_decode.py   # PHY RX chain
│   ├── phy_encode.py   # PHY TX chain
│   ├── frame_sync.py   # Frame synchronization
│   └── frame_gen.py    # Frame generation
├── grc/                # GRC block definitions (.yml)
├── examples/           # Example scripts and flowgraphs
│   ├── loopback_test.py
│   ├── bladerf_rx.py
│   ├── bladerf_tx.py
│   ├── rylr998_loopback.grc
│   ├── rylr998_bladerf_rx.grc
│   └── rylr998_bladerf_tx.grc
├── docs/               # Documentation
│   ├── BLOCK_REFERENCE.md
│   ├── NETWORKID_MAPPING.md
│   └── GRC_FLOWGRAPHS.md
├── CMakeLists.txt      # CMake build
└── pyproject.toml      # Python packaging

Documentation

Document Description
BLOCK_REFERENCE.md All blocks, parameters, troubleshooting
NETWORKID_MAPPING.md RYLR998 NETWORKID ↔ sync word mapping
GRC_FLOWGRAPHS.md GNU Radio Companion usage guide

Dependencies

  • Python 3.10+
  • NumPy
  • GNU Radio 3.10+ (optional, for GRC integration)

References

License

GPL-3.0-or-later

Description
GNU Radio OOT module for RYLR998 LoRa modems - complete TX/RX with NETWORKID support
Readme 117 KiB
Languages
Python 97.2%
CMake 2.8%