gr-rylr998/examples/bladerf_rx.py
Ryan Malloy c839d225a8 Initial release: complete LoRa TX/RX for RYLR998 modems
GNU Radio Out-of-Tree module providing:
- Complete TX chain: PHYEncode → FrameGen → CSSMod
- Complete RX chain: CSSDemod → FrameSync → PHYDecode
- NETWORKID extraction/encoding (0-255 range)
- All SF (7-12) and CR (4/5-4/8) combinations
- Loopback tested with 24/24 configurations passing

Key features:
- Fractional SFD (2.25 downchirp) handling
- Gray encode/decode with proper inverse operations
- gr-lora_sdr compatible decode modes
- GRC block definitions and example flowgraphs
- Comprehensive documentation

Discovered RYLR998 sync word mapping:
  sync_bin_1 = (NETWORKID >> 4) * 8
  sync_bin_2 = (NETWORKID & 0x0F) * 8
2026-02-05 13:38:07 -07:00

128 lines
4.2 KiB
Python

#!/usr/bin/env python3
"""BladeRF RYLR998 receiver example.
Receives LoRa frames from an RYLR998 module via BladeRF SDR.
Usage:
python3 bladerf_rx.py --freq 915e6 --networkid 18
python3 bladerf_rx.py --freq 915e6 --sf 9 --cr 1 --verbose
"""
import sys
import argparse
import numpy as np
# Add parent directory for imports
sys.path.insert(0, '../python')
from rylr998 import CSSDemod, FrameSync, PHYDecode
def receive_frame(iq_samples: np.ndarray, sf: int = 9, cr: int = 1,
expected_networkid: int | None = None,
verbose: bool = False) -> dict | None:
"""Process IQ samples to decode a LoRa frame.
Args:
iq_samples: Complex IQ samples from SDR
sf: Spreading factor
cr: Coding rate (for implicit mode)
expected_networkid: Filter by NETWORKID (None = accept all)
verbose: Print debug info
Returns:
Dict with frame info, or None if no valid frame
"""
fs = 250e3 # Typical sample rate for LoRa
N = 1 << sf
# Frame sync
sync = FrameSync(sf=sf, sample_rate=fs, expected_networkid=expected_networkid)
result = sync.sync_from_samples(iq_samples)
if not result.found:
if verbose:
print("No frame detected")
return None
if verbose:
print(f"Frame detected: NETWORKID={result.networkid}, "
f"CFO={result.cfo_bin:.2f} bins, "
f"preamble={result.preamble_count} symbols")
# PHY decode
decoder = PHYDecode(sf=sf, cr=cr, has_crc=True)
cfo_int = int(round(result.cfo_bin))
frame = decoder.decode(result.data_symbols, cfo_bin=cfo_int)
if verbose:
print(f" header_ok={frame.header_ok}, crc_ok={frame.crc_ok}")
print(f" payload ({frame.payload_length}B): {frame.payload!r}")
return {
'networkid': result.networkid,
'cfo_bin': result.cfo_bin,
'preamble_count': result.preamble_count,
'header_ok': frame.header_ok,
'payload_length': frame.payload_length,
'coding_rate': frame.coding_rate,
'has_crc': frame.has_crc,
'crc_ok': frame.crc_ok,
'errors_corrected': frame.errors_corrected,
'payload': frame.payload,
}
def main():
parser = argparse.ArgumentParser(
description="BladeRF RYLR998 LoRa receiver")
parser.add_argument("--freq", type=float, default=915e6,
help="Center frequency (Hz)")
parser.add_argument("--sf", type=int, default=9,
help="Spreading factor (7-12)")
parser.add_argument("--cr", type=int, default=1,
help="Coding rate (1-4)")
parser.add_argument("--networkid", type=int, default=None,
help="Expected NETWORKID (None = any)")
parser.add_argument("--input", type=str, default=None,
help="Input file (complex64 raw IQ)")
parser.add_argument("--verbose", "-v", action="store_true",
help="Verbose output")
args = parser.parse_args()
if args.input:
# Load from file
print(f"Loading IQ from {args.input}")
iq = np.fromfile(args.input, dtype=np.complex64)
print(f"Loaded {len(iq)} samples")
result = receive_frame(
iq, sf=args.sf, cr=args.cr,
expected_networkid=args.networkid,
verbose=args.verbose
)
if result:
print(f"\nDecoded frame:")
print(f" NETWORKID: {result['networkid']}")
print(f" Payload: {result['payload']!r}")
print(f" CRC OK: {result['crc_ok']}")
else:
print("No valid frame found")
return 1
else:
# Live receive (requires bladeRF Python bindings)
print("Live receive mode requires bladeRF Python bindings")
print("Install with: pip install pybladerf")
print("\nAlternatively, capture IQ to a file:")
print(" bladeRF-cli -e 'set frequency rx 915M; set samplerate 250k; "
"rx config file=capture.raw format=bin n=250000; rx start; rx wait'")
print(f"\nThen run: {sys.argv[0]} --input capture.raw")
return 1
return 0
if __name__ == "__main__":
sys.exit(main())