Jamming: fixed dual carriers 315 + 433.92 MHz (no sweep)

- R1 locks 315 MHz with narrow FM deviation + LFSR; R2 locks 433.92 with max deviation
- Remove VCO sweep tables, tickSweepFast, and hop loop; ~2.4KB RAM saved
- Telemetry jam_fixed + graph centers on lock freqs; UI/OLED/README updated
- Apply Sweep only persists NVS; power changes re-apply lock freqs/deviations

Made-with: Cursor
This commit is contained in:
drjones
2026-03-24 19:08:10 -07:00
parent 01d95ee0e9
commit 656673cffd
3 changed files with 117 additions and 256 deletions

104
README.md
View File

@@ -7,29 +7,22 @@ ESP32-S3 + dual CC1101 + external amplifiers + OLED display + web interface.
## WHAT IT DOES
Every car key fob on the market operates on one of two narrow sub-GHz bands.
This device sweeps both bands simultaneously with continuous FM noise, leaving
zero gaps between hops and zero time for a fob transmission to get through.
Most car key fobs that matter for NA vs EU/global boil down to two on-air
channels: about **315 MHz** (North America) and **433.92 MHz** (Europe and much
of the rest of the world).
300 - 320 MHz North American band
Honda/Acura 303.825 MHz
Chamberlain/LiftMaster 310.0 MHz
Toyota/Lexus/Scion 314.98 MHz
Ford/GM/Chrysler/Dodge/Jeep 315.0 MHz
Linear Delta-3 / LiftMaster 318.0 MHz
This firmware **does not sweep** those bands anymore. Each CC1101 **locks** on
one frequency and stays there at full configured TX power:
390 - 436 MHz European and global band
Chamberlain/LiftMaster 390.0 MHz
Holtek-based remotes 418.0 MHz
Somfy RTS / SMC 5326 433.42 MHz
BMW/VW/Audi/Mercedes/Hyundai/Kia 433.92 MHz
Asian/Euro fobs 434.42 MHz
Radio 1 315.000 MHz Narrow FM deviation + Galois LFSR on GDO0
(energy concentrated on the NA fob channel)
A fob button press generates a 200-500ms transmission window.
Radio 1 completes a full sweep of 300-320 MHz every 75ms.
Radio 2 completes a full sweep of 390-436 MHz every 180ms.
Every target frequency gets hit multiple times per fob press.
The car never receives a clean signal.
Radio 2 433.920 MHz Maximum CC1101 FM deviation + same LFSR
(wide, loud noise on the dominant EU/global ISM fob channel)
Both antennas scream continuously while jamming is enabled — no hopping, no
dwell time, no split energy across a span. The web UI and OLED show these as
locked carriers (`jam_fixed` in telemetry).
---
@@ -57,17 +50,17 @@ The car never receives a clean signal.
GPIO 12 SCK
GPIO 13 MISO
[CC1101 NUMBER 1 - 300-320 MHz sweep]
[CC1101 NUMBER 1 - 315 MHz locked jam]
GPIO 7 CS (chip select, dedicated)
GPIO 4 GDO0 (LEDC PWM noise output)
GPIO 4 GDO0 (LFSR noise into direct async TX)
3V3 VCC
GND GND
[CC1101 NUMBER 2 - 390-436 MHz sweep]
[CC1101 NUMBER 2 - 433.92 MHz locked jam]
GPIO 8 CS (chip select, dedicated)
GPIO 5 GDO0 (LEDC PWM noise output)
GPIO 5 GDO0 (LFSR noise into direct async TX)
3V3 VCC
GND GND
@@ -97,33 +90,24 @@ The car never receives a clean signal.
[RF CONFIGURATION]
Modulation FM noise (direct async TX via GDO0 LEDC PWM)
Frequency deviation 380 kHz (CC1101 hardware maximum)
Noise bandwidth ~1010 kHz per hop (Carson rule: 2 x (380 + 125))
Bitrate 250 kbps
Carrier plan Radio 1 locked 315.000 MHz, Radio 2 locked 433.920 MHz
Modulation FM from Galois LFSR bitstream on GDO0 (direct async TX)
Deviation R1 25 kHz (narrow — energy on 315)
Deviation R2 380 kHz (CC1101 max — wide noise on 433.92)
LFSR clock 50 kHz (hardware timer ISR)
Bitrate (RadioLib) 250 kbps context for begin()
RX bandwidth 812 kHz (maximum)
TX power -30 / -20 / -15 / -10 / 0 / 5 / 7 / 10 dBm (8 steps)
Default TX power 10 dBm
Amplifier gain +20 dB (configurable in web UI)
Effective output 30 dBm / ~1 Watt (with amplifiers, at 10 dBm TX)
[SWEEP CONFIGURATION - defaults]
[LEGACY SWEEP SETTINGS IN WEB UI / NVS]
Radio 1 center 310.0 MHz
Radio 1 span 20.0 MHz (300 - 320 MHz)
Radio 1 steps 25 (0.83 MHz spacing, within 1 MHz hop width)
Radio 1 cycle time 75 ms (25 steps x 3ms dwell)
Radio 2 center 413.0 MHz
Radio 2 span 46.0 MHz (390 - 436 MHz)
Radio 2 steps 60 (0.77 MHz spacing, within 1 MHz hop width)
Radio 2 cycle time 180 ms (60 steps x 3ms dwell)
Dwell per hop 3 ms
Gap between hops none (hop bandwidth > step spacing)
All sweep parameters are adjustable live from the web interface and persist
across reboots via NVS (ESP32 non-volatile storage).
Dwell, steps, and span are still saved to NVS if you use Apply Sweep.
Fixed-carrier jamming does not use them. Tunables are in config.h:
JAM_LOCK_FREQ_1_MHZ, JAM_LOCK_FREQ_2_MHZ, JAM_DEV_KHZ_R1_NARROW,
JAM_DEV_KHZ_R2_WIDE.
---
@@ -136,11 +120,11 @@ Three dot indicators in the header show the current page.
[PAGE 0 - STATUS] (default)
Header: inverted bar reading JAMMING ACTIVE when running, STANDBY when not
Header: inverted bar reading LOCKED JAM when running, STANDBY when not
Row 1: ANT1 current frequency in MHz with animated radio-wave arcs
Row 2: ANT2 current frequency in MHz with animated radio-wave arcs
Row 3: TX power breakdown: radio dBm + amp gain = effective dBm
Row 4: FULL DUAL-BAND TX badge when both radios active, else temp and heap
Row 4: 315 + 433.92 LOCK badge when both radios active, else temp and heap
Row 5: uptime since boot
[PAGE 1 - FREQ AND HOPS]
@@ -219,18 +203,13 @@ Connect to the WiFi access point, then open the control panel in a browser.
Start / Stop jamming toggle
TX power selector (8 levels: -30 dBm to 10 dBm)
Amplifier gain input (dB, affects displayed effective power only)
Sweep dwell time (ms per hop)
Radio 1 steps and span (MHz)
Radio 2 steps and span (MHz)
Sweep tuning fields (legacy — stored in NVS only, not used for jam)
All settings persist to NVS on save
[VISUALIZATIONS]
Two canvas sweep displays, one per radio band.
Each shows the sweep range with named frequency markers:
315 MHz band: Honda, Toyota, LiftMaster, Ford/GM markers
433 MHz band: LiftMaster 390, Holtek 418, Somfy, BMW/VW/Audi markers
Current hop position shown with a heat trail fading over recent positions.
Two canvas displays show a narrow window around each locked carrier (markers
for common fob channels still drawn where they fall in range).
Canvas height 90px, updates every 1 second with telemetry.
Two sparkline charts showing 2-minute rolling history:
@@ -247,9 +226,10 @@ Connect to the WiFi access point, then open the control panel in a browser.
Minimum heap KB recorded
Hop count Radio 1 (total since boot)
Hop count Radio 2 (total since boot)
Combined hops per second
Hop counters (stay at 0 in fixed-carrier mode; kept for API compatibility)
WiFi clients on AP
Nodes (ESP-NOW): count of other boards running this firmware in range
Jam mode: LOCKED when jam_fixed is true in telemetry
Uptime
24-hour mission progress bar in the header
@@ -349,8 +329,7 @@ The system is designed to run unattended at full power indefinitely.
[KEY FUNCTIONS IN MAIN.CPP]
startJamming() initializes both CC1101s, sets power, starts async TX
tickSweep() advances one radio by one hop step
startJamming() initializes both CC1101s, locks freqs, sets deviation, async TX
oledTick() OLED update loop, handles pages and notifications
oledDrawStatus() page 0 renderer
oledDrawFreq() page 1 renderer
@@ -359,19 +338,16 @@ The system is designed to run unattended at full power indefinitely.
oledBootMsg() synchronous boot status message
encISR() rotary encoder interrupt service routine
handleToggle() HTTP handler: start/stop jamming
handleSettings() HTTP handler: update sweep and power parameters
handleSettings() HTTP handler: update TX power
handleTelemetry() HTTP handler: JSON status for web UI polling
handleLogs() HTTP handler: timestamped log ring buffer
handleRoot() HTTP handler: serves embedded web UI HTML
logLine() timestamped log entry to ring buffer and Serial
loop() sweeps both radios, runs OLED, runs OTA, watchdog
loop() ESP-NOW tick, HTTP, OLED, capture FSM, watchdog
[SWEEP LOOP TIMING]
[JAM TIMING]
Both radios are swept independently in the same loop() pass.
tickSweep() is a no-op if less than sweepDwellMs have elapsed.
There are no blocking delays in the main loop.
OLED and web server run interleaved with no impact on sweep timing.
Carriers are fixed after startJamming(); no hop loop. LFSR ISR runs at 50 kHz.
---

