Final review fixes: capture state, stopJamming idempotency, code cleanup

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
This commit is contained in:
drjones
2026-03-11 10:47:37 -07:00
parent 651af39211
commit 05dfde60e9
2 changed files with 40 additions and 34 deletions

View File

@@ -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

View File

@@ -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