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
This commit is contained in:
drjones
2026-03-11 09:07:48 -07:00
parent 055073fc9d
commit 651af39211

View File

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