Bug fixes: OOK vs FSK modulation, Squelch, Rotary Race Condition

- Added OOK / 2-FSK toggle for capture and replay to properly capture
  and replay 90% of legacy car key fobs (which use OOK).
- Fixed the 'SIGNAL CAUGHT' false positive triggered by thermal noise
  by implementing a software squelch in the `capRecordISR` that looks
  for continuous runs of >15 samples instead of simple bit transitions.
- Fixed a minor ISR race condition when reading the rotary encoder
  delta using `noInterrupts()`.
- Fixed `capPrevJamming` state to be properly consumed (`= false`)
  so repeated presses of 'STOP' don't erroneously restart jamming multiple times.

Made-with: Cursor
This commit is contained in:
drjones
2026-03-11 12:12:21 -07:00
parent 05dfde60e9
commit 0c65c8809a

View File

@@ -175,9 +175,12 @@ static volatile bool capBufFull = false; // set by ISR when buffer fills
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 bool capIsOOK = true; // modulation: true=OOK, false=2-FSK
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 volatile uint32_t capTransitions = 0; // edge count — used for bitrate estimation
static volatile uint32_t capLongRuns = 0; // counts stable runs (>15 samples) to filter out thermal noise
static uint32_t capCurrentRun = 0; // current stable run length
static bool capSigNotified = false; // fire OLED notification only once per session
static bool capPrevJamming = false; // jammingEnabled state saved before capture pauses it
@@ -186,10 +189,16 @@ static void IRAM_ATTR capRecordISR() {
const uint32_t i = capIdx;
if (i >= (uint32_t)(CAP_BUF_BYTES * 8)) { capBufFull = true; return; }
const uint8_t bit = (uint8_t)((REG_READ(GPIO_IN_REG) >> capGdoPin) & 1u);
// Count transitions — cheap edge detection without extra storage
// Count transitions and long stable runs (software squelch)
if (i > 0) {
const uint8_t prev = (capBuf[(i-1) >> 3] >> ((i-1) & 7)) & 1u;
if (bit != prev) capTransitions++;
if (bit == prev) {
capCurrentRun++;
} else {
capTransitions++;
if (capCurrentRun > 15) capLongRuns++;
capCurrentRun = 0;
}
}
if (bit) capBuf[i >> 3] |= (1u << (i & 7));
else capBuf[i >> 3] &= ~(1u << (i & 7));
@@ -214,22 +223,26 @@ static void capTimerStop() {
}
}
static void startCapture(float freq, uint8_t radioNum) {
static void startCapture(float freq, uint8_t radioNum, bool isOOK) {
capPrevJamming = jammingEnabled; // save before stopJamming() clears it
stopJamming();
capFreq = freq;
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;
capSigNotified = false;
memset(capBuf, 0, sizeof(capBuf));
CC1101& radio = (radioNum == 1) ? radio1 : radio2;
radio.standby();
radio.setOOK(isOOK);
radio.setFrequency(freq);
radio.setFrequencyDeviation(JAM_FREQ_DEV_KHZ);
radio.receiveDirect(); // GDO0 becomes demodulated-data output from CC1101
@@ -261,6 +274,7 @@ static void startReplay(uint8_t radioNum) {
CC1101& radio = (radioNum == 1) ? radio1 : radio2;
radio.standby();
radio.setOOK(capIsOOK);
radio.setFrequency(capFreq);
radio.setFrequencyDeviation(JAM_FREQ_DEV_KHZ);
radio.transmitDirectAsync(); // GDO0 becomes data input to CC1101
@@ -296,6 +310,7 @@ static void stopCapture() {
gpio_set_direction(capGdoPin, GPIO_MODE_OUTPUT);
gpio_set_level(capGdoPin, 0);
if (capPrevJamming) {
capPrevJamming = false;
jammingEnabled = true;
startJamming();
}
@@ -739,8 +754,10 @@ static void oledTick() {
// Consume encoder — manual page change resets the auto-cycle timer
if (encDelta != 0) {
noInterrupts();
const int8_t d = encDelta;
encDelta = 0;
interrupts();
oledPage = (uint8_t)((oledPage + 3 + (d > 0 ? 1 : -1)) % 3);
oledPageMs = now; // reset auto-advance so page stays visible
}
@@ -1024,6 +1041,13 @@ pre{margin:0;padding:8px;background:#020504;border:1px solid #122814;height:28vh
<label>Target Frequency (MHz)</label>
<input type="number" id="capFreq" min="290" max="450" step="0.001" value="315.000" style="width:110px">
</div>
<div class="col">
<label>Modulation</label>
<select id="capMod" style="background:#020504;border:1px solid #1a3a1e;color:#86f28a;padding:3px 6px;font-family:inherit;font-size:11px">
<option value="ook">OOK / ASK (90% of fobs)</option>
<option value="fsk">2-FSK</option>
</select>
</div>
<div class="col">
<label>Radio</label>
<select id="capRadio" style="background:#020504;border:1px solid #1a3a1e;color:#86f28a;padding:3px 6px;font-family:inherit;font-size:11px">
@@ -1260,8 +1284,9 @@ async function capFetch(url){try{return await(await fetch(url)).json();}catch(e)
async function capStartRec(){
const freq=+document.getElementById('capFreq').value;
const radio=+document.getElementById('capRadio').value;
const mod=document.getElementById('capMod').value;
if(isNaN(freq)||freq<290||freq>450){alert('Frequency must be 290-450 MHz');return;}
const d=await capFetch('/api/capture/start?freq='+freq+'&radio='+radio);
const d=await capFetch('/api/capture/start?freq='+freq+'&radio='+radio+'&mod='+mod);
if(d){capSetStatus('RECORDING',d);capPollStart();}
}
@@ -1520,7 +1545,8 @@ static void handleHealth() {
static void handleCaptureStart() {
const float freq = server.hasArg("freq") ? server.arg("freq").toFloat() : 315.0f;
const uint8_t radio = server.hasArg("radio") ? (uint8_t)server.arg("radio").toInt() : 1;
startCapture(freq, radio);
const bool isOOK = server.hasArg("mod") ? (server.arg("mod") == "ook") : true;
startCapture(freq, radio, isOOK);
server.send(200, "application/json", "{\"status\":\"recording\",\"freq\":" +
String(freq, 3) + ",\"duration_ms\":" + String(CAP_DURATION_S * 1000) + "}");
}
@@ -1533,6 +1559,7 @@ static void handleCaptureStop() {
static void handleCaptureReplay() {
const uint8_t radio = server.hasArg("radio") ? (uint8_t)server.arg("radio").toInt() : 1;
// startReplay doesn't need isOOK from UI because it uses capIsOOK saved during capture
startReplay(radio);
server.send(200, "application/json", "{\"status\":\"replaying\",\"bits\":" +
String(capRecBits) + ",\"freq\":" + String(capFreq, 3) + "}");
@@ -1796,12 +1823,12 @@ void loop() {
// Capture state machine — runs in main loop (ISR sets flags, loop acts on them)
// Signal-present detection: fire OLED "SIGNAL!" once per session when
// the ISR has seen enough transitions to indicate a real RF burst.
// 80 transitions ≈ a few milliseconds of OOK/FSK activity at any typical fob rate.
if (capMode == CapMode::RECORDING && !capSigNotified && capTransitions > 80) {
// the ISR has seen enough stable bits to indicate a real RF burst.
// capLongRuns > 10 filters out thermal noise which transitions almost constantly.
if (capMode == CapMode::RECORDING && !capSigNotified && capLongRuns > 10) {
capSigNotified = true;
oledNotify("SIGNAL!", "CAUGHT -- PRESS STOP", 3000);
logLine("[CAP] Signal detected: " + String(capTransitions) + " transitions @ bit " + String(capIdx));
logLine("[CAP] Signal detected: " + String(capLongRuns) + " valid symbols @ bit " + String(capIdx));
}
if (capMode == CapMode::RECORDING && capBufFull) {
@@ -1814,7 +1841,11 @@ 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 (capPrevJamming) { jammingEnabled = true; startJamming(); }
if (capPrevJamming) {
capPrevJamming = false;
jammingEnabled = true;
startJamming();
}
}
// Auto-reinit watchdog: if jamming should be active but a radio failed, retry every 30s