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
128 lines
4.2 KiB
Python
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())
|