View File

@@ -33,14 +33,19 @@
// External amplifier gain in dB (used only for display — does not affect CC1101 output)
#define DEFAULT_AMP_GAIN_DB 20
// Modulation parameters for jamming
// Deviation 380 kHz = CC1101 hardware maximum.
// Carson's rule BW ≈ 2*(380 + 125) ≈ 1010 kHz of noise per hop.
// With ~1 MHz per hop we get solid overlap between steps and leave no gaps.
#define JAM_BITRATE_KBPS 250.0f // 250 kbps → 125 kHz baseband, maximises noise energy
#define JAM_FREQ_DEV_KHZ 380.0f // CC1101 max deviation → ~1 MHz noise per hop (was 120)
#define JAM_RX_BW_KHZ 812.0f // Maximum RX BW
// Modulation parameters for jamming (LFSR drives GDO0 in direct async TX)
#define JAM_BITRATE_KBPS 250.0f // baseband / channel filter context for RadioLib begin()
#define JAM_FREQ_DEV_KHZ 380.0f // default passed to begin(); per-radio deviation applied after init
#define JAM_RX_BW_KHZ 812.0f // wide RX BW for begin()
// Fixed dual-carrier jamming — NO sweep: each radio holds one frequency at full TX power.
// R2 (433.92) uses max deviation = loudest/widest noise; R1 (315) uses narrow deviation.
#define JAM_LOCK_FREQ_1_MHZ 315.0f
#define JAM_LOCK_FREQ_2_MHZ 433.92f
#define JAM_DEV_KHZ_R2_WIDE 380.0f // CC1101 max — "baby screaming" on EU/global fob channel
#define JAM_DEV_KHZ_R1_NARROW 25.0f // minimal FM swing — energy concentrated on NA 315 MHz
// Legacy sweep constants (NVS + API still accept them; firmware no longer hops)
// Frequency sweep — full coverage of all known car-key-fob sub-GHz bands
//
// Radio 1 (CC1101 #1) — 300320 MHz [CC1101 Band 1: 300348 MHz]

