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
2.8 KiB
2.8 KiB
RYLR998 NETWORKID ↔ LoRa Sync Word Mapping
Discovery
Through SDR captures and analysis, we discovered that the RYLR998 module's NETWORKID (0-255) maps directly to the LoRa sync word byte using a simple nibble-to-bin transformation.
The Mapping
Sync Word Byte: 0xHH where H = high nibble, L = low nibble
CSS Symbol 1 (sync_delta_1) = H × 8 = (NID >> 4) × 8
CSS Symbol 2 (sync_delta_2) = L × 8 = (NID & 0x0F) × 8
The factor of 8 is a fixed constant used by RYLR998 (verified through SDR captures). This differs from standard LoRa which would use N/16 scaling.
Verification
| NETWORKID | Hex | High Nibble | Low Nibble | Sync Bin 1 | Sync Bin 2 | Verified |
|---|---|---|---|---|---|---|
| 3 | 0x03 | 0 | 3 | 0 | 24 | ✓ |
| 5 | 0x05 | 0 | 5 | 0 | 40 | ✓ |
| 17 | 0x11 | 1 | 1 | 8 | 8 | ✓ |
| 18 | 0x12 | 1 | 2 | 8 | 16 | ✓ |
| 52 | 0x34 | 3 | 4 | 24 | 32 | (LoRaWAN) |
Standard LoRa Sync Words
| Name | Sync Byte | NETWORKID | Sync Bins |
|---|---|---|---|
| Private | 0x12 | 18 | [8, 16] |
| LoRaWAN Public | 0x34 | 52 | [24, 32] |
Code Implementation
Python
def networkid_to_sync_word(nid: int) -> tuple[int, int]:
"""Convert NETWORKID to sync word symbol deltas."""
high_nibble = (nid >> 4) & 0x0F
low_nibble = nid & 0x0F
return (high_nibble * 8, low_nibble * 8)
def sync_word_to_networkid(deltas: tuple[int, int]) -> int:
"""Convert sync word deltas back to NETWORKID."""
high_nibble = deltas[0] // 8
low_nibble = deltas[1] // 8
return (high_nibble << 4) | low_nibble
Full Range Table
NID=0 (0x00) → sync=[ 0, 0]
NID=1 (0x01) → sync=[ 0, 8]
NID=2 (0x02) → sync=[ 0, 16]
...
NID=16 (0x10) → sync=[ 8, 0]
NID=17 (0x11) → sync=[ 8, 8]
NID=18 (0x12) → sync=[ 8, 16]
...
NID=255 (0xFF) → sync=[120, 120]
Why This Matters
- Interoperability: You can decode RYLR998 traffic with standard LoRa hardware by knowing the sync word
- Filtering: Filter frames by NETWORKID in software without full decode
- Testing: Generate RYLR998-compatible frames from any SDR
SDR Capture Notes
When capturing RYLR998 frames:
- The preamble consists of 8+ upchirps at bin 0
- Sync word is 2 upchirps with bins = nibble × (N/16)
- SFD is 2.25 downchirps
- Data follows with the encoded payload
The sync word symbols are measured relative to the preamble bin (which represents the carrier frequency offset).