LoRa in Rafe — two separate implementations¶
Rafe has two independent LoRa implementations, deliberately kept apart:
| Audio-band CSS | Real (Semtech) LoRa | |
|---|---|---|
| Module | app/radio/loracss.py |
app/radio/lorasdr/ |
| Path | IC-705 SSB audio (300–3000 Hz) | LimeSDR wideband IQ (UHF) |
| Bandwidth | ~1.5 kHz (audio) | 125–500 kHz |
| Interoperable with real LoRa? | No (CSS family only) | Yes (Semtech PHY) |
| In the codec SDK / mode picker? | Yes (loracss*) |
No (SDR subsystem, like AIS/satdump) |
| Status | complete, in use | PHY + TX/RX validated in loopback; on-air pending |
They share only the idea of chirp spread spectrum. Do not confuse them.
1. Audio-band CSS (loracss.py)¶
A narrowband CSS mode that fits an SSB passband — run it through the IC-705 like
any other keyboard mode. Whitening + Hamming(8,4) FEC + interleaving + CRC-8;
selectable SF/BW sub-modes (loracss-fast/loracss/loracss-robust). See
docs/codec-sdk.md. Not Semtech-compatible.
2. Real LoRa on the LimeSDR (app/radio/lorasdr/)¶
The Semtech-interoperable transceiver. Real LoRa is ≥125 kHz in the UHF ISM bands, so it cannot go through the radio's SSB path — it taps the LimeSDR Mini directly.
RX: SoapySDR IQ -> resample to 2*BW -> chirp demod -> symbols -> PHY decode -> payload
TX: payload -> PHY encode -> chirp modulate -> resample to samp_rate -> SoapySDR IQ
phy.py— the symbol↔byte codec: Gray, diagonal interleaver, Hamming (CR 4/5–4/8), whitening, explicit header + GF(2) checksum, CRC-16, LDRO. Validated interop-correct against jkadbear/LoRaPHY's known-answer vector (a real SF12/BW125k frame decodes to payload01..09, CRCBA2E) and round-trips every SF7–12 × CR4/5–4/8.modem.py— chirp IQ modulate + demod. Frame = preamble + sync word + 2.25 down-chirp SFD + data. The sync word and preamble length are parameters (Meshtastic0x2b/16, private0x12/8, …). Matched-filter frame sync + dechirp/folded-FFT symbols. Full baseband loopback passes every SF/BW/CR to −3 dB.sdr.py— RX:LoRaReceiver(threaded SoapySDR RX),LoRaFramer(streaming glue),decode_iq_file()(offline). TX:build_tx_iq()(payload → IQ),LoRaTransmitter(SoapySDR TX stream),send_meshtastic(). Plus a CLI covering both (--tx).
Validation status¶
- ✅ PHY: interop-correct (known-answer vector + exhaustive round-trip).
- ✅ Baseband loopback: encode→chirp→demod→decode across all SF/BW/CR, to ~−3 dB.
- ✅ Offline SDR chain: cf32/cs16 capture → resample → decode across SF/CR.
- ✅ TX:
build_tx_iq→ framer decode across all SF/BW/CR; full Meshtastic TX→RX with the real0x2bsync word; sync-word isolation (a frame sent with one sync word won't decode under another). SoapySDR TX plumbing is prod-testable but hasn't keyed a radio — seedocs/lora-open-issues.md§D/§F. - ✅ CFO/STO sync: dechirp-run preamble detection (CFO-immune), fractional CFO from preamble phase drift, sub-bin up/down-chirp CFO/STO split with the N/2 grid ambiguity arbitrated by sync-word peak quality. Validated to ±22% of BW carrier offset (±28 kHz at BW125k — beyond ±30 ppm crystals at 868 MHz) at arbitrary sample offsets, incl. a 100-case randomised torture run. Remaining refinement: SFO drift compensation over very long max-length SF11/SF12 frames.
- ⏳ On-air interop: definitive test is the prod LimeSDR + a real LoRa/ Meshtastic node. Not yet done.
Testing on prod¶
Offline, against a recorded capture (no node needed to exercise the chain):
# record with any SoapySDR tool, then:
python -m app.radio.lorasdr.sdr --file capture.cf32 --sf 7 --bw 125e3 --samp 500e3
Live off-air (needs a LoRa transmitter in range; tune close to the channel):
# EU 868.1 MHz, SF7, BW125k, CR4/5
python -m app.radio.lorasdr.sdr --freq 868.1e6 --sf 7 --bw 125e3 --samp 500e3 --cr 1
# US 915 MHz / 70cm 433.175 MHz likewise
Meshtastic uses region presets around 868/915/433 MHz with specific SF/BW/CR per
preset — match --sf/--bw/--cr to the preset to decode it.
Transmit (loopback-validated, not yet keyed on a real radio — see
docs/lora-open-issues.md §D/§F for the on-air plan and RF-power notes):
# raw LoRa frame
python -m app.radio.lorasdr.sdr --tx "hello" --freq 868.1e6 --sf 7 --bw 125e3 --sync 0x12
# Meshtastic text on the public LongFast channel (auto 0x2b sync + preamble 16)
python -m app.radio.lorasdr.sdr --tx "de M0SUP" --meshtastic --preset LongFast \
--freq <derived-freq> --from-node 0xdecafbad
3. Meshtastic decoder (lorasdr/meshtastic.py)¶
Meshtastic is LoRa-PHY with its own thin app layer — and it's what's actually on the air in the hobbyist bands, so this decodes it on top of the PHY:
LoRa payload bytes -> 16-byte mesh header -> AES-CTR decrypt -> Data protobuf
-> portnum + application data (text / position / node-info / ...)
- Header: to / from / id / flags (hop limit, want-ack) / channel-hash.
- Crypto: AES-CTR with the channel key; nonce = packetId(8 LE) + fromNode(4 LE). The public LongFast default key is built in, so public mesh traffic is readable; supply your own PSKs for private channels. AES is pure Python (no dependency) and verified against the FIPS-197 known-answer vectors.
- App decode:
Dataprotobuf is hand-parsed; text messages become strings, positions become lat/lon, node-info becomes names. Other ports are identified by name. - Wrong keys are rejected by the channel-hash hint (as the firmware does), not by guessing at garbage.
Constants (default key d4f1bb3a…6901, nonce layout, header, port numbers)
transcribed from the Meshtastic firmware (CryptoEngine / Channels) + protobufs.
Turn it on in the SDR path with --meshtastic and a modem preset:
# decode public Meshtastic on the EU LongFast channel:
python -m app.radio.lorasdr.sdr --freq 869.525e6 --preset LongFast --meshtastic
# a private channel: --psk <index> (or extend to pass a raw key)
Validated: FIPS-197 AES, text/position round-trips, and a full IQ → LoRa → Meshtastic-text decode (with carrier offset). Presets: ShortTurbo/Fast/Slow, MediumFast/Slow, LongFast (default)/Moderate/Slow. Remaining interop caveat: the AES-CTR counter-increment for payloads > 16 bytes uses the NIST standard (big-endian +1) — correct for single-block messages regardless; confirm on a real multi-block capture.
Licensing¶
The LoRa PHY constants (whitening table, header checksum matrix) are transcribed as protocol facts from jkadbear/LoRaPHY (MIT); the Meshtastic constants from the Meshtastic firmware (GPL-3) and protobufs. Credited in-source.