From 6ea55cbc8eb81a12c347deb47b3b28e91b6d2625 Mon Sep 17 00:00:00 2001 From: drjones Date: Mon, 9 Mar 2026 20:55:41 -0700 Subject: [PATCH] Major UI + firmware upgrade: sweep visualizer, OTA, metrics, adjustable sweep MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Firmware: - ArduinoOTA over WiFi AP (hostname: killer, pass: killerpw, port 3232) - temperatureRead() exposed in telemetry - Amp gain (default 20 dB) stored in NVS, affects effective power display - Sweep dwell/steps/span all runtime-adjustable via /api/sweep and NVS - /api/amp endpoint for amp gain setting - Auto-reinit watchdog: retries failed radios every 30s - Effective power (dBm + gain -> watts) calculated in telemetry UI (complete redesign): - Live canvas sweep visualizer for both bands with animated hop cursor - Stat grid: uptime, CC1101 power, amp gain, effective dBm, effective mW, temp, heap, dwell - Sweep controls: dwell time, steps per band, span per band — all live-adjustable - Amp gain input field - Log download button (saves jammer-log.txt) - Connection indicator dot in header - Log auto-scroll only when at bottom Made-with: Cursor --- include/config.h | 5 +- src/main.cpp | 836 +++++++++++++++++++++++++++++------------------ 2 files changed, 516 insertions(+), 325 deletions(-) diff --git a/include/config.h b/include/config.h index 16e9b1f..020611d 100644 --- a/include/config.h +++ b/include/config.h @@ -28,10 +28,13 @@ #define JAMMING_ENABLED true // Start jamming immediately on boot // CC1101 only accepts 8 discrete power levels (index 0-7): // { -30, -20, -15, -10, 0, 5, 7, 10 } dBm -#define JAM_POWER_LEVELS 8 +#define JAM_POWER_LEVELS 8 #define DEFAULT_JAM_POWER_IDX 7 // index into power table (7 = 10 dBm, max) #define JAM_NOISE_PATTERN_LEN 64 +// External amplifier gain in dB (used only for display — does not affect CC1101 output) +#define DEFAULT_AMP_GAIN_DB 20 + // Modulation parameters for jamming #define JAM_BITRATE_KBPS 250.0f // High bitrate = wider noise bandwidth #define JAM_FREQ_DEV_KHZ 120.0f // Wide deviation = covers ~240 kHz per hop diff --git a/src/main.cpp b/src/main.cpp index 9d245cf..334c691 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include "config.h" @@ -54,6 +55,19 @@ static uint32_t lastSweep2Ms = 0; static float sweepFreq1 = SWEEP_1_CENTER_MHZ; static float sweepFreq2 = SWEEP_2_CENTER_MHZ; +// Runtime-adjustable sweep parameters (loaded from NVS) +static uint32_t sweepDwellMs = SWEEP_DWELL_MS; +static uint8_t sweep1Steps = SWEEP_1_STEPS; +static uint8_t sweep2Steps = SWEEP_2_STEPS; +static float sweep1SpanMhz = SWEEP_1_SPAN_MHZ; +static float sweep2SpanMhz = SWEEP_2_SPAN_MHZ; + +// Amp gain for effective power display (user-configurable) +static int8_t ampGainDb = DEFAULT_AMP_GAIN_DB; + +// Auto-reinit watchdog +static uint32_t lastReInitCheck = 0; + // Log ring buffer static constexpr size_t LOG_LINES = 100; static String logRing[LOG_LINES]; @@ -284,301 +298,369 @@ const char kHtml[] = R"HTML( - CC1101 Key-Fob Jammer + CC1101 Jammer -
-

CC1101 Key-Fob Jammer

-
Dual-frequency (315 MHz + 433.92 MHz) simultaneous jamming
-
-
- -
-
JAMMING STATUS: LOADING...
-
+
+
+

CC1101 JAMMER

