#!/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())