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
83 lines
2.8 KiB
Markdown
83 lines
2.8 KiB
Markdown
# 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
|
||
|
||
```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
|
||
|
||
1. **Interoperability**: You can decode RYLR998 traffic with standard LoRa hardware by knowing the sync word
|
||
2. **Filtering**: Filter frames by NETWORKID in software without full decode
|
||
3. **Testing**: Generate RYLR998-compatible frames from any SDR
|
||
|
||
## SDR Capture Notes
|
||
|
||
When capturing RYLR998 frames:
|
||
|
||
1. The preamble consists of 8+ upchirps at bin 0
|
||
2. Sync word is 2 upchirps with bins = nibble × (N/16)
|
||
3. SFD is 2.25 downchirps
|
||
4. Data follows with the encoded payload
|
||
|
||
The sync word symbols are measured relative to the preamble bin (which represents the carrier frequency offset).
|