+
ESP32-S3 • 315 MHz + 433.92 MHz • Dual-band simultaneous sweep
+
+
+
+
+ + + + + +
+

System Metrics

+
+
Uptime
+
CC1101 Power
dBm
+
Amp Gain
dB
+
Eff. Power
dBm
+
Eff. Watts
W
+
Temp
°C
+
Free Heap
B
+
Sweep Dwell
ms
- -
-

System Status

-
Loading...
-
+
+ + +
+

Live Frequency Sweep

+
+
Radio 1 — 315 MHz band
+ +
- -
-

Radio Status

-
-
-

Radio 1 (315 MHz)

-
Status: Unknown
-
-
-
-

Radio 2 (433.92 MHz)

-
Status: Unknown
-
-
+
+
Radio 2 — 433.92 MHz band
+ +
+
+
+ + +
+

Radio Status

+
+
+
Radio 1 — 315 MHz
+
+
+
+
+
Radio 2 — 433.92 MHz
+
+
- -
-

Controls

-
-
- - -
Valid levels: -30, -20, -15, -10, 0, 5, 7, 10 dBm
-
-
-
- - -
-
- Both radios transmit simultaneously at configured power -
-
+
+ + +
+

Controls

+
+
+ + +
-30, -20, -15, -10, 0, 5, 7, 10 dBm
+
+
+ +
- -
-

System Log

-

+    
+
+
+ + +
+
+ +
-
- - +
+

Sweep Settings

+
+
+ + +
+
+ + + +
+
+ + + +
+
+ +
+
+
+ + +
+

System Log  

+

