/** * Dual CC1101 always-on key-fob jammer (315 MHz + 433.92 MHz). * ESP32-S3 DevKitC-1: two CC1101 on shared SPI. * On power-up, WiFi AP + web UI starts immediately; both radios begin jamming. * Simultaneous jamming on both 315 MHz and 433.92 MHz frequencies with adjustable power. */ #include #include #include #include #include #include #include #include "config.h" // Shared SPI; each Module uses its own CS. // Must pass SPIClass explicitly so RadioLib uses our configured pins. static SPIClass spi(FSPI); static ArduinoHal hal(spi, SPISettings(SPI_SPEED_HZ, MSBFIRST, SPI_MODE0)); static Module mod1(&hal, CC1101_1_CS, CC1101_1_GDO0, RADIOLIB_NC, RADIOLIB_NC); static Module mod2(&hal, CC1101_2_CS, CC1101_2_GDO0, RADIOLIB_NC, RADIOLIB_NC); CC1101 radio1(&mod1); CC1101 radio2(&mod2); static WebServer server(WEB_PORT); static Preferences preferences; // CC1101 valid discrete power levels in dBm (RadioLib only accepts these exact values) static const int8_t kPowerTable[JAM_POWER_LEVELS] = { -30, -20, -15, -10, 0, 5, 7, 10 }; // Jamming state static bool jammingEnabled = JAMMING_ENABLED; static uint8_t jamPowerIdx = DEFAULT_JAM_POWER_IDX; // index into kPowerTable static int8_t jamPower = 10; // actual dBm value passed to RadioLib // Individual radio status tracking static int8_t radio1Status = -1; // 0=standby, 1=initialized, 2=transmitting, -1=disabled/error static int8_t radio2Status = -1; static String radio1Error = "Disabled / not initialized"; static String radio2Error = "Disabled / not initialized"; // Telemetry 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; // Log ring buffer static constexpr size_t LOG_LINES = 100; static String logRing[LOG_LINES]; static size_t logHead = 0; static size_t logCount = 0; static void logLine(const String& s) { logRing[logHead] = s; logHead = (logHead + 1) % LOG_LINES; if (logCount < LOG_LINES) logCount++; Serial.println(s); } static String getLogsText() { String out; out.reserve(4096); const size_t start = (logCount == LOG_LINES) ? logHead : 0; for (size_t i = 0; i < logCount; i++) { const size_t idx = (start + i) % LOG_LINES; out += logRing[idx]; out += '\n'; } return out; } static String jsonEscape(const String& in) { String out; out.reserve(in.length() + 8); for (size_t i = 0; i < in.length(); ++i) { const char c = in.charAt(i); if (c == '\\') out += "\\\\"; else if (c == '\"') out += "\\\""; else if (c == '\n') out += "\\n"; else if (c == '\r') out += "\\r"; else if (c == '\t') out += "\\t"; else out += c; } return out; } // 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. static uint8_t probeCC1101(uint8_t csPin) { pinMode(csPin, OUTPUT); digitalWrite(csPin, HIGH); delayMicroseconds(50); spi.beginTransaction(SPISettings(SPI_SPEED_HZ, MSBFIRST, SPI_MODE0)); digitalWrite(csPin, LOW); delayMicroseconds(10); spi.transfer(0xF1); // read status reg 0x31 (VERSION) uint8_t val = spi.transfer(0x00); digitalWrite(csPin, HIGH); spi.endTransaction(); return val; } // Manually pulse CS to hardware-reset a CC1101 before RadioLib init. static void hardResetCC1101(uint8_t csPin) { pinMode(csPin, OUTPUT); digitalWrite(csPin, LOW); delayMicroseconds(5); digitalWrite(csPin, HIGH); delayMicroseconds(45); // Hold CS low, wait for MISO to settle, then release spi.beginTransaction(SPISettings(SPI_SPEED_HZ, MSBFIRST, SPI_MODE0)); digitalWrite(csPin, LOW); delay(10); spi.transfer(0x30); // SRES strobe digitalWrite(csPin, HIGH); spi.endTransaction(); delay(5); } // Start simultaneous jamming on both radios static void startJamming() { logLine("[JAM] Starting simultaneous jamming system"); logLine("[JAM] Power: " + String(jamPower) + " dBm"); // Reset radio status radio1Status = 0; radio2Status = 0; radio1Error = ""; radio2Error = ""; // Initialize radio 1 with retries int st1 = RADIOLIB_ERR_CHIP_NOT_FOUND; for (int attempt = 0; attempt < 3 && st1 != RADIOLIB_ERR_NONE; attempt++) { if (attempt > 0) { delay(50); } st1 = radio1.begin(CC1101_1_FREQ_MHZ, JAM_BITRATE_KBPS, JAM_FREQ_DEV_KHZ, JAM_RX_BW_KHZ, jamPower, 16); } if (st1 != RADIOLIB_ERR_NONE) { radio1Status = -1; radio1Error = "Init failed: " + String(st1); logLine("[R1] init failed: " + String(st1)); } else { radio1Status = 1; } // Initialize radio 2 with retries int st2 = RADIOLIB_ERR_CHIP_NOT_FOUND; for (int attempt = 0; attempt < 3 && st2 != RADIOLIB_ERR_NONE; attempt++) { if (attempt > 0) { delay(50); } st2 = radio2.begin(CC1101_2_FREQ_MHZ, JAM_BITRATE_KBPS, JAM_FREQ_DEV_KHZ, JAM_RX_BW_KHZ, jamPower, 16); } if (st2 != RADIOLIB_ERR_NONE) { radio2Status = -1; radio2Error = "Init failed: " + String(st2); logLine("[R2] init failed: " + String(st2)); } else { radio2Status = 1; } // Start both radios transmitting simultaneously int stTx1 = RADIOLIB_ERR_NONE; int stTx2 = RADIOLIB_ERR_NONE; if (radio1Status == 1) { stTx1 = radio1.transmitDirect(); if (stTx1 != RADIOLIB_ERR_NONE) { radio1Status = -1; radio1Error = "Transmit failed: " + String(stTx1); logLine("[R1] transmitDirect failed: " + String(stTx1)); } else { radio1Status = 2; // Transmitting } } if (radio2Status == 1) { stTx2 = radio2.transmitDirect(); if (stTx2 != RADIOLIB_ERR_NONE) { radio2Status = -1; radio2Error = "Transmit failed: " + String(stTx2); logLine("[R2] transmitDirect failed: " + String(stTx2)); } else { radio2Status = 2; // Transmitting } } if (radio1Status == 2 || radio2Status == 2) { logLine("[JAM] Jamming active:"); logLine("[JAM] Radio 1: 315 MHz at " + String(jamPower) + " dBm (status: " + String(radio1Status == 2 ? "TX" : "FAIL") + ")"); logLine("[JAM] Radio 2: 433.92 MHz at " + String(jamPower) + " dBm (status: " + String(radio2Status == 2 ? "TX" : "FAIL") + ")"); } else { logLine("[JAM] Both radios failed to start - check SPI connections"); logLine("[JAM] R1 error: " + radio1Error); logLine("[JAM] R2 error: " + radio2Error); // jammingEnabled stays true so it retries on next toggle or reboot } } // Stop jamming static void stopJamming() { if (!jammingEnabled) { logLine("[JAM] Not currently jamming, ignoring stop request"); return; } logLine("[JAM] Stopping jamming system"); // Stop transmission on both radios if (radio1Status == 2) { // Transmitting int st1 = radio1.standby(); if (st1 != RADIOLIB_ERR_NONE) { radio1Error = "Standby failed: " + String(st1); logLine("[R1] standby failed: " + String(st1)); } else { radio1Status = 1; // Initialized but not transmitting radio1Error = ""; } } if (radio2Status == 2) { // Transmitting int st2 = radio2.standby(); if (st2 != RADIOLIB_ERR_NONE) { radio2Error = "Standby failed: " + String(st2); logLine("[R2] standby failed: " + String(st2)); } else { radio2Status = 1; // Initialized but not transmitting radio2Error = ""; } } jammingEnabled = false; logLine("[JAM] Jamming stopped"); } // Update jamming power; idx is 0-7 mapping to kPowerTable dBm values. static void updateJamPower(uint8_t idx) { if (idx >= JAM_POWER_LEVELS) idx = JAM_POWER_LEVELS - 1; int8_t newDbm = kPowerTable[idx]; logLine("[JAM] Updating TX power: index " + String(idx) + " = " + String(newDbm) + " dBm"); jamPowerIdx = idx; jamPower = newDbm; preferences.putInt("jamPowerIdx", jamPowerIdx); if (radio1Status >= 1) { int st1 = radio1.setOutputPower(newDbm); if (st1 != RADIOLIB_ERR_NONE) { radio1Error = "Power update failed: " + String(st1); logLine("[R1] setOutputPower(" + String(newDbm) + ") failed: " + String(st1)); } else { radio1Error = ""; logLine("[R1] TX power -> " + String(newDbm) + " dBm"); } } if (radio2Status >= 1) { int st2 = radio2.setOutputPower(newDbm); if (st2 != RADIOLIB_ERR_NONE) { radio2Error = "Power update failed: " + String(st2); logLine("[R2] setOutputPower(" + String(newDbm) + ") failed: " + String(st2)); } else { radio2Error = ""; logLine("[R2] TX power -> " + String(newDbm) + " dBm"); } } logLine("[JAM] Power update complete"); } // Web server handlers const char kHtml[] = R"HTML( CC1101 Key-Fob Jammer

