From 1bd2e21a796154153dd4d3780e2e9d750ab0fb2f Mon Sep 17 00:00:00 2001 From: drjones Date: Wed, 11 Mar 2026 23:13:03 -0700 Subject: [PATCH] Performance and RF improvements: zero-alloc HTTP, PATABLE, ETag caching MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Memory/Performance: - handleHealth: replaced String += with static snprintf buffer - handleCaptureStatus: replaced String += with static snprintf buffer, inlined capAnalyze() to eliminate intermediate String allocation - handleCaptureWave: replaced 256-iteration String += loop with static 1280-byte char buffer and snprintf — eliminates ~256 heap allocs per call - handleRoot: added ETag based on compile timestamp so the browser caches the ~15 KB HTML page and revalidates with If-None-Match; returns 304 Not Modified on subsequent loads instead of re-transmitting the full page RF Replay: - PATABLE OOK pulse shaping: before replay in OOK mode, writes PATABLE[0]=0x00 (full off) and PATABLE[1]=0xC0 (max +10 dBm) via SPI burst write. This gives the sharpest possible on/off keying contrast, eliminates residual carrier leakage during OFF bits, and maximizes effective replay range. Made-with: Cursor --- src/main.cpp | 103 +++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 76 insertions(+), 27 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index 79f9518..18d8cda 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -172,6 +172,7 @@ static void noiseGenStart(); static void startJamming(); static void stopJamming(); 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; @@ -286,10 +287,27 @@ static void startReplay(uint8_t radioNum) { 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); @@ -1439,7 +1457,14 @@ window.addEventListener('resize',capDrawWave); )HTML"; static void handleRoot() { - server.sendHeader("Cache-Control", "no-store, max-age=0"); + // 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); @@ -1655,13 +1680,13 @@ static void handleAmpSettings() { } 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 char buf[128]; + snprintf(buf, sizeof(buf), + "{\"ok\":true,\"uptime_ms\":%lu,\"heap\":%lu,\"ap_clients\":%d}", + (unsigned long)(millis() - uptimeStart), + (unsigned long)ESP.getFreeHeap(), + (int)WiFi.softAPgetStationNum()); + server.send(200, "application/json; charset=utf-8", buf); } // ─── Capture / replay HTTP handlers ────────────────────────────────────────── @@ -1689,30 +1714,52 @@ static void handleCaptureReplay() { } static void handleCaptureStatus() { - const String modeStr[] = {"idle","recording","recorded","replaying"}; + static const char* const modeStr[] = {"idle","recording","recorded","replaying"}; const uint8_t m = (uint8_t)capMode; - String json = "{"; - json += "\"mode\":" + String(m) + ","; - json += "\"mode_str\":\"" + modeStr[m < 4 ? m : 0] + "\","; - json += "\"bits\":" + String(capIdx) + ","; - json += "\"buf_bits\":" + String(CAP_BUF_BYTES * 8) + ","; - json += "\"rec_bits\":" + String(capRecBits) + ","; - json += "\"pct\":" + String((uint32_t)(capIdx * 100UL / (CAP_BUF_BYTES * 8))) + ","; - json += "\"freq\":" + String(capFreq, 3); + 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)capIdx, + (unsigned long)(CAP_BUF_BYTES * 8), + (unsigned long)capRecBits, + (unsigned long)(capIdx * 100UL / (CAP_BUF_BYTES * 8)), + (double)capFreq); + if (capMode == CapMode::RECORDED || capMode == CapMode::REPLAYING) { - const String analysis = capAnalyze(); - json += "," + analysis.substring(1, analysis.length() - 1); // merge JSON fields + 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)); + } } - json += "}"; - server.send(200, "application/json; charset=utf-8", json); + 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); // bits per point - String json = "["; + 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; @@ -1720,11 +1767,13 @@ static void handleCaptureWave() { for (uint32_t b = start; b < end; b++) { ones += (capBuf[b >> 3] >> (b & 7)) & 1u; } - json += String(bpp > 0 ? ones * 100 / bpp : 0); - if (p < N - 1) json += ','; + pos += snprintf(waveBuf + pos, sizeof(waveBuf) - pos, "%lu%s", + (unsigned long)(bpp > 0 ? ones * 100 / bpp : 0), + (p < N - 1) ? "," : ""); } - json += ']'; - server.send(200, "application/json", json); + waveBuf[pos++] = ']'; + waveBuf[pos] = '\0'; + server.send(200, "application/json", waveBuf); } static void handleNotFound() {