diff --git a/include/config.h b/include/config.h index 0df8049..5d46c74 100644 --- a/include/config.h +++ b/include/config.h @@ -25,7 +25,8 @@ #define WEB_PORT 80 // Jamming: max CC1101 TX (+10 dBm). External PA removed — radiated power is chip + antenna gain only. -#define DEFAULT_AUTO_START_JAM false // NVS key autoStartJam; do not jam until user enables or saves auto-start +// Jam always starts after boot self-test; use web “Stop Jamming” (or capture REC) to pause for RX/capture. +#define DEFAULT_AUTO_START_JAM true // CC1101 only accepts 8 discrete power levels (index 0-7); jam path always uses max (10 dBm). #define JAM_POWER_LEVELS 8 #define DEFAULT_JAM_POWER_IDX 7 // 10 dBm — full device output (see TI SWRS061 PATABLE / output power) @@ -34,12 +35,32 @@ #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() +// GDO0 toggle rate during jam (1 MHz timer tick). Higher → wider AM/FM splatter on both carriers; 100 kHz typical. +#define JAM_LFSR_KEY_HZ 100000 // Fixed dual-carrier jamming: each radio holds one frequency at full TX power (TI CC1101 freq + deviation). +// Many NA ~315 MHz RKE remotes are ASK/OOK (see TI CC1101 datasheet MDMCFG2.MOD_FORMAT). Jam path uses OOK on +// R1 so LFSR on GDO0 keys the PA (broad AM sidebands). Set JAM_R1_USE_OOK 0 for 2-FSK only at JAM_DEV_KHZ_R1_WIDE. +// Fobs may sit on 314.8–315.2 MHz — measure with an SDR and retune JAM_LOCK_FREQ_1_MHZ if needed. #define JAM_LOCK_FREQ_1_MHZ 315.0f #define JAM_LOCK_FREQ_2_MHZ 433.92f #define JAM_DEV_KHZ_R2_WIDE 380.0f -#define JAM_DEV_KHZ_R1_NARROW 25.0f +#define JAM_DEV_KHZ_R1_NARROW 25.0f // probe / RadioLib begin() only +#define JAM_DEV_KHZ_R1_WIDE 200.0f // FSK jam spread when JAM_R1_USE_OOK is 0 (or OOK setup fails) +#define JAM_R1_USE_OOK 1 // 1 = R1 jam ASK/OOK (typical NA); 0 = wide 2-FSK on R1 + +// Precision jam: narrow 2-FSK on both + slower LFSR (energy in a smaller RF slice). +#define JAM_PRECISION_DEV_R1_KHZ 28.0f +#define JAM_PRECISION_DEV_R2_KHZ 55.0f +#define JAM_PRECISION_LFSR_HZ 40000 + +// Flood jam: packet-mode random payloads (mcore1976-style bursty TX); deviation for symbol spread. +#define JAM_FLOOD_PKT_BYTES 64 +#define JAM_FLOOD_DEV_R1_KHZ 140.0f +#define JAM_FLOOD_DEV_R2_KHZ 200.0f + +// SmartRF-style dump: config space only (TI SWRS061); PATABLE/ strobes not included. +#define CC1101_CFG_REG_LAST 0x2E // 0.96" SSD1306 OLED display — I2C via SW_I2C (any free GPIO) #define OLED_SDA_PIN 17 diff --git a/platformio.ini b/platformio.ini index c101a26..0d81407 100644 --- a/platformio.ini +++ b/platformio.ini @@ -27,3 +27,5 @@ board_build.extra_flags = -DBOARD_HAS_PSRAM monitor_speed = 115200 +; Avoid macOS picking Bluetooth “serial” instead of the ESP32 USB-UART +upload_port = /dev/cu.usbserial-A5069RR4 diff --git a/src/main.cpp b/src/main.cpp index b835a80..e2b8304 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -43,8 +43,26 @@ 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; +// Policy: jam always starts after boot. NVS autoStartJam is forced true on each boot (no “boot jam off”). +static bool autoStartJam = true; + +enum class JamMode : uint8_t { DIRECT = 0, PRECISION = 1, FLOOD = 2 }; +static JamMode jamMode = JamMode::DIRECT; +static bool s_floodJamActive = false; +static uint32_t s_jamLfsrKeyHz = JAM_LFSR_KEY_HZ; + +static const char* jamModeToCstr(JamMode m) { + switch (m) { + case JamMode::PRECISION: return "precision"; + case JamMode::FLOOD: return "flood"; + default: return "direct"; + } +} + +static JamMode jamModeFromU8(uint8_t u) { + if (u <= (uint8_t)JamMode::FLOOD) return (JamMode)u; + return JamMode::DIRECT; +} // Individual radio status tracking static int8_t radio1Status = -1; // 0=init, 1=standby/idle OK, 2=transmitting, -1=error @@ -74,6 +92,22 @@ static uint32_t hopCount2 = 0; static uint32_t minFreeHeap = 0xFFFFFFFF; // lowest heap ever observed static uint32_t lastTempWarnMs = 0; // rate-limit temperature warnings +// Boot self-test snapshot (SRES + RadioLib begin + VERSION); unchanged until next reboot +static bool bootSelftestR1Pass = false; +static bool bootSelftestR2Pass = false; +static uint32_t bootSelftestDurationMs = 0; +static String bootSelftestR1Detail = "not run"; +static String bootSelftestR2Detail = "not run"; + +// Last on-demand self-test (POST /api/selftest); does not overwrite boot snapshot +static bool lastSelftestRan = false; +static bool lastSelftestR1Pass = false; +static bool lastSelftestR2Pass = false; +static uint32_t lastSelftestDurationMs = 0; +static uint32_t lastSelftestAtMs = 0; +static String lastSelftestR1Detail = "never"; +static String lastSelftestR2Detail = "never"; + // ─── 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. @@ -268,8 +302,15 @@ static void noiseGenStart(); static void startJamming(); static void stopJamming(); static void probeRadiosStandby(); +static void runBootSelfTest(); +static void runManualSelfTest(); static void oledNotify(const char* l1, const char* l2, uint32_t dur); static void spiWriteReg(uint8_t csPin, uint8_t reg, uint8_t val); +static void jamFloodTick(); +static void handleJamMode(); +static void handleCc1101RegsGet(); +static void handleCc1101RegsPost(); +static void handleCc1101RegsRoute(); // ─── Noise generator globals (used by stopJamming before definition) ───────── static volatile uint32_t s_lfsr = 0xDEADBEEFu; @@ -283,7 +324,7 @@ 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 volatile uint32_t capRecBits = 0; // replay ISR + main; keep consistent with capIdx 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 @@ -387,9 +428,6 @@ static void startCapture(float freq, uint8_t radioNum, bool isOOK) { 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; @@ -407,7 +445,12 @@ static void startCapture(float freq, uint8_t radioNum, bool isOOK) { gpio_set_direction(capGdoPin, GPIO_MODE_INPUT); capTimerStop(); - capMode = CapMode::RECORDING; + noInterrupts(); + capIdx = 0; + capBufFull = false; + capRecBits = 0; + capMode = CapMode::RECORDING; + interrupts(); capTimer = timerBegin(3, 80, true); // timer 3, 1 MHz tick timerAttachInterrupt(capTimer, &capRecordISR, true); timerAlarmWrite(capTimer, 1000000 / CAP_SAMPLE_HZ, true); // period in µs @@ -426,7 +469,6 @@ static void startReplay(uint8_t radioNum) { 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; @@ -455,7 +497,10 @@ static void startReplay(uint8_t radioNum) { gpio_set_direction(capGdoPin, GPIO_MODE_OUTPUT); capTimerStop(); - capMode = CapMode::REPLAYING; + noInterrupts(); + capIdx = 0; + capMode = CapMode::REPLAYING; + interrupts(); capTimer = timerBegin(3, 80, true); timerAttachInterrupt(capTimer, &capReplayISR, true); timerAlarmWrite(capTimer, 1000000 / CAP_SAMPLE_HZ, true); @@ -469,16 +514,22 @@ static void startReplay(uint8_t radioNum) { static void stopCapture() { capTimerStop(); - if (capMode == CapMode::RECORDING) { + noInterrupts(); + const CapMode was = capMode; + if (was == CapMode::RECORDING) { capRecBits = capIdx; capMode = (capRecBits > 0) ? CapMode::RECORDED : CapMode::IDLE; + } else if (was == CapMode::REPLAYING) { + capMode = CapMode::RECORDED; + } + interrupts(); + if (was == CapMode::RECORDING) { 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; + } else if (was == CapMode::REPLAYING) { logLine("[CAP] Replay stopped"); oledNotify("REPLAY", "STOPPED", 2500); } @@ -654,6 +705,57 @@ static void probeRadiosStandby() { else logLine("[R2] probe FAIL: " + radio2Error); } +static String selftestTruncateDetail(const String& s) { + const size_t maxLen = 72; + if ((size_t)s.length() <= maxLen) return s; + return s.substring(0, (unsigned)maxLen) + "..."; +} + +// Runs on every power-up after SPI is ready: same path as chamber probe (TI SWRS061 + VERSION). +static void runBootSelfTest() { + const uint32_t t0 = millis(); + logLine("[SELFTEST] Boot radio check (every power-up)..."); + probeRadiosStandby(); + bootSelftestR1Pass = (radio1Status == 1); + bootSelftestR2Pass = (radio2Status == 1); + bootSelftestR1Detail = selftestTruncateDetail(bootSelftestR1Pass ? String("PASS") : radio1Error); + bootSelftestR2Detail = selftestTruncateDetail(bootSelftestR2Pass ? String("PASS") : radio2Error); + bootSelftestDurationMs = millis() - t0; + logLine("[SELFTEST] R1 " + bootSelftestR1Detail + " | R2 " + bootSelftestR2Detail + + " | " + String(bootSelftestDurationMs) + " ms"); + if (bootSelftestR1Pass && bootSelftestR2Pass) { + logLine("[SELFTEST] Result: ALL PASS"); + } else { + logLine("[SELFTEST] Result: FAIL — fix wiring/power before trusting TX"); + } +} + +// On-demand: no reboot; ends in standby or restores jam if it was on. Rejects while capture active (HTTP). +static void runManualSelfTest() { + const bool wasJam = jammingEnabled; + if (wasJam) stopJamming(); + + const uint32_t t0 = millis(); + logLine("[SELFTEST] Manual radio check..."); + probeRadiosStandby(); + + lastSelftestRan = true; + lastSelftestR1Pass = (radio1Status == 1); + lastSelftestR2Pass = (radio2Status == 1); + lastSelftestDurationMs = millis() - t0; + lastSelftestAtMs = millis() - uptimeStart; + lastSelftestR1Detail = selftestTruncateDetail(lastSelftestR1Pass ? String("PASS") : radio1Error); + lastSelftestR2Detail = selftestTruncateDetail(lastSelftestR2Pass ? String("PASS") : radio2Error); + + logLine("[SELFTEST] Manual R1 " + lastSelftestR1Detail + " | R2 " + lastSelftestR2Detail + + " | " + String(lastSelftestDurationMs) + " ms"); + + if (wasJam) { + jammingEnabled = true; + startJamming(); + } +} + // 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"); @@ -703,7 +805,6 @@ static void startJamming() { } else { radio1Status = 1; radio1.setFrequency(JAM_LOCK_FREQ_1_MHZ); - radio1.setFrequencyDeviation(JAM_DEV_KHZ_R1_NARROW); } } @@ -732,42 +833,99 @@ static void startJamming() { } 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; - + // Per-mode modulation (freq already set) 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; + switch (jamMode) { + case JamMode::PRECISION: + (void)radio1.setOOK(false); + radio1.setFrequencyDeviation(JAM_PRECISION_DEV_R1_KHZ); + break; + case JamMode::FLOOD: + (void)radio1.setOOK(false); + radio1.setFrequencyDeviation(JAM_FLOOD_DEV_R1_KHZ); + break; + case JamMode::DIRECT: + default: +#if JAM_R1_USE_OOK + if (radio1.setOOK(true) != RADIOLIB_ERR_NONE) { + logLine("[R1] setOOK failed — falling back to wide 2-FSK jam"); + (void)radio1.setOOK(false); + radio1.setFrequencyDeviation(JAM_DEV_KHZ_R1_WIDE); + } else { + logLine("[JAM] R1 ASK/OOK + LFSR (direct mode)"); + } +#else + (void)radio1.setOOK(false); + radio1.setFrequencyDeviation(JAM_DEV_KHZ_R1_WIDE); +#endif + break; } } 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; + switch (jamMode) { + case JamMode::PRECISION: + radio2.setFrequencyDeviation(JAM_PRECISION_DEV_R2_KHZ); + break; + case JamMode::FLOOD: + radio2.setFrequencyDeviation(JAM_FLOOD_DEV_R2_KHZ); + break; + case JamMode::DIRECT: + default: + radio2.setFrequencyDeviation(JAM_DEV_KHZ_R2_WIDE); + break; } } - s_noiseEn1 = (radio1Status == 2); - s_noiseEn2 = (radio2Status == 2); - if (s_noiseEn1 || s_noiseEn2) { - noiseGenStart(); + s_floodJamActive = false; + int stTx1 = RADIOLIB_ERR_NONE; + int stTx2 = RADIOLIB_ERR_NONE; + + if (jamMode == JamMode::FLOOD) { + logLine(String("[JAM] Mode: flood (packet PRNG, ") + String(JAM_FLOOD_PKT_BYTES) + " B) @ " + + String(JAM_LOCK_FREQ_1_MHZ, 2) + " / " + String(JAM_LOCK_FREQ_2_MHZ, 2) + " MHz"); + if (radio1Status == 1) radio1Status = 2; + if (radio2Status == 1) radio2Status = 2; + s_floodJamActive = (radio1Status == 2 || radio2Status == 2); + s_noiseEn1 = false; + s_noiseEn2 = false; + } else { + s_jamLfsrKeyHz = (jamMode == JamMode::PRECISION) ? (uint32_t)JAM_PRECISION_LFSR_HZ : (uint32_t)JAM_LFSR_KEY_HZ; + logLine(String("[JAM] Mode: ") + jamModeToCstr(jamMode) + " | LFSR " + String(s_jamLfsrKeyHz) + " Hz on GDO0"); + + 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) { @@ -787,6 +945,7 @@ static void startJamming() { static void stopJamming() { const bool wasActive = jammingEnabled; + s_floodJamActive = false; s_noiseEn1 = false; s_noiseEn2 = false; @@ -826,6 +985,163 @@ static void stopJamming() { if (wasActive) logLine("[JAM] Jamming stopped"); } +// Packet-flood jam: PRNG frames on both radios (shared SPI — sequential). +static void jamFloodTick() { + if (!s_floodJamActive || !jammingEnabled) return; + static uint8_t pkt[JAM_FLOOD_PKT_BYTES]; + static uint32_t lastMs; + const uint32_t now = millis(); + if ((uint32_t)(now - lastMs) < 2u) return; + lastMs = now; + + if (radio1Status == 2) { + esp_fill_random(pkt, sizeof(pkt)); + (void)radio1.transmit(pkt, sizeof(pkt)); + } + if (radio2Status == 2) { + esp_fill_random(pkt, sizeof(pkt)); + (void)radio2.transmit(pkt, sizeof(pkt)); + } +} + +static int hexNibble(char c) { + if (c >= '0' && c <= '9') return c - '0'; + if (c >= 'a' && c <= 'f') return 10 + (c - 'a'); + if (c >= 'A' && c <= 'F') return 10 + (c - 'A'); + return -1; +} + +static void handleJamMode() { + if (!server.hasArg("plain")) { + server.send(400, "application/json; charset=utf-8", "{\"ok\":false,\"err\":\"need_json\"}"); + return; + } + const String body = server.arg("plain"); + JamMode newM = JamMode::DIRECT; + if (body.indexOf("\"mode\":\"flood\"") >= 0) { + newM = JamMode::FLOOD; + } else if (body.indexOf("\"mode\":\"precision\"") >= 0) { + newM = JamMode::PRECISION; + } else if (body.indexOf("\"mode\":\"direct\"") >= 0) { + newM = JamMode::DIRECT; + } else { + server.send(400, "application/json; charset=utf-8", "{\"ok\":false,\"err\":\"bad_mode\"}"); + return; + } + + jamMode = newM; + preferences.putUChar("jamMode", (uint8_t)jamMode); + logLine(String("[JAM] UI mode -> ") + jamModeToCstr(jamMode)); + + const bool wasJam = jammingEnabled; + if (wasJam) { + stopJamming(); + jammingEnabled = true; + startJamming(); + } + + String j = String("{\"ok\":true,\"mode\":\"") + jamModeToCstr(jamMode) + "\"}"; + server.send(200, "application/json; charset=utf-8", j); +} + +static void handleCc1101RegsGet() { + if (!server.hasArg("radio")) { + server.send(400, "application/json; charset=utf-8", "{\"ok\":false,\"err\":\"radio\"}"); + return; + } + const int r = server.arg("radio").toInt(); + if (r != 1 && r != 2) { + server.send(400, "application/json; charset=utf-8", "{\"ok\":false,\"err\":\"radio\"}"); + return; + } + const uint8_t cs = (r == 1) ? CC1101_1_CS : CC1101_2_CS; + String hex; + hex.reserve((CC1101_CFG_REG_LAST + 1u) * 2u + 4u); + for (uint8_t a = 0; a <= CC1101_CFG_REG_LAST; a++) { + const uint8_t v = cc1101ReadRegister(cs, a); + char pair[4]; + snprintf(pair, sizeof(pair), "%02X", (unsigned)v); + hex += pair; + } + server.send(200, "application/json; charset=utf-8", + String("{\"ok\":true,\"radio\":") + r + ",\"hex\":\"" + hex + "\"}"); +} + +static void handleCc1101RegsPost() { + int r = 0; + String hx; + if (server.hasArg("radio") && server.hasArg("hex")) { + r = server.arg("radio").toInt(); + hx = server.arg("hex"); + } else if (server.hasArg("plain")) { + const String p = server.arg("plain"); + if (p.indexOf("\"radio\":1") >= 0 || p.indexOf("\"radio\": 1") >= 0) r = 1; + else if (p.indexOf("\"radio\":2") >= 0 || p.indexOf("\"radio\": 2") >= 0) r = 2; + const int iq = p.indexOf("\"hex\""); + if (iq < 0) { + server.send(400, "application/json; charset=utf-8", "{\"ok\":false,\"err\":\"missing_args\"}"); + return; + } + int q1 = p.indexOf('"', iq + 5); + q1 = p.indexOf('"', q1 + 1); + const int q2 = p.indexOf('"', q1 + 1); + if (q1 < 0 || q2 < 0) { + server.send(400, "application/json; charset=utf-8", "{\"ok\":false,\"err\":\"hex_parse\"}"); + return; + } + hx = p.substring(q1 + 1, q2); + } else { + server.send(400, "application/json; charset=utf-8", "{\"ok\":false,\"err\":\"missing_args\"}"); + return; + } + if (r != 1 && r != 2) { + server.send(400, "application/json; charset=utf-8", "{\"ok\":false,\"err\":\"radio\"}"); + return; + } + hx.trim(); + hx.replace(" ", ""); + const size_t need = (size_t)(CC1101_CFG_REG_LAST + 1) * 2u; + if (hx.length() < need) { + server.send(400, "application/json; charset=utf-8", "{\"ok\":false,\"err\":\"hex_len\"}"); + return; + } + uint8_t buf[64]; + for (size_t i = 0; i <= (size_t)CC1101_CFG_REG_LAST; i++) { + const int hi = hexNibble(hx[(unsigned)(i * 2)]); + const int lo = hexNibble(hx[(unsigned)(i * 2 + 1)]); + if (hi < 0 || lo < 0) { + server.send(400, "application/json; charset=utf-8", "{\"ok\":false,\"err\":\"hex_parse\"}"); + return; + } + buf[i] = (uint8_t)((unsigned)hi << 4 | (unsigned)lo); + } + + const bool wasJam = jammingEnabled; + if (wasJam) stopJamming(); + + const uint8_t cs = (r == 1) ? CC1101_1_CS : CC1101_2_CS; + for (uint8_t a = 0; a <= CC1101_CFG_REG_LAST; a++) { + spiWriteReg(cs, a, buf[a]); + } + logLine(String("[REG] SmartRF-style cfg import R") + String(r) + " (" + String(CC1101_CFG_REG_LAST + 1) + " bytes)"); + + if (wasJam) { + jammingEnabled = true; + startJamming(); + } + server.send(200, "application/json; charset=utf-8", "{\"ok\":true}"); +} + +static void handleCc1101RegsRoute() { + if (server.method() == HTTP_GET) { + handleCc1101RegsGet(); + } else if (server.method() == HTTP_POST) { + handleCc1101RegsPost(); + } else { + server.send(405, "text/plain", "Method Not Allowed"); + } +} + // ─── OLED functions ────────────────────────────────────────────────────────── // Queue a full-screen notification overlay for dur ms. @@ -873,9 +1189,14 @@ static void oledPageDots(uint8_t page) { // 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; + const bool r1Tx = (radio1Status == 2); + const bool r2Tx = (radio2Status == 2); + const bool r1Ok = (radio1Status != -1); + const bool r2Ok = (radio2Status != -1); + CapMode cm; + noInterrupts(); + cm = capMode; + interrupts(); // Yellow zone header u8g2.setFont(u8g2_font_6x10_tf); @@ -928,8 +1249,13 @@ static void oledDrawStatus() { (unsigned)CAP_DURATION_S, (unsigned long)currentTransitions); } else { - // Replaying — show loop position - const uint32_t pct = capRecBits ? capIdx * 100 / capRecBits : 0; + // Replaying — show loop position (atomic idx + recBits vs ISR) + uint32_t cidx, crec; + noInterrupts(); + cidx = capIdx; + crec = capRecBits; + interrupts(); + const uint32_t pct = crec ? cidx * 100 / crec : 0; u8g2.drawFrame(0, 26, 128, 5); u8g2.drawBox(0, 26, (uint8_t)(pct * 128 / 100), 5); snprintf(buf, sizeof(buf), "%lu bits looping", @@ -941,23 +1267,31 @@ static void oledDrawStatus() { (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); + // Normal jamming / standby: OK = probed CC1101; TX = actively jamming (status 2). + // Do not label idle/standby as OFFLINE — that was only for status -1 (hardware fault). + char buf[24]; + if (r1Ok) { + if (r1Tx) { + snprintf(buf, sizeof(buf), "1: %.3f MHz", (double)jamFreq1); + u8g2.drawStr(0, 24, buf); + oledDrawWaves(101, 19, nW); + } else { + snprintf(buf, sizeof(buf), "1: %.3f idle", (double)jamFreq1); + u8g2.drawStr(0, 24, buf); + } } 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); + if (r2Ok) { + if (r2Tx) { + snprintf(buf, sizeof(buf), "2: %.3f MHz", (double)jamFreq2); + u8g2.drawStr(0, 33, buf); + oledDrawWaves(101, 28, nW); + } else { + snprintf(buf, sizeof(buf), "2: %.3f idle", (double)jamFreq2); + u8g2.drawStr(0, 33, buf); + } } else { u8g2.drawStr(0, 33, "2: [OFFLINE]"); } @@ -970,7 +1304,7 @@ static void oledDrawStatus() { } // Row 4: temp + heap OR FULL TX badge - if (jam && r1 && r2) { + if (jam && r1Tx && r2Tx) { u8g2.drawStr(0, 55, "[ 315 + 433.92 LOCK ]"); } else { char buf[28]; @@ -1115,8 +1449,8 @@ static void oledTick() { // 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 +// A 32-bit Galois LFSR clocked at JAM_LFSR_KEY_HZ generates a maximal-length pseudo- +// random bit sequence (period 2^32-1). 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. @@ -1152,11 +1486,12 @@ static void noiseGenStart() { 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. + // Hardware timer at JAM_LFSR_KEY_HZ — true ISR, no jitter, no FreeRTOS overhead. + // prescaler 80 → 1 MHz tick; alarm period = 1e6 / JAM_LFSR_KEY_HZ µs. + const uint32_t noisePeriodUs = (1000000u + (s_jamLfsrKeyHz / 2u)) / s_jamLfsrKeyHz; 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 + timerAlarmWrite(s_noiseTimer, noisePeriodUs, true); timerAlarmEnable(s_noiseTimer); } @@ -1215,6 +1550,8 @@ select:focus{border-color:#4fbf59;outline:none} button{padding:6px 11px;background:#0a1e0c;color:#86f28a;border:1px solid #2a5a2e;cursor:pointer;font-family:inherit;font-size:11px;letter-spacing:.04em;transition:all .15s} button:hover{background:#142a16;box-shadow:0 0 8px rgba(134,242,138,.15)} button:active{transform:scale(.97)} +button.jm{font-size:10px;padding:5px 10px} +button.jm-active{outline:2px solid #86f28a;box-shadow:0 0 10px rgba(134,242,138,.28)} button.d{background:#140608;border-color:#4a1820;color:#f28a86} button.d:hover{background:#200a10;box-shadow:0 0 8px rgba(242,138,134,.15)} .err{color:#f28a86;font-size:10px;margin-top:2px} @@ -1228,7 +1565,13 @@ body{animation:bootIn .6s ease-out} @keyframes flicker{0%{opacity:1}3%{opacity:.4}6%{opacity:1}7%{opacity:.6}9%{opacity:1}100%{opacity:1}} h1{animation:flicker .4s ease-out} .sv{font-variant-numeric:tabular-nums} +.toast-wrap{position:fixed;left:50%;bottom:22px;transform:translateX(-50%);z-index:10000;pointer-events:none} +#toast{background:#0a1e0c;border:1px solid #4fbf59;color:#a0f5a4;padding:7px 16px;font-size:11px;letter-spacing:.06em;opacity:0;transition:opacity .22s,transform .22s;transform:translateY(10px);box-shadow:0 4px 22px rgba(0,0,0,.55);max-width:90vw;text-align:center} +#toast.show{opacity:1;transform:translateY(0)} +textarea{background:#020504;border:1px solid #1a3a1e;color:#86f28a;padding:6px;font-family:inherit;font-size:10px;resize:vertical} +textarea:focus{border-color:#4fbf59;outline:none}
+Boot: once per power-up before WiFi (SRES, RadioLib, VERSION). Last: most recent POST self-test (no reboot).
TX power is fixed at +10 dBm (CC1101 max, TI SWRS061). Radiated level includes your antenna gain only — no external PA.
+Jamming starts automatically on every power-up. Press Stop Jamming to go standby for capture (REC), then start jam again when done.
+Jam strategy — Direct: wide LFSR + async TX. Precision: narrower deviation / key rate. Flood: bursty random packets (hypothesis for burst-sensitive links; verify in your chamber).
+Export configuration bytes for one radio, edit offline, paste hex and apply. TX pauses briefly during SPI write.
+