Skip to content

About

the public repository for the LoRa ELRS Interceptor Decoder.

Resources

Stars

1 star

Watchers

0 watching

Forks

Latest commit

 

History

2 Commits

Folders and files

Repository files navigation

ELRS Interceptor

Passive intercept and decode system for ExpressLRS (ELRS) drone control links operating on 915 MHz LoRa. Detects active drone control and decodes RC channel commands (throttle, roll, pitch, yaw, arm state) transmitted from a ground control station (GCS) to a drone.


How It Works

ELRS uses LoRa chirp spread spectrum modulation with Frequency Hopping Spread Spectrum (FHSS) to maximize range and resistance to interference. The hop sequence is seeded by the pilot's binding phrase (a 6-byte UID). This system:

  1. Passively characterizes the RF environment via SDR to determine spreading factor (SF)
  2. Sits on a fixed channel waiting for the FHSS sequence to cross it and deliver a SYNC packet
  3. Extracts 3 of 4 UID bytes from the SYNC packet, then brute-forces the 4th (256 guesses)
  4. Reconstructs the hop table via the ELRS Fisher-Yates/LCG algorithm
  5. Locks on in real-time using FreeRTOS hardware interrupts and a microsecond timer anchor
  6. Dewhitens and decodes every subsequent RC data packet

Project Structure

elrs-interceptor/
│
├── README.md                    ← this file
│
├── phase0/
│   └── sdr_characterize.py      ← run on laptop with RTL-SDR before deploying hardware
│
└── firmware/
    ├── include/
    │   ├── elrs_common.h        ← shared constants, types, pin definitions
    │   ├── phase1_net.h
    │   ├── phase2_exploit.h
    │   ├── phase3_lockons.h
    │   └── phase4_autopsy.h
    └── src/
        ├── main.cpp             ← entry point, FreeRTOS task setup
        ├── phase1_net.cpp       ← LR1121 init, CRC, OTA detection, UID extraction
        ├── phase2_exploit.cpp   ← Fisher-Yates LCG, 256-guess brute force
        ├── phase3_lockons.cpp   ← hop task, timer anchor, dead-reckoning
        └── phase4_autopsy.cpp   ← dewhiten, 11-bit channel unpack, output

Hardware Requirements

Component Purpose Notes
ESP32 (240 MHz) Main MCU, FreeRTOS host ESP32-S3 preferred for faster SPI
LR1121 or SX1262 breakout LoRa transceiver Semtech LR1121 covers 900MHz + 2.4GHz
915 MHz antenna Reception range Yagi or patch for NLOS scenarios
RTL-SDR v3 (laptop) Phase 0 characterization only One-time setup step

Wiring (ESP32 → LR1121)

ESP32 GPIO    LR1121 Pin    Function
──────────    ──────────    ────────
GPIO 18       SCK           SPI clock       (16–18 MHz)
GPIO 19       MISO          SPI data out
GPIO 23       MOSI          SPI data in
GPIO 5        NSS           SPI chip select
GPIO 26       DIO1          Packet interrupt (ISR trigger)
GPIO 14       NRESET        Hardware reset
3.3V          VCC
GND           GND

Software Dependencies

Phase 0 (Python — laptop)

pip install numpy scipy pyrtlsdr matplotlib

Firmware (ESP32 — PlatformIO)

# platformio.ini
[env:esp32dev]
platform = espressif32
board = esp32dev
framework = espidf
lib_deps =
    jgromes/RadioLib@^6.4.0

Quickstart

Step 1 — Characterize with SDR

Connect RTL-SDR, tune to 902–928 MHz band, run:

cd phase0
python sdr_characterize.py

Output:

Detected SF: 7
Packet rate: ~250 Hz
Hop window:  4000 µs
BW: 500 kHz (confirmed)
SyncWord: 0x12 (hardcoded)

Take note of the detected SF. Update elrs_common.h:

#define ELRS_SF   7   // set from sdr_characterize.py output

Step 2 — Flash Firmware

cd firmware
pio run --target upload
pio device monitor --baud 115200

Step 3 — Observe Output

[PHASE1] Waiting for SYNC packet on 915.000 MHz...
[PHASE1] SYNC caught. OTA v2. UID: 0xA3 0x7F 0x2C [??]
[PHASE2] Brute forcing uid[3]... locked at 0x91 after 73 attempts
[PHASE3] Hop table generated. Anchored at t=0. Hopping...
[PHASE4] ACTIVE | Throttle: 68% | Roll: +12° | Pitch: -3° | Yaw: 0 | ARMED
[PHASE4] ACTIVE | Throttle: 71% | Roll: +8°  | Pitch: +1° | Yaw: 0 | ARMED

State Machine

         ┌─────────┐
  boot → │ HUNTING │ ← fallback on >MAX_MISSED consecutive misses
         └────┬────┘
              │ SYNC packet received + CRC pass
         ┌────▼──────┐
         │ EXPLOITING│  brute force uid[3] — avg ~128 hop intervals
         └────┬──────┘
              │ RC data packet CRC pass
         ┌────▼──────┐
         │  LOCKED   │  real-time hop tracking + decode
         └────┬──────┘
              │ >MAX_MISSED misses
              └──────────────────► HUNTING

Key Constants (ELRS 900 MHz)

Parameter Value Source
Physical sync word 0x12 ELRS/src/lib/SX126XDriver/SX126XDriver.cpp
Bandwidth 500 kHz Fixed across all ELRS 900 modes
LCG multiplier 2891336453 ELRS/src/lib/FHSS/FHSS.cpp
LCG increment 1 Same
LFSR whitening poly 0x69 ELRS/src/lib/FHSS/FHSS.cpp
CRC polynomial 0x07 (CRC8/CCITT) ELRS packet spec
SYNC packet type 0x01 ELRS OTA packet format
RC data packet type 0x00 ELRS OTA packet format
OTA v2 UID offset byte 3 ELRS v3.x source

Important Notes

  • This tool is for authorized security research and RF environment analysis only
  • Intercepting drone control signals may be subject to local regulations
  • ELRS is open-source; all constants above are sourced from the public GitHub repository
  • Crossfire (TBS) uses a proprietary protocol — this system targets ELRS only

About

the public repository for the LoRa ELRS Interceptor Decoder.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages