From 05dfde60e95927dde94728ba24c8161d7d35b4b6 Mon Sep 17 00:00:00 2001 From: drjones Date: Wed, 11 Mar 2026 10:47:37 -0700 Subject: [PATCH] Final review fixes: capture state, stopJamming idempotency, code cleanup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Critical bug fixes: - stopJamming() was returning early when jammingEnabled=false, leaving radios in unknown state and breaking capture when jamming was off - stopJamming() now idempotent: always stops noise timer (+ nulls pointer), drives GDO0 low, puts transmitting radios to standby — safe to call anytime - Add capPrevJamming flag saved before stopJamming() clears jammingEnabled; used by stopCapture() and buffer-full handler to correctly restart jamming after capture/replay sessions end Code quality: - capAnalyze() called twice per /api/capture/status request — now called once - Remove unused JAM_NOISE_PATTERN_LEN define (leftover from LEDC era) - Fix stale "LEDC 120 kHz square wave" comment in startJamming() - Web UI: show rec_bits (final stored count) instead of capIdx in Bits cell; progress bar shows 100% when in RECORDED/REPLAYING state Made-with: Cursor --- include/config.h | 2 -- src/main.cpp | 72 +++++++++++++++++++++++++++--------------------- 2 files changed, 40 insertions(+), 34 deletions(-) diff --git a/include/config.h b/include/config.h index 92493c5..4305f6f 100644 --- a/include/config.h +++ b/include/config.h @@ -30,8 +30,6 @@ // { -30, -20, -15, -10, 0, 5, 7, 10 } dBm #define JAM_POWER_LEVELS 8 #define DEFAULT_JAM_POWER_IDX 7 // index into power table (7 = 10 dBm, max) -#define JAM_NOISE_PATTERN_LEN 64 - // External amplifier gain in dB (used only for display — does not affect CC1101 output) #define DEFAULT_AMP_GAIN_DB 20 diff --git a/src/main.cpp b/src/main.cpp index 9b15885..a8282de 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -176,9 +176,10 @@ 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 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 to detect live signal +static hw_timer_t* capTimer = nullptr; +static volatile uint32_t capTransitions = 0; // edge count — used to detect live signal static bool capSigNotified = false; // fire OLED notification only once per session +static bool capPrevJamming = false; // jammingEnabled state saved before capture pauses it // ─── Capture/replay ISRs ────────────────────────────────────────────────────── static void IRAM_ATTR capRecordISR() { @@ -214,6 +215,7 @@ static void capTimerStop() { } static void startCapture(float freq, uint8_t radioNum) { + capPrevJamming = jammingEnabled; // save before stopJamming() clears it stopJamming(); capFreq = freq; @@ -250,6 +252,7 @@ static void startCapture(float freq, uint8_t radioNum) { 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; @@ -289,10 +292,13 @@ static void stopCapture() { logLine("[CAP] Replay stopped"); oledNotify("REPLAY", "STOPPED", 2500); } - // Restore pin directions then restart jamming + // 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 (jammingEnabled) startJamming(); + if (capPrevJamming) { + jammingEnabled = true; + startJamming(); + } } // Simple signal analysis — counts transitions to estimate original bitrate @@ -401,8 +407,8 @@ static void startJamming() { int stTx1 = RADIOLIB_ERR_NONE; int stTx2 = RADIOLIB_ERR_NONE; - // Start async direct TX — GDO0 becomes serial-data input, LEDC drives it with - // a 120 kHz square wave producing ±120 kHz FM noise instead of narrow-band CW. + // Start LFSR noise generator — drives GDO0 pins from a 50 kHz hardware timer ISR, + // producing spectrally flat pseudo-random broadband FM noise (~810 kHz per hop). noiseGenStart(); if (radio1Status == 1) { @@ -439,45 +445,46 @@ static void startJamming() { } } -// Stop jamming +// 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() { - if (!jammingEnabled) { - logLine("[JAM] Not currently jamming, ignoring stop request"); - return; + const bool wasActive = jammingEnabled; + + // 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; } - - logLine("[JAM] Stopping jamming system"); - - // Stop transmission on both radios - if (radio1Status == 2) { // Transmitting + 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; // Initialized but not transmitting - radio1Error = ""; + radio1Status = 1; + radio1Error = ""; } } - - if (radio2Status == 2) { // Transmitting + + 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; // Initialized but not transmitting - radio2Error = ""; + radio2Status = 1; + radio2Error = ""; } } - - // Stop noise timer and drive GDO0 pins low - if (s_noiseTimer) { timerAlarmDisable(s_noiseTimer); timerDetachInterrupt(s_noiseTimer); } - gpio_set_level((gpio_num_t)CC1101_1_GDO0, 0); - gpio_set_level((gpio_num_t)CC1101_2_GDO0, 0); jammingEnabled = false; - logLine("[JAM] Jamming stopped"); + if (wasActive) logLine("[JAM] Jamming stopped"); } // ─── OLED functions ────────────────────────────────────────────────────────── @@ -1295,13 +1302,13 @@ function capDrawWave(){ function capSetStatus(label,d){ document.getElementById('capStat').textContent=label; - if(d&&d.bits!==undefined)document.getElementById('capBits').textContent=d.bits.toLocaleString()+' bits'; + const dispBits=d?(d.rec_bits||d.bits):0;if(dispBits!==undefined)document.getElementById('capBits').textContent=dispBits.toLocaleString()+' bits'; if(d&&d.dur_ms)document.getElementById('capDur').textContent=(d.dur_ms/1000).toFixed(2)+'s'; if(d&&d.est_bps)document.getElementById('capBps').textContent=d.est_bps.toLocaleString()+' bps'; if(d&&d.duty_pct!==undefined)document.getElementById('capDuty').textContent=d.duty_pct+'%'; if(d&&d.freq)document.getElementById('capRecFreq').textContent=d.freq.toFixed(3)+' MHz'; - const pct=d&&d.buf_bits?Math.round(d.bits*100/d.buf_bits):0; - document.getElementById('capProg').style.width=pct+'%'; + const pct=d?(d.mode===1&&d.buf_bits?Math.round(d.bits*100/d.buf_bits):(d.mode===3&&d.rec_bits?Math.round(d.bits*100/d.rec_bits):100)):0; + document.getElementById('capProg').style.width=(d&&(d.mode===2||d.mode===3)?100:pct)+'%'; } let capPollTimer=null; @@ -1543,7 +1550,8 @@ static void handleCaptureStatus() { json += "\"pct\":" + String((uint32_t)(capIdx * 100UL / (CAP_BUF_BYTES * 8))) + ","; json += "\"freq\":" + String(capFreq, 3); if (capMode == CapMode::RECORDED || capMode == CapMode::REPLAYING) { - json += "," + capAnalyze().substring(1, capAnalyze().length() - 1); // merge JSON fields + const String analysis = capAnalyze(); + json += "," + analysis.substring(1, analysis.length() - 1); // merge JSON fields } json += "}"; server.send(200, "application/json; charset=utf-8", json); @@ -1806,7 +1814,7 @@ void loop() { 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 (jammingEnabled) startJamming(); + if (capPrevJamming) { jammingEnabled = true; startJamming(); } } // Auto-reinit watchdog: if jamming should be active but a radio failed, retry every 30s