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