gr-rylr998/examples/bladerf_tx.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

113 lines
3.6 KiB
Python

#!/usr/bin/env python3
"""BladeRF RYLR998 transmitter example.
Transmits LoRa frames compatible with RYLR998 modules via BladeRF SDR.
Usage:
python3 bladerf_tx.py --freq 915e6 --networkid 18 --payload "Hello"
python3 bladerf_tx.py --output frame.raw --sf 9 --payload "Test"
"""
import sys
import argparse
import numpy as np
# Add parent directory for imports
sys.path.insert(0, '../python')
from rylr998 import PHYEncode, FrameGen
def generate_frame(payload: bytes, sf: int = 9, cr: int = 1,
networkid: int = 18, preamble_len: int = 8,
verbose: bool = False) -> np.ndarray:
"""Generate a LoRa frame as IQ samples.
Args:
payload: Payload bytes
sf: Spreading factor
cr: Coding rate (1-4)
networkid: RYLR998 NETWORKID
preamble_len: Number of preamble symbols
verbose: Print debug info
Returns:
Complex64 IQ samples
"""
fs = 125e3 # Sample rate = bandwidth
N = 1 << sf
if verbose:
print(f"Generating frame: SF{sf} CR4/{cr+4} NETWORKID={networkid}")
print(f" Payload ({len(payload)}B): {payload!r}")
# PHY encode
encoder = PHYEncode(sf=sf, cr=cr, has_crc=True)
data_bins = encoder.encode(payload)
if verbose:
print(f" Encoded: {len(data_bins)} symbols")
# Frame generate
frame_gen = FrameGen(sf=sf, sample_rate=fs, networkid=networkid,
preamble_len=preamble_len)
iq = frame_gen.generate_frame(data_bins)
if verbose:
duration_ms = len(iq) / fs * 1000
print(f" Generated: {len(iq)} samples ({duration_ms:.2f} ms)")
return iq
def main():
parser = argparse.ArgumentParser(
description="BladeRF RYLR998 LoRa transmitter")
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=18,
help="NETWORKID (0-255)")
parser.add_argument("--preamble", type=int, default=8,
help="Preamble length (symbols)")
parser.add_argument("--payload", type=str, default="Hello, LoRa!",
help="Payload string")
parser.add_argument("--output", "-o", type=str, default=None,
help="Output file (complex64 raw IQ)")
parser.add_argument("--verbose", "-v", action="store_true",
help="Verbose output")
args = parser.parse_args()
# Generate frame
iq = generate_frame(
args.payload.encode(),
sf=args.sf,
cr=args.cr,
networkid=args.networkid,
preamble_len=args.preamble,
verbose=args.verbose
)
if args.output:
# Save to file
iq.tofile(args.output)
print(f"Wrote {len(iq)} samples to {args.output}")
print(f"\nTo transmit with bladeRF-cli:")
print(f" bladeRF-cli -e 'set frequency tx {args.freq/1e6:.1f}M; "
f"set samplerate 125k; set bandwidth 125k; "
f"tx config file={args.output} format=bin; tx start; tx wait'")
else:
# Live transmit (requires bladeRF Python bindings)
print("Live transmit mode requires bladeRF Python bindings")
print("Install with: pip install pybladerf")
print(f"\nAlternatively, save to file with: {sys.argv[0]} -o frame.raw")
return 0
if __name__ == "__main__":
sys.exit(main())