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
142 lines
4.0 KiB
Python
142 lines
4.0 KiB
Python
"""RYLR998 NETWORKID ↔ LoRa sync word mapping utilities.
|
||
|
||
The RYLR998 uses NETWORKID (0-255) directly as the LoRa sync word byte.
|
||
The sync word byte is transmitted as 2 CSS symbols, each encoding 4 bits:
|
||
- Sync symbol 1 = (NID >> 4) × 8 (high nibble × 8)
|
||
- Sync symbol 2 = (NID & 0x0F) × 8 (low nibble × 8)
|
||
|
||
Verified through SDR captures:
|
||
NID=3 (0x03): sync=[0, 24] ✓
|
||
NID=5 (0x05): sync=[0, 40] ✓
|
||
NID=17 (0x11): sync=[8, 8] ✓
|
||
NID=18 (0x12): sync=[8, 16] ✓
|
||
"""
|
||
|
||
|
||
def networkid_to_sync_word(nid: int) -> tuple[int, int]:
|
||
"""Convert RYLR998 NETWORKID to LoRa sync word symbol deltas.
|
||
|
||
Args:
|
||
nid: Network ID (0-255)
|
||
|
||
Returns:
|
||
Tuple of (sync_symbol_1_delta, sync_symbol_2_delta) where each
|
||
delta is the offset from the preamble bin in CSS demodulation.
|
||
|
||
Example:
|
||
>>> networkid_to_sync_word(18)
|
||
(8, 16)
|
||
>>> networkid_to_sync_word(0x34) # LoRaWAN public
|
||
(24, 32)
|
||
"""
|
||
if not 0 <= nid <= 255:
|
||
raise ValueError(f"NETWORKID must be 0-255, got {nid}")
|
||
high_nibble = (nid >> 4) & 0x0F
|
||
low_nibble = nid & 0x0F
|
||
return (high_nibble * 8, low_nibble * 8)
|
||
|
||
|
||
def sync_word_to_networkid(sync_deltas: tuple[int, int]) -> int:
|
||
"""Convert LoRa sync word symbol deltas back to RYLR998 NETWORKID.
|
||
|
||
Args:
|
||
sync_deltas: Tuple of (sync_symbol_1_delta, sync_symbol_2_delta)
|
||
|
||
Returns:
|
||
Network ID (0-255)
|
||
|
||
Example:
|
||
>>> sync_word_to_networkid((8, 16))
|
||
18
|
||
"""
|
||
high_nibble = sync_deltas[0] // 8
|
||
low_nibble = sync_deltas[1] // 8
|
||
return (high_nibble << 4) | low_nibble
|
||
|
||
|
||
def sync_word_byte_to_deltas(sync_byte: int) -> tuple[int, int]:
|
||
"""Convert standard LoRa sync word byte to CSS symbol deltas.
|
||
|
||
Standard LoRa sync words:
|
||
0x34 (52): Public LoRaWAN networks -> [24, 32]
|
||
0x12 (18): Private networks -> [8, 16]
|
||
|
||
Args:
|
||
sync_byte: Sync word byte (0-255)
|
||
|
||
Returns:
|
||
Tuple of symbol deltas
|
||
"""
|
||
return networkid_to_sync_word(sync_byte)
|
||
|
||
|
||
def networkid_from_symbols(sym1: int, sym2: int, sf: int = 9) -> int:
|
||
"""Extract NETWORKID from demodulated sync word symbols.
|
||
|
||
After CSS demodulation, the sync word symbols contain the bin values.
|
||
This function extracts the NETWORKID by reversing the modulation.
|
||
|
||
Args:
|
||
sym1: First sync word symbol bin value
|
||
sym2: Second sync word symbol bin value
|
||
sf: Spreading factor (determines N = 2^sf)
|
||
|
||
Returns:
|
||
Network ID (0-255)
|
||
|
||
Example:
|
||
>>> # After CSS demod with preamble at bin 0
|
||
>>> networkid_from_symbols(8, 16, sf=9)
|
||
18
|
||
"""
|
||
N = 1 << sf
|
||
# Sync word symbols are scaled by N/16 during modulation
|
||
# Each nibble n becomes bin = n * (N / 16) = n * (N >> 4)
|
||
scale = N >> 4
|
||
high_nibble = (sym1 // scale) & 0x0F
|
||
low_nibble = (sym2 // scale) & 0x0F
|
||
return (high_nibble << 4) | low_nibble
|
||
|
||
|
||
def validate_sync_word(deltas: tuple[int, int]) -> bool:
|
||
"""Check if sync word deltas are valid (multiples of 8, in range).
|
||
|
||
Args:
|
||
deltas: Tuple of (delta1, delta2)
|
||
|
||
Returns:
|
||
True if valid RYLR998 sync word pattern
|
||
"""
|
||
d1, d2 = deltas
|
||
return (
|
||
d1 % 8 == 0 and d2 % 8 == 0 and
|
||
0 <= d1 < 128 and 0 <= d2 < 128
|
||
)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
print("NETWORKID ↔ Sync Word Mapping Test")
|
||
print("=" * 50)
|
||
|
||
# Test all 256 possible NETWORKIDs
|
||
for nid in range(256):
|
||
deltas = networkid_to_sync_word(nid)
|
||
recovered = sync_word_to_networkid(deltas)
|
||
assert recovered == nid, f"Round-trip failed for NID={nid}"
|
||
|
||
print("✓ All 256 NETWORKID round-trips verified")
|
||
|
||
# Show some common values
|
||
test_cases = [
|
||
(3, "RYLR998 default 3"),
|
||
(5, "RYLR998 test"),
|
||
(17, "0x11"),
|
||
(18, "0x12 private"),
|
||
(52, "0x34 LoRaWAN public"),
|
||
]
|
||
|
||
print("\nCommon NETWORKID values:")
|
||
for nid, desc in test_cases:
|
||
d1, d2 = networkid_to_sync_word(nid)
|
||
print(f" NID={nid:3d} (0x{nid:02X}) {desc:20s} → sync=[{d1:3d}, {d2:3d}]")
|