From 651af39211aa93f2a2bb951ad8ac700f0e824f02 Mon Sep 17 00:00:00 2001 From: drjones Date: Wed, 11 Mar 2026 09:07:48 -0700 Subject: [PATCH] Add signal detection notification and capture-aware OLED modes - capRecordISR: counts bit transitions to detect live RF activity - loop(): fires oledNotify("SIGNAL!", "CAUGHT -- PRESS STOP") once per recording session when transition count exceeds 80 (a few ms of any OOK/FSK burst), then logs bit index and transition count - oledDrawStatus: distinct header states for RECORDING (blinking box), REPLAYING (solid inverted), JAMMING ACTIVE, and STANDBY - Blue zone during capture shows frequency, radio, live progress bar, elapsed seconds, transition count; during replay shows bit count + loop indicator Made-with: Cursor --- src/main.cpp | 138 +++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 101 insertions(+), 37 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index c568028..9b15885 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -177,13 +177,19 @@ 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 bool capSigNotified = false; // fire OLED notification only once per session // ─── Capture/replay ISRs ────────────────────────────────────────────────────── static void IRAM_ATTR capRecordISR() { const uint32_t i = capIdx; if (i >= (uint32_t)(CAP_BUF_BYTES * 8)) { capBufFull = true; return; } - // Direct register read — 1-2 CPU cycles, safe from ISR const uint8_t bit = (uint8_t)((REG_READ(GPIO_IN_REG) >> capGdoPin) & 1u); + // Count transitions — cheap edge detection without extra storage + if (i > 0) { + const uint8_t prev = (capBuf[(i-1) >> 3] >> ((i-1) & 7)) & 1u; + if (bit != prev) capTransitions++; + } if (bit) capBuf[i >> 3] |= (1u << (i & 7)); else capBuf[i >> 3] &= ~(1u << (i & 7)); capIdx = i + 1; @@ -213,9 +219,11 @@ static void startCapture(float freq, uint8_t radioNum) { capFreq = freq; capRadioNum = radioNum; capGdoPin = (radioNum == 1) ? (gpio_num_t)CC1101_1_GDO0 : (gpio_num_t)CC1101_2_GDO0; - capIdx = 0; - capBufFull = false; - capRecBits = 0; + capIdx = 0; + capBufFull = false; + capRecBits = 0; + capTransitions = 0; + capSigNotified = false; memset(capBuf, 0, sizeof(capBuf)); CC1101& radio = (radioNum == 1) ? radio1 : radio2; @@ -521,10 +529,24 @@ static void oledDrawStatus() { const bool jam = jammingEnabled; const bool r1 = (radio1Status == 2); const bool r2 = (radio2Status == 2); + const CapMode cm = capMode; - // Yellow zone header — inverted box when jamming + // Yellow zone header u8g2.setFont(u8g2_font_6x10_tf); - if (jam) { + if (cm == CapMode::RECORDING) { + // Flashing border effect — blink every ~500 ms using bit 9 of millis() + if (millis() & 512) { + u8g2.drawBox(0, 0, 128, 13); + u8g2.setDrawColor(0); + } + u8g2.drawStr(2, 10, ">> RECORDING <<"); + u8g2.setDrawColor(1); + } else if (cm == CapMode::REPLAYING) { + u8g2.drawBox(0, 0, 128, 13); + u8g2.setDrawColor(0); + u8g2.drawStr(2, 10, ">> REPLAYING <<"); + u8g2.setDrawColor(1); + } else if (jam) { u8g2.drawBox(0, 0, 110, 13); u8g2.setDrawColor(0); u8g2.drawStr(2, 10, ">> JAMMING ACTIVE <<"); @@ -534,46 +556,78 @@ static void oledDrawStatus() { } oledPageDots(0); - // Blue zone — 5x7 font, 4 rows at y=22,32,43,54 then 5th at y=63 + // Blue zone — 5x7 font u8g2.setFont(u8g2_font_5x7_tf); const uint8_t nW = waveFrame; - // Row 1: ANT1 - if (r1) { - char buf[20]; - snprintf(buf, sizeof(buf), "1: %.3f MHz", (double)sweepFreq1); + if (cm == CapMode::RECORDING || cm == CapMode::REPLAYING) { + // Show capture/replay status instead of sweep info + char buf[24]; + snprintf(buf, sizeof(buf), "%.3f MHz R%u", + (double)capFreq, (unsigned)capRadioNum); u8g2.drawStr(0, 24, buf); - oledDrawWaves(101, 19, nW); - } else { - u8g2.drawStr(0, 24, "1: [OFFLINE]"); - } - // Row 2: ANT2 - if (r2) { - char buf[20]; - snprintf(buf, sizeof(buf), "2: %.3f MHz", (double)sweepFreq2); - u8g2.drawStr(0, 33, buf); - oledDrawWaves(101, 28, nW); - } else { - u8g2.drawStr(0, 33, "2: [OFFLINE]"); - } + // Progress bar for recording + if (cm == CapMode::RECORDING) { + const uint32_t pct = capIdx * 100 / (CAP_BUF_BYTES * 8); + u8g2.drawFrame(0, 26, 128, 5); + u8g2.drawBox(0, 26, (uint8_t)(pct * 128 / 100), 5); + snprintf(buf, sizeof(buf), "%lus / %us %lu tr", + (unsigned long)(capIdx / CAP_SAMPLE_HZ), + (unsigned)CAP_DURATION_S, + (unsigned long)capTransitions); + } else { + // Replaying — show loop position + const uint32_t pct = capRecBits ? capIdx * 100 / capRecBits : 0; + u8g2.drawFrame(0, 26, 128, 5); + u8g2.drawBox(0, 26, (uint8_t)(pct * 128 / 100), 5); + snprintf(buf, sizeof(buf), "%lu bits looping", + (unsigned long)capRecBits); + } + u8g2.drawStr(0, 40, buf); - // Row 3: power - { - char buf[28]; - snprintf(buf, sizeof(buf), "TX %d+%d=%ddBm", - (int)jamPower, (int)ampGainDb, (int)jamPower + (int)ampGainDb); - u8g2.drawStr(0, 44, buf); - } - - // Row 4: temp + heap OR FULL TX badge - if (jam && r1 && r2) { - u8g2.drawStr(0, 55, "[ FULL DUAL-BAND TX ]"); - } else { - char buf[28]; snprintf(buf, sizeof(buf), "%.1fC %lukB", (double)temperatureRead(), (unsigned long)(ESP.getFreeHeap() / 1024)); u8g2.drawStr(0, 55, buf); + } else { + // Normal jamming / standby display + // Row 1: ANT1 + if (r1) { + char buf[20]; + snprintf(buf, sizeof(buf), "1: %.3f MHz", (double)sweepFreq1); + u8g2.drawStr(0, 24, buf); + oledDrawWaves(101, 19, nW); + } else { + u8g2.drawStr(0, 24, "1: [OFFLINE]"); + } + + // Row 2: ANT2 + if (r2) { + char buf[20]; + snprintf(buf, sizeof(buf), "2: %.3f MHz", (double)sweepFreq2); + u8g2.drawStr(0, 33, buf); + oledDrawWaves(101, 28, nW); + } else { + u8g2.drawStr(0, 33, "2: [OFFLINE]"); + } + + // Row 3: power + { + char buf[28]; + snprintf(buf, sizeof(buf), "TX %d+%d=%ddBm", + (int)jamPower, (int)ampGainDb, (int)jamPower + (int)ampGainDb); + u8g2.drawStr(0, 44, buf); + } + + // Row 4: temp + heap OR FULL TX badge + if (jam && r1 && r2) { + u8g2.drawStr(0, 55, "[ FULL DUAL-BAND TX ]"); + } else { + char buf[28]; + snprintf(buf, sizeof(buf), "%.1fC %lukB", + (double)temperatureRead(), (unsigned long)(ESP.getFreeHeap() / 1024)); + u8g2.drawStr(0, 55, buf); + } } // Row 5: uptime small @@ -1732,6 +1786,16 @@ void loop() { const uint32_t now = millis(); // 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) { + capSigNotified = true; + oledNotify("SIGNAL!", "CAUGHT -- PRESS STOP", 3000); + logLine("[CAP] Signal detected: " + String(capTransitions) + " transitions @ bit " + String(capIdx)); + } + if (capMode == CapMode::RECORDING && capBufFull) { capTimerStop(); capRecBits = capIdx;