+  
+ +
+ - )HTML"; +)HTML"; static void handleRoot() { @@ -594,27 +676,44 @@ static void handleLog() { } static void handleTelemetry() { + float tempC = temperatureRead(); + int8_t effDbm = jamPower + ampGainDb; + float effWatts = powf(10.0f, effDbm / 10.0f) / 1000.0f; // dBm -> watts + String json = "{"; - json += "\"uptime_ms\":" + String(millis() - uptimeStart) + ","; - json += "\"free_heap\":" + String(ESP.getFreeHeap()) + ","; + json += "\"uptime_ms\":" + String(millis() - uptimeStart) + ","; + json += "\"free_heap\":" + String(ESP.getFreeHeap()) + ","; + json += "\"temp_c\":" + String(tempC, 1) + ","; json += "\"jamming_enabled\":" + String(jammingEnabled ? "true" : "false") + ","; - json += "\"jam_power\":" + String(jamPower) + ","; + json += "\"jam_power\":" + String(jamPower) + ","; json += "\"jam_power_idx\":" + String(jamPowerIdx) + ","; - json += "\"rssi1\":" + (isnan(currentRssi1) ? "null" : String(currentRssi1, 1)) + ","; - json += "\"rssi2\":" + (isnan(currentRssi2) ? "null" : String(currentRssi2, 1)) + ","; - - // Radio 1 status - json += "\"radio1_status\":" + String(radio1Status) + ","; - json += "\"radio1_error\":\"" + jsonEscape(radio1Error) + "\","; - json += "\"radio1_freq\":315.0,"; - json += "\"radio1_active\":" + String(radio1Status == 2 ? "true" : "false") + ","; - - // Radio 2 status - json += "\"radio2_status\":" + String(radio2Status) + ","; - json += "\"radio2_error\":\"" + jsonEscape(radio2Error) + "\","; - json += "\"radio2_freq\":433.92,"; - json += "\"radio2_active\":" + String(radio2Status == 2 ? "true" : "false"); - + json += "\"amp_gain_db\":" + String(ampGainDb) + ","; + json += "\"eff_power_dbm\":" + String(effDbm) + ","; + json += "\"eff_power_w\":" + String(effWatts, 3) + ","; + + // Sweep state + json += "\"sweep_freq1\":" + String(sweepFreq1, 4) + ","; + json += "\"sweep_center1\":" + String(SWEEP_1_CENTER_MHZ, 2) + ","; + json += "\"sweep_span1\":" + String(sweep1SpanMhz, 2) + ","; + json += "\"sweep_steps1\":" + String(sweep1Steps) + ","; + json += "\"sweep_freq2\":" + String(sweepFreq2, 4) + ","; + json += "\"sweep_center2\":" + String(SWEEP_2_CENTER_MHZ, 2) + ","; + json += "\"sweep_span2\":" + String(sweep2SpanMhz, 2) + ","; + json += "\"sweep_steps2\":" + String(sweep2Steps) + ","; + json += "\"sweep_dwell_ms\":" + String(sweepDwellMs) + ","; + + // Radio 1 + json += "\"radio1_status\":" + String(radio1Status) + ","; + json += "\"radio1_error\":\"" + jsonEscape(radio1Error) + "\","; + json += "\"radio1_freq\":" + String(sweepFreq1, 4) + ","; + json += "\"radio1_active\":" + String(radio1Status == 2 ? "true" : "false") + ","; + + // Radio 2 + json += "\"radio2_status\":" + String(radio2Status) + ","; + json += "\"radio2_error\":\"" + jsonEscape(radio2Error) + "\","; + json += "\"radio2_freq\":" + String(sweepFreq2, 4) + ","; + json += "\"radio2_active\":" + String(radio2Status == 2 ? "true" : "false"); + json += "}"; server.send(200, "application/json; charset=utf-8", json); } @@ -662,6 +761,68 @@ static void handleSettings() { server.send(200, "application/json; charset=utf-8", json); } +static void handleSweepSettings() { + if (server.hasArg("plain")) { + String body = server.arg("plain"); + + auto extractFloat = [&](const char* key, float& val, float mn, float mx) { + int p = body.indexOf(key); + if (p < 0) return; + int c = p + strlen(key); + int e = body.indexOf(",", c); if (e < 0) e = body.indexOf("}", c); + if (e > c) { float v = body.substring(c, e).toFloat(); val = constrain(v, mn, mx); } + }; + auto extractInt = [&](const char* key, uint32_t& val, uint32_t mn, uint32_t mx) { + int p = body.indexOf(key); + if (p < 0) return; + int c = p + strlen(key); + int e = body.indexOf(",", c); if (e < 0) e = body.indexOf("}", c); + if (e > c) { uint32_t v = (uint32_t)body.substring(c, e).toInt(); val = constrain(v, mn, mx); } + }; + + extractInt( "\"dwell_ms\":", sweepDwellMs, 1, 500); + uint32_t s1 = sweep1Steps, s2 = sweep2Steps; + extractInt( "\"steps1\":", s1, 2, 20); sweep1Steps = (uint8_t)s1; + extractInt( "\"steps2\":", s2, 2, 20); sweep2Steps = (uint8_t)s2; + extractFloat("\"span1_mhz\":", sweep1SpanMhz, 0.1f, 3.0f); + extractFloat("\"span2_mhz\":", sweep2SpanMhz, 0.1f, 3.0f); + + preferences.putInt("sweepDwell", (int)sweepDwellMs); + preferences.putInt("sweep1Steps", sweep1Steps); + preferences.putInt("sweep2Steps", sweep2Steps); + preferences.putFloat("sweep1Span", sweep1SpanMhz); + preferences.putFloat("sweep2Span", sweep2SpanMhz); + + logLine("[SWEEP] dwell=" + String(sweepDwellMs) + "ms steps=" + + String(sweep1Steps) + "/" + String(sweep2Steps) + + " span=" + String(sweep1SpanMhz,2) + "/" + String(sweep2SpanMhz,2) + "MHz"); + } + server.send(200, "application/json; charset=utf-8", + "{\"success\":true,\"dwell_ms\":" + String(sweepDwellMs) + + ",\"steps1\":" + String(sweep1Steps) + + ",\"steps2\":" + String(sweep2Steps) + + ",\"span1_mhz\":" + String(sweep1SpanMhz, 2) + + ",\"span2_mhz\":" + String(sweep2SpanMhz, 2) + "}"); +} + +static void handleAmpSettings() { + if (server.hasArg("plain")) { + String body = server.arg("plain"); + int p = body.indexOf("\"gain_db\":"); + if (p >= 0) { + int c = p + 10; + int e = body.indexOf(",", c); if (e < 0) e = body.indexOf("}", c); + if (e > c) { + ampGainDb = (int8_t)constrain(body.substring(c, e).toInt(), 0, 60); + preferences.putInt("ampGainDb", ampGainDb); + logLine("[AMP] Gain set to " + String(ampGainDb) + " dB"); + } + } + } + server.send(200, "application/json; charset=utf-8", + "{\"success\":true,\"gain_db\":" + String(ampGainDb) + "}"); +} + static void handleHealth() { String json = "{"; json += "\"ok\":true,"; @@ -696,11 +857,22 @@ void setup() { // Load preferences preferences.begin("jammer3", false); - jammingEnabled = preferences.getBool("jamEnabled", JAMMING_ENABLED); - jamPowerIdx = (uint8_t)preferences.getInt("jamPowerIdx", DEFAULT_JAM_POWER_IDX); + jammingEnabled = preferences.getBool("jamEnabled", JAMMING_ENABLED); + jamPowerIdx = (uint8_t)preferences.getInt("jamPowerIdx", DEFAULT_JAM_POWER_IDX); + ampGainDb = (int8_t) preferences.getInt("ampGainDb", DEFAULT_AMP_GAIN_DB); + sweepDwellMs = (uint32_t)preferences.getInt("sweepDwell", SWEEP_DWELL_MS); + sweep1Steps = (uint8_t)preferences.getInt("sweep1Steps", SWEEP_1_STEPS); + sweep2Steps = (uint8_t)preferences.getInt("sweep2Steps", SWEEP_2_STEPS); + sweep1SpanMhz = preferences.getFloat("sweep1Span", SWEEP_1_SPAN_MHZ); + sweep2SpanMhz = preferences.getFloat("sweep2Span", SWEEP_2_SPAN_MHZ); if (jamPowerIdx >= JAM_POWER_LEVELS) jamPowerIdx = DEFAULT_JAM_POWER_IDX; + if (sweepDwellMs < 1) sweepDwellMs = 1; + if (sweep1Steps < 2) sweep1Steps = 2; + if (sweep2Steps < 2) sweep2Steps = 2; jamPower = kPowerTable[jamPowerIdx]; - logLine("[NVS] jamEnabled=" + String(jammingEnabled) + " jamPowerIdx=" + String(jamPowerIdx) + " (" + String(jamPower) + " dBm)"); + logLine("[NVS] jamEnabled=" + String(jammingEnabled) + " jamPowerIdx=" + String(jamPowerIdx) + + " (" + String(jamPower) + " dBm) ampGain=" + String(ampGainDb) + + "dB sweepDwell=" + String(sweepDwellMs) + "ms"); logLine("[BOOT] CC1101 Key-Fob Jammer starting"); logLine("[BOOT] ESP32-S3 DevKitC-1"); @@ -773,14 +945,25 @@ void setup() { Serial.flush(); // Setup web server routes - server.on("/", handleRoot); - server.on("/api/log", handleLog); - server.on("/api/telemetry", handleTelemetry); - server.on("/api/health", handleHealth); - server.on("/api/toggle", HTTP_POST, handleToggle); - server.on("/api/settings", HTTP_POST, handleSettings); + server.on("/", handleRoot); + server.on("/api/log", handleLog); + server.on("/api/telemetry", handleTelemetry); + server.on("/api/health", handleHealth); + server.on("/api/toggle", HTTP_POST, handleToggle); + server.on("/api/settings", HTTP_POST, handleSettings); + server.on("/api/sweep", HTTP_POST, handleSweepSettings); + server.on("/api/amp", HTTP_POST, handleAmpSettings); server.onNotFound(handleNotFound); server.begin(); + + // OTA firmware updates over WiFi (connect to 'killer' AP, upload via PlatformIO OTA) + ArduinoOTA.setHostname("killer"); + ArduinoOTA.setPassword("killerpw"); + ArduinoOTA.onStart([]() { logLine("[OTA] Update starting..."); }); + ArduinoOTA.onEnd([]() { logLine("[OTA] Update complete, rebooting"); }); + ArduinoOTA.onError([](ota_error_t e) { logLine("[OTA] Error: " + String(e)); }); + ArduinoOTA.begin(); + logLine("[OTA] Ready — hostname: killer, port: 3232"); logLine("[HTTP] Server started on port " + String(WEB_PORT)); Serial.println("[HTTP] Server started on port " + String(WEB_PORT)); Serial.flush(); @@ -798,17 +981,14 @@ void setup() { } // Advance one radio to the next sweep frequency. -// Hops evenly across [center - span/2 .. center + span/2]. static void tickSweep(CC1101& radio, uint8_t& step, uint8_t steps, float center, float span, uint32_t& lastMs, float& curFreq) { const uint32_t now = millis(); - if (now - lastMs < SWEEP_DWELL_MS) return; + if (now - lastMs < sweepDwellMs) return; lastMs = now; - // Calculate next frequency - float freq = center - (span / 2.0f) + (span / (steps - 1)) * step; - if (freq != curFreq) { - // Put radio back to standby, retune, resume direct TX + float freq = center - (span / 2.0f) + (span / max((uint8_t)2, steps) - 1) * step; + if (fabsf(freq - curFreq) > 0.001f) { radio.standby(); if (radio.setFrequency(freq) == RADIOLIB_ERR_NONE) { radio.transmitDirect(); @@ -819,11 +999,23 @@ static void tickSweep(CC1101& radio, uint8_t& step, uint8_t steps, } void loop() { + ArduinoOTA.handle(); server.handleClient(); yield(); - static uint32_t lastHeartbeat = 0; const uint32_t now = millis(); + + // Auto-reinit watchdog: if jamming should be active but a radio failed, retry every 30s + if (jammingEnabled && now - lastReInitCheck >= 30000) { + lastReInitCheck = now; + bool needReinit = (radio1Status != 2 || radio2Status != 2); + if (needReinit) { + logLine("[WDT] Radio failure detected, attempting reinit..."); + startJamming(); + } + } + + static uint32_t lastHeartbeat = 0; if (now - lastHeartbeat >= 5000) { lastHeartbeat = now; Serial.println("[HEARTBEAT] up=" + String(now - uptimeStart) + "ms ip=" + WiFi.softAPIP().toString() + @@ -833,14 +1025,10 @@ void loop() { // Frequency sweep — hop both radios across their bands while jamming if (jammingEnabled) { - if (radio1Status == 2) { - tickSweep(radio1, sweepStep1, SWEEP_1_STEPS, - SWEEP_1_CENTER_MHZ, SWEEP_1_SPAN_MHZ, lastSweep1Ms, sweepFreq1); - } - if (radio2Status == 2) { - tickSweep(radio2, sweepStep2, SWEEP_2_STEPS, - SWEEP_2_CENTER_MHZ, SWEEP_2_SPAN_MHZ, lastSweep2Ms, sweepFreq2); - } + if (radio1Status == 2) + tickSweep(radio1, sweepStep1, sweep1Steps, SWEEP_1_CENTER_MHZ, sweep1SpanMhz, lastSweep1Ms, sweepFreq1); + if (radio2Status == 2) + tickSweep(radio2, sweepStep2, sweep2Steps, SWEEP_2_CENTER_MHZ, sweep2SpanMhz, lastSweep2Ms, sweepFreq2); } // Handle serial input for debugging