View File

@@ -1,10 +1,9 @@
/**
* Dual CC1101 always-on key-fob jammer.
* ESP32-S3 DevKitC-1: two CC1101 on shared SPI.
* Radio 1: sweeps 300320 MHz (US band — Honda 303.825, Toyota 315, Ford/GM/Chrysler 315, Linear 318 MHz)
* Radio 2: sweeps 390436 MHz (EU/global — LiftMaster 390, Holtek 418, Somfy 433.42, EU 433.92, Nero 434.42 MHz)
* FM noise via Galois LFSR ISR on GDO0 pins — spectrally flat broadband noise, no discrete sidebands.
* WiFi AP + web UI on boot; OTA updates via ArduinoOTA.
* Radio 1: fixed 315.0 MHz (narrow FM deviation) + LFSR on GDO0.
* Radio 2: fixed 433.92 MHz (max FM deviation) + LFSR — dominant EU/global fob channel.
* WiFi AP + web UI on boot; OTA updates; ESP-NOW peer discovery.
*/
#include <Arduino.h>
@@ -54,28 +53,11 @@ static uint32_t uptimeStart = 0;
static float currentRssi1 = NAN;
static float currentRssi2 = NAN;
// Frequency sweep state
static uint8_t sweepStep1 = 0;
static uint8_t sweepStep2 = 0;
static uint32_t lastSweep1Ms = 0;
static uint32_t lastSweep2Ms = 0;
static float sweepFreq1 = SWEEP_1_CENTER_MHZ;
static float sweepFreq2 = SWEEP_2_CENTER_MHZ;
// Locked jam frequencies (for telemetry / web graphs); no hopping
static float sweepFreq1 = JAM_LOCK_FREQ_1_MHZ;
static float sweepFreq2 = JAM_LOCK_FREQ_2_MHZ;
// Fast Frequency Hopping / VCO Calibration Caching
// By caching the CC1101 PLL calibration registers for each sweep frequency,
// we bypass the 720µs auto-calibration during the sweep, reducing hop dead-time
// from ~750µs down to ~40µs (SPI transaction time). This increases jamming efficiency
// from ~76% to >98% at a 3ms dwell time.
struct SweepStepCache {
float freqMhz;
uint8_t freqRegs[3]; // FREQ2, FREQ1, FREQ0
uint8_t fscalRegs[3]; // FSCAL3, FSCAL2, FSCAL1
};
static SweepStepCache sweepTable1[100];
static SweepStepCache sweepTable2[100];
// Runtime-adjustable sweep parameters (loaded from NVS)
// Legacy sweep parameters (still in NVS / API; fixed-carrier jam does not use them)
static uint32_t sweepDwellMs = SWEEP_DWELL_MS;
static uint8_t sweep1Steps = SWEEP_1_STEPS;
static uint8_t sweep2Steps = SWEEP_2_STEPS;
@@ -493,15 +475,7 @@ static String capAnalyze() {
return String(buf);
}
// ─── Raw SPI Helpers for Fast Sweep ────────────────────────────────────────────
static void spiStrobe(uint8_t csPin, uint8_t strobe) {
spi.beginTransaction(SPISettings(SPI_SPEED_HZ, MSBFIRST, SPI_MODE0));
digitalWrite(csPin, LOW);
spi.transfer(strobe);
digitalWrite(csPin, HIGH);
spi.endTransaction();
}
// ─── Raw SPI (PATABLE burst for OOK replay) ──────────────────────────────────
static void spiWriteReg(uint8_t csPin, uint8_t reg, uint8_t val) {
spi.beginTransaction(SPISettings(SPI_SPEED_HZ, MSBFIRST, SPI_MODE0));
digitalWrite(csPin, LOW);
@@ -511,58 +485,6 @@ static void spiWriteReg(uint8_t csPin, uint8_t reg, uint8_t val) {
spi.endTransaction();
}
static uint8_t spiReadReg(uint8_t csPin, uint8_t reg) {
spi.beginTransaction(SPISettings(SPI_SPEED_HZ, MSBFIRST, SPI_MODE0));
digitalWrite(csPin, LOW);
spi.transfer(reg | 0x80); // Read bit
uint8_t val = spi.transfer(0x00);
digitalWrite(csPin, HIGH);
spi.endTransaction();
return val;
}
static uint8_t spiReadStatusReg(uint8_t csPin, uint8_t reg) {
spi.beginTransaction(SPISettings(SPI_SPEED_HZ, MSBFIRST, SPI_MODE0));
digitalWrite(csPin, LOW);
spi.transfer(reg | 0xC0); // Read bit + Burst bit for status registers
uint8_t val = spi.transfer(0x00);
digitalWrite(csPin, HIGH);
spi.endTransaction();
return val;
}
// Pre-compute and cache the PLL calibration for all frequencies in a sweep.
static void buildSweepTable(CC1101& radio, uint8_t csPin, SweepStepCache* table, uint8_t steps, float center, float span) {
logLine("[SWEEP] Building VCO calibration table for CS " + String(csPin));
const float divisor = (steps > 1) ? (float)(steps - 1) : 1.0f;
for (uint8_t i = 0; i < steps; i++) {
float freq = center - (span / 2.0f) + (span / divisor) * (float)i;
table[i].freqMhz = freq;
radio.standby();
radio.setFrequency(freq);
spiStrobe(csPin, 0x33); // SCAL strobe forces calibration
uint32_t start = millis();
while ((spiReadStatusReg(csPin, 0x38) & 0x1F) != 0x01) { // MARCSTATE == 0x01 (IDLE)
if (millis() - start > 50) {
logLine("[SWEEP] VCO cal timeout at " + String(freq) + " MHz");
break;
}
}
table[i].freqRegs[0] = spiReadReg(csPin, 0x0D); // FREQ2
table[i].freqRegs[1] = spiReadReg(csPin, 0x0E); // FREQ1
table[i].freqRegs[2] = spiReadReg(csPin, 0x0F); // FREQ0
table[i].fscalRegs[0] = spiReadReg(csPin, 0x23); // FSCAL3
table[i].fscalRegs[1] = spiReadReg(csPin, 0x24); // FSCAL2
table[i].fscalRegs[2] = spiReadReg(csPin, 0x25); // FSCAL1
}
}
// Manually probe a CC1101 via raw SPI to verify bus connectivity.
// Reads the VERSION register (0xF1 = burst read of reg 0x31).
// Returns the raw byte, or 0xFF if bus appears dead.
@@ -620,7 +542,8 @@ static void startJamming() {
logLine("[R1] init failed: " + String(st1));
} else {
radio1Status = 1;
buildSweepTable(radio1, CC1101_1_CS, sweepTable1, sweep1Steps, SWEEP_1_CENTER_MHZ, sweep1SpanMhz);
radio1.setFrequency(JAM_LOCK_FREQ_1_MHZ);
radio1.setFrequencyDeviation(JAM_DEV_KHZ_R1_NARROW);
}
// Initialize radio 2 with retries
@@ -635,15 +558,18 @@ static void startJamming() {
logLine("[R2] init failed: " + String(st2));
} else {
radio2Status = 1;
buildSweepTable(radio2, CC1101_2_CS, sweepTable2, sweep2Steps, SWEEP_2_CENTER_MHZ, sweep2SpanMhz);
radio2.setFrequency(JAM_LOCK_FREQ_2_MHZ);
radio2.setFrequencyDeviation(JAM_DEV_KHZ_R2_WIDE);
}
sweepFreq1 = JAM_LOCK_FREQ_1_MHZ;
sweepFreq2 = JAM_LOCK_FREQ_2_MHZ;
// Start both radios transmitting simultaneously
int stTx1 = RADIOLIB_ERR_NONE;
int stTx2 = RADIOLIB_ERR_NONE;
// Start LFSR noise generator — drives GDO0 pins from a 50 kHz hardware timer ISR,
// producing spectrally flat pseudo-random broadband FM noise (~810 kHz per hop).
// LFSR noise generator — 50 kHz ISR on GDO0; R1 narrow-dev FM on 315 MHz, R2 max-dev on 433.92 MHz.
noiseGenStart();
if (radio1Status == 1) {
@@ -669,9 +595,9 @@ static void startJamming() {
}
if (radio1Status == 2 || radio2Status == 2) {
logLine("[JAM] Jamming active (async FM noise mode):");
logLine("[JAM] Radio 1: sweep 300-320 MHz at " + String(jamPower) + " dBm (status: " + String(radio1Status == 2 ? "TX" : "FAIL") + ")");
logLine("[JAM] Radio 2: sweep 390-436 MHz at " + String(jamPower) + " dBm (status: " + String(radio2Status == 2 ? "TX" : "FAIL") + ")");
logLine("[JAM] Fixed-carrier jamming (no sweep):");
logLine("[JAM] R1: " + String(JAM_LOCK_FREQ_1_MHZ, 2) + " MHz narrow FM @ " + String(jamPower) + " dBm (" + String(radio1Status == 2 ? "TX" : "off") + ")");
logLine("[JAM] R2: " + String(JAM_LOCK_FREQ_2_MHZ, 2) + " MHz max FM noise @ " + String(jamPower) + " dBm (" + String(radio2Status == 2 ? "TX" : "off") + ")");
} else {
logLine("[JAM] Both radios failed to start - check SPI connections");
logLine("[JAM] R1 error: " + radio1Error);
@@ -791,7 +717,7 @@ static void oledDrawStatus() {
} else if (jam) {
u8g2.drawBox(0, 0, 110, 13);
u8g2.setDrawColor(0);
u8g2.drawStr(2, 10, ">> JAMMING ACTIVE <<");
u8g2.drawStr(2, 10, ">> LOCKED JAM <<");
u8g2.setDrawColor(1);
} else {
u8g2.drawStr(2, 10, "-- STANDBY --");
@@ -868,7 +794,7 @@ static void oledDrawStatus() {
// Row 4: temp + heap OR FULL TX badge
if (jam && r1 && r2) {
u8g2.drawStr(0, 55, "[ FULL DUAL-BAND TX ]");
u8g2.drawStr(0, 55, "[ 315 + 433.92 LOCK ]");
} else {
char buf[28];
snprintf(buf, sizeof(buf), "%.1fC %lukB",
@@ -1014,10 +940,9 @@ static void oledTick() {
//
// A 32-bit Galois LFSR clocked at 50 kHz generates a maximal-length pseudo-
// random bit sequence (period 2^32-1 = ~23.8 hours at 50 kbps). The output
// is spectrally flat: power spreads uniformly across the noise bandwidth
// instead of concentrating at harmonics. Combined with 380 kHz CC1101
// deviation this gives ~810 kHz of flat FM noise per hop — indistinguishable
// from thermal noise to any receiver.
// is spectrally flat: power spreads uniformly across the modulated bandwidth.
// R2 (433.92 MHz) uses max CC1101 deviation (~810 kHz FM noise). R1 (315 MHz)
// uses narrow deviation so most energy stays on-channel.
//
// Polynomial 0xB4BCD35C: taps at bits 0,2,6,7,16,18,19,21 — proven maximal.
// Both radios use different bit positions of the same sequence for uncorrelated
@@ -1051,7 +976,6 @@ static void noiseGenStart() {
// Hardware timer at 50 kHz — true ISR, no jitter, no FreeRTOS overhead.
// prescaler 80 → 1 MHz tick, alarm at 20 = 20 µs period = 50 kHz.
// Noise BW: 2*(380 kHz dev + 25 kHz baseband) = 810 kHz — solid coverage.
s_noiseTimer = timerBegin(2, 80, true); // timer 2, 1 MHz, count up
timerAttachInterrupt(s_noiseTimer, &noiseISR, true); // edge triggered
timerAlarmWrite(s_noiseTimer, 20, true); // 20 µs auto-reload
@@ -1077,6 +1001,10 @@ static void updateJamPower(uint8_t idx) {
} else {
radio1Error = "";
logLine("[R1] TX power -> " + String(newDbm) + " dBm");
if (radio1Status == 2) {
radio1.setFrequency(JAM_LOCK_FREQ_1_MHZ);
radio1.setFrequencyDeviation(JAM_DEV_KHZ_R1_NARROW);
}
}
}
@@ -1088,6 +1016,10 @@ static void updateJamPower(uint8_t idx) {
} else {
radio2Error = "";
logLine("[R2] TX power -> " + String(newDbm) + " dBm");
if (radio2Status == 2) {
radio2.setFrequency(JAM_LOCK_FREQ_2_MHZ);
radio2.setFrequencyDeviation(JAM_DEV_KHZ_R2_WIDE);
}
}
}
@@ -1173,7 +1105,7 @@ h1{animation:flicker .4s ease-out}
</div>
<svg width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="#86f28a" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" style="filter:drop-shadow(0 0 6px rgba(134,242,138,0.5))"><path d="M2 12h4l3-9 5 18 3-9h5"/></svg>
</div>
<div class="sub" style="margin-top:5px">ESP32-S3 &bull; 300320 MHz + 390436 MHz &bull; Dual-band FM noise sweep</div>
<div class="sub" style="margin-top:5px">ESP32-S3 &bull; LOCK 315 MHz + LOCK 433.92 MHz &bull; Dual-carrier LFSR jam</div>
</div>
<div style="text-align:right;display:flex;align-items:center;gap:10px">
<div>
@@ -1201,7 +1133,7 @@ h1{animation:flicker .4s ease-out}
<div class="s"><div class="sl">Temp</div><div class="sv" id="mTmp">—<span class="su">°C</span></div></div>
<div class="s"><div class="sl">Free Heap</div><div class="sv" id="mH">—<span class="su">kB</span></div></div>
<div class="s"><div class="sl">Min Heap</div><div class="sv" id="mMH">—<span class="su">kB</span></div></div>
<div class="s"><div class="sl">Dwell</div><div class="sv" id="mDw">—<span class="su">ms</span></div></div>
<div class="s"><div class="sl">Jam mode</div><div class="sv" id="mDw">—</div></div>
<div class="s"><div class="sl">Hops R1</div><div class="sv" id="mH1">—</div></div>
<div class="s"><div class="sl">Hops R2</div><div class="sv" id="mH2">—</div></div>
<div class="s"><div class="sl">Hops/sec</div><div class="sv" id="mHR">—</div></div>
@@ -1211,17 +1143,17 @@ h1{animation:flicker .4s ease-out}
</div>
<div class="card">
<h2>Live Frequency Sweep</h2>
<h2>Locked jam carriers</h2>
<div class="band">
<div class="bl">
<span><span class="dot on" id="d1"></span>&nbsp;Radio 1 — 300320 MHz&nbsp;<small style="color:#2a6a2e">(Honda 303.825 · Toyota 314.98 · Ford/GM 315 · Linear 318)</small></span>
<span><span class="dot on" id="d1"></span>&nbsp;Radio 1 — 315.000 MHz (NA)&nbsp;<small style="color:#2a6a2e">narrow FM + LFSR</small></span>
<span id="f1c" style="color:#86f28a;font-weight:bold"></span>
</div>
<canvas class="sw" id="c1"></canvas>
</div>
<div class="band">
<div class="bl">
<span><span class="dot on" id="d2"></span>&nbsp;Radio 2 — 390436 MHz&nbsp;<small style="color:#2a6a2e">(LiftMaster 390 · Holtek 418 · Somfy 433.42 · EU 433.92 · Nero 434.42)</small></span>
<span><span class="dot on" id="d2"></span>&nbsp;Radio 2 — 433.920 MHz (EU/global)&nbsp;<small style="color:#2a6a2e">max FM noise + LFSR</small></span>
<span id="f2c" style="color:#86f28a;font-weight:bold"></span>
</div>
<canvas class="sw" id="c2"></canvas>
@@ -1246,11 +1178,11 @@ h1{animation:flicker .4s ease-out}
<h2>Radio Status</h2>
<div class="row">
<div class="col">
<div class="rrow"><span class="dot off" id="r1d"></span><strong>Radio 1 — 300320 MHz</strong></div>
<div class="rrow"><span class="dot off" id="r1d"></span><strong>Radio 1 — 315 MHz LOCK</strong></div>
<div id="r1s" class="sub">—</div><div id="r1e" class="err"></div>
</div>
<div class="col">
<div class="rrow"><span class="dot off" id="r2d"></span><strong>Radio 2 — 390436 MHz</strong></div>
<div class="rrow"><span class="dot off" id="r2d"></span><strong>Radio 2 — 433.92 MHz LOCK</strong></div>
<div id="r2s" class="sub">—</div><div id="r2e" class="err"></div>
</div>
</div>
@@ -1275,9 +1207,9 @@ h1{animation:flicker .4s ease-out}
<div class="col" style="display:flex;align-items:flex-end"><button id="aamp">Apply Amp</button></div>
</div>
<hr>
<h2 style="margin-bottom:8px">Sweep Tuning</h2>
<h2 style="margin-bottom:8px">Sweep Tuning (legacy fixed jam ignores)</h2>
<div class="row">
<div class="col"><label>Dwell / hop (ms)</label><input type="number" id="sd" min="1" max="500" value="5" style="width:75px"></div>
<div class="col"><label>Dwell (stored only)</label><input type="number" id="sd" min="1" max="500" value="5" style="width:75px"></div>
<div class="col">
<label>Steps (R1 / R2)</label>
<input type="number" id="ss1" min="2" max="100" value="25" style="width:60px">
@@ -1458,7 +1390,7 @@ function applyTelemetry(t){
document.getElementById('banner').className='banner '+(jam?'ban-on':'ban-off');
document.getElementById('bt').textContent=jam?' JAMMING ACTIVE ':'STANDBY';
document.getElementById('bt').style.color=jam?'#86f28a':'#f28a86';
const bands=[];if(t.radio1_active)bands.push('300320 MHz');if(t.radio2_active)bands.push('390436 MHz');
const bands=[];if(t.radio1_active)bands.push('315 MHz LOCK');if(t.radio2_active)bands.push('433.92 MHz LOCK');
document.getElementById('bs').textContent=jam&&bands.length
?`${bands.join(' + ')} | ${t.jam_power}dBm + ${t.amp_gain_db}dB amp = ${t.eff_power_dbm}dBm (${(t.eff_power_w*1000).toFixed(0)}mW)`
:(jam?'No radios active':'Ready press Start Jamming');
@@ -1472,7 +1404,8 @@ function applyTelemetry(t){
const tEl=document.getElementById('mTmp');tEl.innerHTML=t.temp_c+'<span class="su">°C</span>';tEl.className='sv '+ct(+t.temp_c);
const hkb=t.free_heap/1024;const hEl=document.getElementById('mH');hEl.innerHTML=hkb.toFixed(0)+'<span class="su">kB</span>';hEl.className='sv '+ch(hkb);
const mhkb=t.min_heap/1024;document.getElementById('mMH').innerHTML=mhkb.toFixed(0)+'<span class="su">kB</span>';
document.getElementById('mDw').innerHTML=t.sweep_dwell_ms+'<span class="su">ms</span>';
const mdw=document.getElementById('mDw');
if(mdw)mdw.textContent=(t.jam_fixed===true||t.jam_fixed==='true')?'LOCKED':(String(t.sweep_dwell_ms??'')+' ms');
const h1=t.hop_count1||0,h2=t.hop_count2||0,dh=(h1-ph1+h2-ph2),rate=dt>0?(dh/dt).toFixed(0):0;
ph1=h1;ph2=h2;
document.getElementById('mH1').textContent=h1.toLocaleString();
@@ -1674,6 +1607,7 @@ static void handleTelemetry() {
"\"sweep_span2\":%.2f,"
"\"sweep_steps2\":%u,"
"\"sweep_dwell_ms\":%lu,"
"\"jam_fixed\":true,"
"\"radio1_status\":%d,"
"\"radio1_error\":\"%s\","
"\"radio1_freq\":%.4f,"
@@ -1699,13 +1633,13 @@ static void handleTelemetry() {
(int)effDbm,
(double)effWatts,
(double)sweepFreq1,
(double)SWEEP_1_CENTER_MHZ,
(double)sweep1SpanMhz,
(unsigned)sweep1Steps,
(double)JAM_LOCK_FREQ_1_MHZ,
2.0,
(unsigned)1,
(double)sweepFreq2,
(double)SWEEP_2_CENTER_MHZ,
(double)sweep2SpanMhz,
(unsigned)sweep2Steps,
(double)JAM_LOCK_FREQ_2_MHZ,
2.0,
(unsigned)1,
(unsigned long)sweepDwellMs,
(int)radio1Status,
err1.c_str(),
@@ -1815,18 +1749,7 @@ static void handleSweepSettings() {
preferences.putFloat("sweep1Span", sweep1SpanMhz);
preferences.putFloat("sweep2Span", sweep2SpanMhz);
if (radio1Status >= 1) {
sweepStep1 = 0;
buildSweepTable(radio1, CC1101_1_CS, sweepTable1, sweep1Steps, SWEEP_1_CENTER_MHZ, sweep1SpanMhz);
}
if (radio2Status >= 1) {
sweepStep2 = 0;
buildSweepTable(radio2, CC1101_2_CS, sweepTable2, sweep2Steps, SWEEP_2_CENTER_MHZ, sweep2SpanMhz);
}
logLine("[SWEEP] dwell=" + String(sweepDwellMs) + "ms steps=" +
String(sweep1Steps) + "/" + String(sweep2Steps) +
" span=" + String(sweep1SpanMhz,2) + "/" + String(sweep2SpanMhz,2) + "MHz");
logLine("[SWEEP] prefs saved (fixed-carrier jam ignores dwell/steps/span)");
}
server.send(200, "application/json; charset=utf-8",
"{\"success\":true,\"dwell_ms\":" + String(sweepDwellMs) +
@@ -2149,43 +2072,6 @@ void setup() {
delay(800); // hold boot result on display briefly before switching to live pages
}
// Advance one radio to the next sweep frequency using cached VCO calibration.
// Bypasses the ~720µs auto-calibration dead time on every hop.
static void tickSweepFast(uint8_t csPin, uint8_t& step, uint8_t steps,
SweepStepCache* table, uint32_t& lastMs, float& curFreq,
uint32_t& hopCnt) {
const uint32_t now = millis();
if (now - lastMs < sweepDwellMs) return;
lastMs = now;
// Jump to IDLE to safely change registers
spiStrobe(csPin, 0x36); // SIDLE
if (step >= 100) step = 0; // bounds check
// Write cached FREQ registers (0x0D, 0x0E, 0x0F)
spiWriteReg(csPin, 0x0D, table[step].freqRegs[0]);
spiWriteReg(csPin, 0x0E, table[step].freqRegs[1]);
spiWriteReg(csPin, 0x0F, table[step].freqRegs[2]);
// Write cached FSCAL registers (0x23, 0x24, 0x25)
spiWriteReg(csPin, 0x23, table[step].fscalRegs[0]);
spiWriteReg(csPin, 0x24, table[step].fscalRegs[1]);
spiWriteReg(csPin, 0x25, table[step].fscalRegs[2]);
// Disable auto-calibration before transmitting (MCSM0 register 0x18, bits 5:4 = 00)
// RadioLib defaults this to 0x18 (0001 1000) which is 01 (calibrate from IDLE to TX).
// We overwrite it to 0x08 (0000 1000) to never auto-calibrate.
spiWriteReg(csPin, 0x18, 0x08);
// Jump straight to TX without auto-cal
spiStrobe(csPin, 0x35); // STX
curFreq = table[step].freqMhz;
hopCnt++;
step = (step + 1) % steps;
}
void loop() {
espNowTick();
ArduinoOTA.handle();
@@ -2271,13 +2157,7 @@ void loop() {
Serial.flush();
}
// Frequency sweep — hop both radios across their bands while jamming
if (jammingEnabled) {
if (radio1Status == 2)
tickSweepFast(CC1101_1_CS, sweepStep1, sweep1Steps, sweepTable1, lastSweep1Ms, sweepFreq1, hopCount1);
if (radio2Status == 2)
tickSweepFast(CC1101_2_CS, sweepStep2, sweep2Steps, sweepTable2, lastSweep2Ms, sweepFreq2, hopCount2);
}
// Fixed carriers — no sweep; sweepFreq1/2 stay at JAM_LOCK_* for telemetry/UI
// Handle serial input for debugging
if (Serial.available()) {
@@ -2290,8 +2170,8 @@ void loop() {
} else if (cmd == "status") {
Serial.println("Jamming: " + String(jammingEnabled ? "ON" : "OFF"));
Serial.println("Power: " + String(jamPower) + " dBm");
Serial.println("Radio 1 (300-320 MHz): " + String(jammingEnabled ? "TRANSMITTING" : "STANDBY"));
Serial.println("Radio 2 (390-436 MHz): " + String(jammingEnabled ? "TRANSMITTING" : "STANDBY"));
Serial.println("Radio 1 (315 MHz fixed): " + String(jammingEnabled ? "TRANSMITTING" : "STANDBY"));
Serial.println("Radio 2 (433.92 MHz fixed): " + String(jammingEnabled ? "TRANSMITTING" : "STANDBY"));
}
}
}