gr-rylr998/python/rylr998/networkid.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

142 lines
4.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""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}]")