Performance and RF improvements: zero-alloc HTTP, PATABLE, ETag caching

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
This commit is contained in:
drjones
2026-03-11 23:13:03 -07:00
parent 13be73f7af
commit 1bd2e21a79

View File

@@ -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; }
}
json += "}";
server.send(200, "application/json; charset=utf-8", json);
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); // 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() {