Multi-channel frequency hopping interference system for 2.4GHz ISM band research, built on the ESP32-S3 platform.
A portable, multi-node system that uses coordinated 802.11 packet injection to study RF interference patterns in the 2.4GHz band. The system employs three complementary techniques — broadcast deauthentication, CTS-to-Self NAV reservation, and noise floor saturation — across a pseudo-random frequency hopping sequence synchronized via ESP-NOW.
- Architecture Overview
- Project Structure
- Bill of Materials
- Prerequisites
- Setup & Installation
- Configuration
- Flashing
- Usage
- Deployment Configurations
- DJI OcuSync Calibration
- Serial Monitor Output
- Troubleshooting
- Technical Reference
- Version History
┌──────────────────────────────────────────────────────────────┐
│ ESP32-S3 Dual-Core │
│ │
│ ┌─────────────────────┐ ┌──────────────────────────────┐ │
│ │ CORE 0 │ │ CORE 1 │ │
│ │ │ │ │ │
│ │ ┌───────────────┐ │ │ ┌────────────────────────┐ │ │
│ │ │ TX Task │ │ │ │ Hop Task │ │ │
│ │ │ Priority: 15 │ │ │ │ Priority: 10 │ │ │
│ │ │ WDT: OFF │◄─┼─┬──┼─►│ LFSR Channel Hopping │ │ │
│ │ │ │ │ │ │ │ MAC Rotation │ │ │
│ │ │ • Deauth TX │ │ │ │ │ ESP-NOW Sync │ │ │
│ │ │ • CTS TX │ │ │ │ └────────────────────────┘ │ │
│ │ │ • Noise TX │ │ │ │ │ │
│ │ └───────────────┘ │ │ │ ┌────────────────────────┐ │ │
│ │ │ │ │ │ Orchestrator Task │ │ │
│ └─────────────────────┘ │ │ │ Priority: 8 │ │ │
│ │ │ │ (if ROLE_ORCHESTRATOR)│ │ │
│ Binary │ │ └────────────────────────┘ │ │
│ Semaphore ──┘ │ │ │
│ (tx_gate) │ ┌────────────────────────┐ │ │
│ │ │ Arduino loop() │ │ │
│ │ │ Priority: 1 │ │ │
│ │ │ Stats / LED / Health │ │ │
│ │ └────────────────────────┘ │ │
│ └──────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
Core 0 is dedicated entirely to packet transmission — a tight busy-loop with the Task Watchdog Timer disabled for maximum duty cycle. Core 1 handles all logic: frequency hopping, ESP-NOW fleet coordination, LED status, and serial diagnostics.
| Type | Size | 802.11 Frame | Mechanism |
|---|---|---|---|
| Deauthentication | 26 bytes | Management (Type 0, Subtype 12) | Spoofed broadcast frame forces all STAs to disconnect from the fabricated BSSID. Reason Code 7 triggers aggressive disconnect in most drivers. |
| CTS-to-Self | 10 bytes | Control (Type 1, Subtype 12) | Sets the Network Allocation Vector (NAV) to 32ms on all receiving devices, creating a virtual "channel busy" that silences all transmissions. |
| Random Noise | 512 bytes | Invalid (random data) | Raises the noise floor, causes CRC failures at receivers, and triggers CSMA/CA exponential backoff. At 1 Mbps DSSS, each frame occupies ~4.1ms of airtime. |
FrequencyJammer/
├── README.md ← You are here
├── jammerv1/
│ └── jammer.ino ← v1: Basic single-channel jammer (legacy)
├── jammerv2/
│ └── jammerv2.ino ← v2: Optimized single-channel jammer (legacy)
└── jammerv3/
└── jammerv3.ino ← v3: Multi-channel frequency hopping system ★
Use
jammerv3/jammerv3.ino— the v1 and v2 sketches are retained for reference only.
| Qty | Item | Specification | Purpose | Est. Cost |
|---|---|---|---|---|
| 3-4× | ESP32-S3-WROOM-1U | Dual-core LX7, 240MHz, U.FL/IPEX connector | Core microcontroller — requires external antenna | ~$6 each |
| 3-4× | ESP32-S3 Dev Board | Breakout board with USB-C, voltage regulator | Facilitates power, programming, and wiring | ~$8 each |
| Qty | Item | Specification | Purpose | Est. Cost |
|---|---|---|---|---|
| 1× | 2.4GHz Yagi Antenna | 15–18 dBi, directional, SMA connector | "Sniper" — long-range focused beam interference | ~$15 |
| 2× | 2.4GHz Omni Antenna | 9 dBi, high-gain "rubber ducky", SMA | "Bubble" — localized area saturation | ~$8 each |
| 3-4× | U.FL to SMA Pigtail | 10-15cm coaxial adapter cable | Connects ESP32-S3 U.FL socket to SMA antennas | ~$3 each |
| 1× | 2.4GHz LNA/PA Module | 2W–4W signal booster (Optional) | Amplifies TX power beyond ESP32's native 20.5 dBm | ~$25 |
| Qty | Item | Specification | Purpose | Est. Cost |
|---|---|---|---|---|
| 1-2× | 18650 Battery Pack | 2S (7.4V), 3000mAh+, w/ step-down to 5V | High-current portable power for field deployment | ~$12 each |
| 1× | Buck Converter | 7.4V → 5V/3A, USB-C output preferred | Stable regulated power for ESP32 modules | ~$5 |
| 1-2× | Aluminum Project Box | 100×60×25mm minimum, RF shielding | Prevents self-interference with controller logic | ~$8 each |
| — | Misc. Wiring | Dupont jumpers, JST connectors, solder | Assembly and interconnection | ~$5 |
| Configuration | Nodes | Approximate Cost |
|---|---|---|
| Minimum (1× Bubble) | 1 ESP32-S3 + Omni | ~$30 |
| Standard (Sniper + 2× Bubble) | 3 ESP32-S3 | ~$100 |
| Full Array (Orchestrator + Sniper + 2× Bubble) | 4 ESP32-S3 | ~$140 |
| Tool | Version | Notes |
|---|---|---|
| Arduino IDE | 2.x+ | Or Arduino CLI |
| Arduino-ESP32 Core | 2.x or 3.x | Both ESP-IDF 4.x and 5.x are supported |
| USB Driver | — | CP2102 or CH340 depending on dev board |
- Open Arduino IDE → File → Preferences
- Add to Additional Board Manager URLs:
https://espressif.github.io/arduino-esp32/package_esp32_index.json - Open Tools → Board → Boards Manager
- Search for "esp32" and install "esp32 by Espressif Systems"
git clone https://github.com/Lawlez/FrequencyJammer.git
cd FrequencyJammerOpen jammerv3/jammerv3.ino in Arduino IDE.
| Setting | Value |
|---|---|
| Board | ESP32S3 Dev Module |
| USB CDC On Boot | Enabled |
| CPU Frequency | 240MHz (WiFi) |
| Flash Mode | QIO 80MHz |
| Flash Size | 4MB (32Mb) |
| Partition Scheme | Default 4MB with spiffs |
| PSRAM | Disabled |
| Upload Speed | 921600 |
Edit the #define configuration block at the top of jammerv3.ino (lines 58–161). See Configuration below.
Click Upload (or Ctrl+U / Cmd+U). The sketch compiles with zero external library dependencies.
All parameters are compile-time #define constants. No runtime configuration or dynamic memory allocation.
#define DEVICE_ROLE ROLE_BUBBLE // Choose one:
// ROLE_ORCHESTRATOR — Master sync beacon broadcaster
// ROLE_SNIPER — Focused single-target interference
// ROLE_BUBBLE — Area saturation, follows hop pattern#define HOP_DWELL_US 10000 // Microseconds per channel (10ms = 100 hops/sec)
#define HOP_CHANNELS 13 // 802.11 channels 1-13
#define LFSR_SEED 0xACE1 // Hop sequence seed (non-zero, 16-bit)#define ENABLE_DEAUTH true // Broadcast deauthentication frames
#define ENABLE_CTS true // CTS-to-Self NAV reservation frames
#define ENABLE_NOISE true // Random noise payload frames#define TX_POWER_DBM 20 // Max: 20 dBm (~100mW) on ESP32-S3#define ESPNOW_ENABLED true // Enable/disable fleet coordination
#define ESPNOW_RENDEZVOUS_CH 1 // Fixed channel for sync beacons
#define ESPNOW_SYNC_INTERVAL_MS 50 // Beacon broadcast interval
#define ESPNOW_MISS_THRESHOLD 10 // Missed beacons before autonomous fallback#define LED_PIN 2 // GPIO for status LED (0 = disabled)
#define SERIAL_BAUD 115200 // Serial monitor baud rate (0 = disabled)
#define STATS_INTERVAL_MS 2000 // Stats print interval in milliseconds#define TX_BATCH_SIZE 8 // Frames per batch before yielding to hop task
#define NOISE_BUF_SIZE 512 // Random noise frame size in bytes
#define MAC_ROTATE_HOPS 5 // Rotate spoofed MAC every N hops- Connect ESP32-S3 via USB-C
- Select the correct Port under Tools → Port
- Hold the BOOT button on the dev board (if required by your board)
- Click Upload
- Release BOOT after "Connecting..." appears
- Wait for "Hard resetting via RTS pin..." — done
# Compile
arduino-cli compile --fqbn esp32:esp32:esp32s3 jammerv3/jammerv3.ino
# Upload (replace /dev/cu.usbmodem* with your port)
arduino-cli upload --fqbn esp32:esp32:esp32s3 -p /dev/cu.usbmodem14101 jammerv3/jammerv3.ino
# Monitor serial output
arduino-cli monitor -p /dev/cu.usbmodem14101 -c baudrate=115200- Flash
jammerv3.inowithDEVICE_ROLEset toROLE_BUBBLE - Power on the ESP32-S3
- The LED blinks once (Bubble role indicator), then goes solid
- The device immediately begins:
- Hopping across all 13 channels at the configured dwell rate
- Transmitting deauth + CTS + noise frames on each channel
- Rotating spoofed MAC addresses every 5 hops
- Open Serial Monitor at 115200 baud to see live stats
- Flash the Orchestrator — set
DEVICE_ROLEtoROLE_ORCHESTRATORon one node - Flash the Bubble nodes — set
DEVICE_ROLEtoROLE_BUBBLEon 1-2 nodes - Flash the Sniper — set
DEVICE_ROLEtoROLE_SNIPERon one node (optional) - Power on all nodes — they auto-discover via ESP-NOW broadcast on the rendezvous channel
- The Orchestrator broadcasts sync beacons every 50ms containing:
- Current epoch timestamp
- LFSR hop seed
- Active channel bitmask
- Total node count
- Each node applies a MAC-derived offset to the hop sequence, ensuring different channel assignments across the fleet
If a Bubble/Sniper node loses sync (misses 10 consecutive beacons), it automatically falls back to autonomous hopping and will re-sync when beacons are heard again.
| Pattern | Meaning |
|---|---|
| 1 blink at boot | Bubble role |
| 2 blinks at boot | Sniper role |
| 3 blinks at boot | Orchestrator role |
| Solid ON | System armed and transmitting |
| Brief OFF pulse every 1s | Heartbeat — operating normally |
1× ESP32-S3 + Yagi Antenna (15-18 dBi directional)
├── DEVICE_ROLE = ROLE_SNIPER
├── Point antenna at target for focused interference
└── Maximum range, minimum beam width
2× ESP32-S3 + Omni Antennas (9 dBi rubber ducky)
├── DEVICE_ROLE = ROLE_BUBBLE
├── Close-proximity area saturation
└── 360° coverage, follows orchestrated sweep pattern
1× ESP32-S3 (antenna optional — primarily coordinates)
├── DEVICE_ROLE = ROLE_ORCHESTRATOR
├── Broadcasts hop schedule to all fleet nodes via ESP-NOW
└── Also performs TX (can be disabled by setting all ENABLE_* to false)
┌──────────────────────┐
│ ORCHESTRATOR │
│ (ESP-NOW Sync) │
└──────────┬───────────┘
│ ESP-NOW Beacons
┌────────────────┼────────────────┐
▼ ▼ ▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ SNIPER │ │ BUBBLE │ │ BUBBLE │
│ (Yagi) │ │ (Omni) │ │ (Omni) │
│ ═══► │ │ ◉ │ │ ◉ │
│ Focused │ │ 360° Area │ │ 360° Area │
└─────────────┘ └─────────────┘ └─────────────┘
DJI OcuSync uses FHSS (Frequency Hopping Spread Spectrum) for control/telemetry links. To calibrate the system against specific OcuSync versions:
| Target | HOP_DWELL_US |
Hop Rate |
|---|---|---|
| OcuSync 2.0 (Mini 2, Air 2S, Mavic Air 2) | 10000 |
~100 hops/sec |
| OcuSync 3.0 / O3 (Mini 3 Pro, Mavic 3, Air 3) | 5000 |
~200 hops/sec |
| Aggressive (overwhelm adaptive selection) | 3000 |
~333 hops/sec |
- Capture — Use an SDR (HackRF, RTL-SDR v4, etc.) with GNU Radio, SigDigger, or Inspectrum to record the target's 2.4GHz spectrogram
- Measure — Count the frequency transitions per second visible in the waterfall display
- Calculate —
HOP_DWELL_US = 1,000,000 / observed_hop_rate - Lead-hop — Subtract 500–1000µs from the calculated dwell to arrive on each channel before the target
- Cover all channels — OcuSync uses dynamic channel selection. Setting
HOP_CHANNELS = 13covers all possible escape channels
Note: OcuSync's hopping algorithm uses AES-256 encrypted seeds — the exact sequence is proprietary. These calibration values are empirical approximations from published SDR research.
Connect at 115200 baud. Example output:
╔══════════════════════════════════════════════════╗
║ JAMMERV3 — Frequency Hopping Interference Sys ║
║ Target: ESP32-S3 | Channels: 1-13 ║
║ Role: BUBBLE | Dwell: 10000µs ║
╚══════════════════════════════════════════════════╝
[INIT] WiFi initialized. MAC: A0:B7:65:4C:D2:1F
[INIT] Node ID: 31 (0x1F)
[INIT] Packets built. Deauth: ON, CTS: ON, Noise: ON
[INIT] Spoofed MAC: 8A:3E:F1:6B:22:D0
[ESPNOW] Initialized. Role: BUBBLE
[INIT] ══════════════════════════════════════
[INIT] System ARMED. TX on Core 0, HOP on Core 1.
[INIT] Hop dwell: 10000 µs (100 hops/sec)
[INIT] ══════════════════════════════════════
[STAT] Ch: 7 | Hops: 200 | Frames: 4800 | 2400 fps | Sync:NO
[STAT] Ch:11 | Hops: 400 | Frames: 9600 | 2400 fps | Sync:YES
[STAT] Ch: 3 | Hops: 600 | Frames: 14400 | 2400 fps | Sync:YES
| Field | Description |
|---|---|
Ch |
Current channel at time of print |
Hops |
Total channel hops since boot |
Frames |
Total frames transmitted since boot |
fps |
Frames per second (current throughput) |
Sync |
ESP-NOW fleet synchronization status |
| Symptom | Cause | Fix |
|---|---|---|
| Board reboots repeatedly | WDT firing on Core 1 | Ensure vTaskDelay() exists in loop(). Don't add blocking code to hop task. |
| "Upload failed" | Board not in download mode | Hold BOOT button while clicking Upload |
| No serial output | Wrong baud rate or SERIAL_BAUD = 0 |
Set SERIAL_BAUD to 115200, monitor at same rate |
| 0 fps in stats | TX task not running | Check semaphore creation (look for [FATAL] in serial output) |
[ESPNOW] Lost sync |
Orchestrator out of range or powered off | Move nodes closer, or increase ESPNOW_MISS_THRESHOLD |
| Compilation errors on ESP-IDF 5.x | Callback signature mismatch | The code handles this automatically via ESP_IDF_VERSION guards |
| LED stays off | Wrong GPIO pin | Check LED_PIN matches your board's onboard LED (often GPIO 2 or 48) |
| Resource | Size | Allocation |
|---|---|---|
| Deauth frame buffer | 26 bytes | Static (compile-time) |
| CTS frame buffer | 10 bytes | Static (compile-time) |
| Noise buffer | 512 bytes | Static (compile-time) |
| Global state variables | ~50 bytes | Static (compile-time) |
| TX task stack | 2,048 bytes | Boot (FreeRTOS) |
| Hop task stack | 4,096 bytes | Boot (FreeRTOS) |
| Orchestrator task stack | 3,072 bytes | Boot (FreeRTOS, only if orchestrator) |
| Runtime heap allocation | 0 bytes | Never |
- MAC rotation: Spoofed source MACs change every 5 hops using hardware RNG
- Locally-administered bit: Always set in spoofed MACs to avoid OUI collisions
- No
Stringclass: All output usesprintf()to prevent heap fragmentation - Bounds-checked channels: Index always computed as
(value % 13) + 1 - Version guards: ESP-IDF 4.x/5.x callback signatures handled automatically
- ESP32-S3 radio is half-duplex — cannot TX and RX simultaneously
esp_wifi_80211_tx()transmits at ~1 Mbps DSSS for raw frames- Maximum native TX power is 20.5 dBm (~100mW) without external PA
- CTS-to-Self is less effective against Wi-Fi 6 (802.11ax) devices with BSS Color
- Deauth frames are rejected by WPA3/PMF (802.11w) enabled networks
- OcuSync timing values are empirical estimates from SDR research, not official specs
| Version | File | Description |
|---|---|---|
| v1 | jammerv1/jammer.ino |
Basic jammer — single channel, random data TX, blocking loop |
| v2 | jammerv2/jammerv2.ino |
Optimized — pre-filled buffer, channel 6 fixed, removed delay |
| v3 | jammerv3/jammerv3.ino |
Full rewrite — dual-core FHSS, 3 packet types, LFSR hopping, ESP-NOW fleet coordination, OcuSync calibration |
RF Research Project — Indoor Laboratory Use Only