CC1101 Key-Fob Jammer

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

System Status

Loading...

Radio Status

Radio 1 (315 MHz)

Status: Unknown

Radio 2 (433.92 MHz)

Status: Unknown

Controls

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

System Log


    
)HTML"; static void handleRoot() { logLine("[HTTP] GET /"); server.sendHeader("Cache-Control", "no-store, max-age=0"); server.setContentLength(sizeof(kHtml) - 1); server.send(200, "text/html; charset=utf-8", ""); server.sendContent(kHtml); } static void handleLog() { server.send(200, "text/plain; charset=utf-8", getLogsText()); } static void handleTelemetry() { String json = "{"; json += "\"uptime_ms\":" + String(millis() - uptimeStart) + ","; json += "\"free_heap\":" + String(ESP.getFreeHeap()) + ","; json += "\"jamming_enabled\":" + String(jammingEnabled ? "true" : "false") + ","; 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 += "}"; server.send(200, "application/json; charset=utf-8", json); } static void handleToggle() { if (jammingEnabled) { stopJamming(); } else { startJamming(); } // Save new state preferences.putBool("jamEnabled", jammingEnabled); String json = "{\"enabled\":" + String(jammingEnabled ? "true" : "false") + "}"; server.send(200, "application/json; charset=utf-8", json); } static void handleSettings() { if (server.hasArg("plain")) { String body = server.arg("plain"); body.trim(); // Expected format: {"power_idx":7} int keyPos = body.indexOf("\"power_idx\":"); if (keyPos >= 0) { int colonPos = keyPos + 12; int endPos = body.indexOf(",", colonPos); if (endPos == -1) endPos = body.indexOf("}", colonPos); if (endPos > colonPos) { String valStr = body.substring(colonPos, endPos); valStr.trim(); uint8_t idx = (uint8_t)constrain(valStr.toInt(), 0, JAM_POWER_LEVELS - 1); updateJamPower(idx); } } else { logLine("[HTTP] No power_idx in JSON body"); } } else { logLine("[HTTP] No JSON body received"); } // Return current state String json = "{\"success\":true,\"power_idx\":" + String(jamPowerIdx) + ",\"jam_power\":" + String(jamPower) + "}"; server.send(200, "application/json; charset=utf-8", json); } static void handleHealth() { String json = "{"; json += "\"ok\":true,"; json += "\"uptime_ms\":" + String(millis() - uptimeStart) + ","; json += "\"heap\":" + String(ESP.getFreeHeap()) + ","; json += "\"ap_clients\":" + String(WiFi.softAPgetStationNum()); json += "}"; server.send(200, "application/json; charset=utf-8", json); } static void handleNotFound() { const String uri = server.uri(); logLine("[HTTP] 404 " + uri); server.send(404, "text/plain", "404: Not found"); } void setup() { // Shorter delay for Serial to initialize on ESP32-S3 in production Serial.begin(115200); // Wait for Serial to be ready (timeout after 500ms for production) unsigned long start = millis(); while (!Serial && (millis() - start) < 500) { delay(10); } // Immediate debug output to verify boot Serial.println("=== CAR-KEY-KILLER BOOT START ==="); Serial.flush(); uptimeStart = millis(); // Load preferences preferences.begin("jammer3", false); jammingEnabled = preferences.getBool("jamEnabled", JAMMING_ENABLED); jamPowerIdx = (uint8_t)preferences.getInt("jamPowerIdx", DEFAULT_JAM_POWER_IDX); if (jamPowerIdx >= JAM_POWER_LEVELS) jamPowerIdx = DEFAULT_JAM_POWER_IDX; jamPower = kPowerTable[jamPowerIdx]; logLine("[NVS] jamEnabled=" + String(jammingEnabled) + " jamPowerIdx=" + String(jamPowerIdx) + " (" + String(jamPower) + " dBm)"); logLine("[BOOT] CC1101 Key-Fob Jammer starting"); logLine("[BOOT] ESP32-S3 DevKitC-1"); Serial.flush(); // Initialize SPI (required for CC1101 communication) Serial.println("[SPI] Initializing SPI bus..."); Serial.flush(); // Drive CS pins HIGH before SPI init to prevent bus collisions pinMode(CC1101_1_CS, OUTPUT); digitalWrite(CC1101_1_CS, HIGH); pinMode(CC1101_2_CS, OUTPUT); digitalWrite(CC1101_2_CS, HIGH); delay(10); // Initialize the FSPI bus on the explicit ESP32-S3 pins spi.begin(SPI_SCK_PIN, SPI_MISO_PIN, SPI_MOSI_PIN, -1); // Pull MISO high to prevent floating bus reads from returning garbage pinMode(SPI_MISO_PIN, INPUT_PULLUP); logLine("[SPI] SPI bus initialized on SCK=" + String(SPI_SCK_PIN) + " MISO=" + String(SPI_MISO_PIN) + " MOSI=" + String(SPI_MOSI_PIN) + " speed=" + String(SPI_SPEED_HZ)); delay(150); // Allow CC1101 VCC to stabilize // Start WiFi AP Serial.println("[DEBUG] Starting WiFi AP..."); Serial.flush(); WiFi.persistent(false); WiFi.setSleep(false); WiFi.mode(WIFI_MODE_AP); WiFi.softAPdisconnect(true); delay(100); WiFi.softAPConfig(IPAddress(192, 168, 4, 1), IPAddress(192, 168, 4, 1), IPAddress(255, 255, 255, 0)); bool apOk = false; for (int attempt = 1; attempt <= 5 && !apOk; ++attempt) { if (strlen(WIFI_AP_PASS) == 0) { apOk = WiFi.softAP(WIFI_AP_SSID, nullptr, 1, 0, 4); } else { apOk = WiFi.softAP(WIFI_AP_SSID, WIFI_AP_PASS, 1, 0, 4); } Serial.println("[DEBUG] WiFi.softAP attempt " + String(attempt) + ": " + (apOk ? "OK" : "FAILED")); Serial.flush(); if (!apOk) { delay(300); } } Serial.println(String("[DEBUG] WiFi.softAP result: ") + (apOk ? "OK" : "FAILED")); Serial.flush(); if (!apOk) { logLine("[WIFI] softAP failed"); Serial.println("[ERROR] WiFi softAP failed"); Serial.flush(); } delay(250); IPAddress ip = WiFi.softAPIP(); logLine("[WIFI] AP started: " + String(WIFI_AP_SSID) + " IP: " + ip.toString()); Serial.println("[WIFI] AP SSID: " + String(WIFI_AP_SSID)); Serial.println("[WIFI] AP IP: " + ip.toString()); Serial.flush(); if (MDNS.begin("killer")) { MDNS.addService("http", "tcp", WEB_PORT); Serial.println("[MDNS] Started: http://killer.local"); } else { Serial.println("[MDNS] Failed"); } 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.onNotFound(handleNotFound); server.begin(); logLine("[HTTP] Server started on port " + String(WEB_PORT)); Serial.println("[HTTP] Server started on port " + String(WEB_PORT)); Serial.flush(); // Start jamming immediately if enabled if (jammingEnabled) { startJamming(); } else { radio1Status = -1; radio2Status = -1; radio1Error = "Disabled / not initialized"; radio2Error = "Disabled / not initialized"; logLine("[JAM] Jamming disabled on boot"); } } // 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; 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 radio.standby(); if (radio.setFrequency(freq) == RADIOLIB_ERR_NONE) { radio.transmitDirect(); curFreq = freq; } } step = (step + 1) % steps; } void loop() { server.handleClient(); yield(); static uint32_t lastHeartbeat = 0; const uint32_t now = millis(); if (now - lastHeartbeat >= 5000) { lastHeartbeat = now; Serial.println("[HEARTBEAT] up=" + String(now - uptimeStart) + "ms ip=" + WiFi.softAPIP().toString() + " clients=" + String(WiFi.softAPgetStationNum()) + " heap=" + String(ESP.getFreeHeap())); Serial.flush(); } // 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); } } // Handle serial input for debugging if (Serial.available()) { String cmd = Serial.readStringUntil('\n'); cmd.trim(); if (cmd == "start") { startJamming(); } else if (cmd == "stop") { stopJamming(); } else if (cmd == "status") { Serial.println("Jamming: " + String(jammingEnabled ? "ON" : "OFF")); Serial.println("Power: " + String(jamPower) + " dBm"); Serial.println("Radio 1 (315 MHz): " + String(jammingEnabled ? "TRANSMITTING" : "STANDBY")); Serial.println("Radio 2 (433.92 MHz): " + String(jammingEnabled ? "TRANSMITTING" : "STANDBY")); } } }