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:
53
src/main.cpp
53
src/main.cpp
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user