diff --git a/include/config.h b/include/config.h index 5754460..92493c5 100644 --- a/include/config.h +++ b/include/config.h @@ -84,4 +84,11 @@ #define ENC_CLK_PIN 14 #define ENC_DT_PIN 21 +// Signal capture / replay +// Samples GDO0 (CC1101 demodulated output) at CAP_SAMPLE_HZ during direct RX mode. +// Bit-packed into a static buffer. Replay drives GDO0 in direct TX mode at same rate. +#define CAP_SAMPLE_HZ 100000 // 100 kHz sample clock +#define CAP_DURATION_S 4 // max capture window (seconds) +#define CAP_BUF_BYTES ((CAP_SAMPLE_HZ * CAP_DURATION_S) / 8 + 8) // ~50 KB + #endif diff --git a/src/main.cpp b/src/main.cpp index bd449d0..c568028 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -156,11 +156,166 @@ static String jsonEscape(const String& in) { // Forward declarations static void noiseGenStart(); +static void startJamming(); +static void stopJamming(); +static void oledNotify(const char* l1, const char* l2, uint32_t dur); // ─── Noise generator globals (used by stopJamming before definition) ───────── static volatile uint32_t s_lfsr = 0xDEADBEEFu; static hw_timer_t* s_noiseTimer = nullptr; +// ─── Signal capture / replay globals ───────────────────────────────────────── +// Buffer lives in BSS (static) — 50 KB, no heap fragmentation. +static uint8_t capBuf[CAP_BUF_BYTES]; + +enum class CapMode : uint8_t { IDLE=0, RECORDING=1, RECORDED=2, REPLAYING=3 }; +static volatile CapMode capMode = CapMode::IDLE; +static volatile uint32_t capIdx = 0; // current bit index +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 gpio_num_t capGdoPin = (gpio_num_t)CC1101_1_GDO0; +static hw_timer_t* capTimer = nullptr; + +// ─── 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); + if (bit) capBuf[i >> 3] |= (1u << (i & 7)); + else capBuf[i >> 3] &= ~(1u << (i & 7)); + capIdx = i + 1; +} + +static void IRAM_ATTR capReplayISR() { + uint32_t i = capIdx; + if (i >= capRecBits) { i = 0; } // loop seamlessly + const uint8_t bit = (capBuf[i >> 3] >> (i & 7)) & 1u; + gpio_set_level(capGdoPin, bit); + capIdx = i + 1; +} + +// ─── Capture/replay management ─────────────────────────────────────────────── +static void capTimerStop() { + if (capTimer) { + timerAlarmDisable(capTimer); + timerDetachInterrupt(capTimer); + timerEnd(capTimer); + capTimer = nullptr; + } +} + +static void startCapture(float freq, uint8_t radioNum) { + stopJamming(); + + 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; + memset(capBuf, 0, sizeof(capBuf)); + + CC1101& radio = (radioNum == 1) ? radio1 : radio2; + radio.standby(); + radio.setFrequency(freq); + radio.setFrequencyDeviation(JAM_FREQ_DEV_KHZ); + radio.receiveDirect(); // GDO0 becomes demodulated-data output from CC1101 + + // After receiveDirect, CC1101 drives GDO0 — set ESP32 pin as input to read it + gpio_set_direction(capGdoPin, GPIO_MODE_INPUT); + + capTimerStop(); + capMode = CapMode::RECORDING; + capTimer = timerBegin(3, 80, true); // timer 3, 1 MHz tick + timerAttachInterrupt(capTimer, &capRecordISR, true); + timerAlarmWrite(capTimer, 1000000 / CAP_SAMPLE_HZ, true); // period in µs + timerAlarmEnable(capTimer); + + logLine("[CAP] Recording " + String(freq, 3) + " MHz via radio " + + String(radioNum) + " @ " + String(CAP_SAMPLE_HZ/1000) + " kHz"); + oledNotify("RECORDING", (String(freq, 2) + " MHz").c_str(), 2500); +} + +static void startReplay(uint8_t radioNum) { + if (capRecBits == 0) { logLine("[CAP] Nothing captured to replay"); return; } + + stopJamming(); + + capRadioNum = radioNum; + capGdoPin = (radioNum == 1) ? (gpio_num_t)CC1101_1_GDO0 : (gpio_num_t)CC1101_2_GDO0; + capIdx = 0; + + CC1101& radio = (radioNum == 1) ? radio1 : radio2; + radio.standby(); + radio.setFrequency(capFreq); + radio.setFrequencyDeviation(JAM_FREQ_DEV_KHZ); + radio.transmitDirectAsync(); // GDO0 becomes data input to CC1101 + + gpio_set_direction(capGdoPin, GPIO_MODE_OUTPUT); + + capTimerStop(); + capMode = CapMode::REPLAYING; + capTimer = timerBegin(3, 80, true); + timerAttachInterrupt(capTimer, &capReplayISR, true); + timerAlarmWrite(capTimer, 1000000 / CAP_SAMPLE_HZ, true); + timerAlarmEnable(capTimer); + + logLine("[CAP] Replaying " + String(capFreq, 3) + " MHz, " + + String(capRecBits) + " bits (" + + String(capRecBits * 1000 / CAP_SAMPLE_HZ) + " ms), looping"); + oledNotify("REPLAYING", (String(capFreq, 2) + " MHz").c_str(), 2500); +} + +static void stopCapture() { + capTimerStop(); + if (capMode == CapMode::RECORDING) { + capRecBits = capIdx; + capMode = (capRecBits > 0) ? CapMode::RECORDED : CapMode::IDLE; + logLine("[CAP] Stopped: " + String(capRecBits) + " bits saved"); + oledNotify("CAPTURED", (String(capRecBits / 1000) + "k bits").c_str(), 2500); + } else if (capMode == CapMode::REPLAYING) { + capMode = CapMode::RECORDED; + logLine("[CAP] Replay stopped"); + oledNotify("REPLAY", "STOPPED", 2500); + } + // Restore pin directions then restart jamming + gpio_set_direction(capGdoPin, GPIO_MODE_OUTPUT); + gpio_set_level(capGdoPin, 0); + if (jammingEnabled) startJamming(); +} + +// Simple signal analysis — counts transitions to estimate original bitrate +// and measures duty cycle (fraction of 1s = carrier-on time). +static String capAnalyze() { + if (capRecBits < 100) return "{\"err\":\"no data\"}"; + + uint32_t ones = 0, transitions = 0; + uint8_t prev = (capBuf[0] >> 0) & 1u; + for (uint32_t i = 1; i < capRecBits; i++) { + const uint8_t b = (capBuf[i >> 3] >> (i & 7)) & 1u; + if (b) ones++; + if (b != prev) { transitions++; prev = b; } + } + + // Approximate original bitrate: each symbol averages capRecBits/transitions samples + const uint32_t avgRunLen = (transitions > 0) ? (capRecBits / transitions) : capRecBits; + const uint32_t estBps = (avgRunLen > 0) ? (CAP_SAMPLE_HZ / avgRunLen) : 0; + const uint32_t dutyPct = (uint32_t)(ones * 100UL / capRecBits); + const uint32_t durMs = capRecBits * 1000 / CAP_SAMPLE_HZ; + + char buf[200]; + snprintf(buf, sizeof(buf), + "{\"bits\":%lu,\"dur_ms\":%lu,\"transitions\":%lu," + "\"est_bps\":%lu,\"duty_pct\":%lu,\"freq\":%.3f}", + (unsigned long)capRecBits, (unsigned long)durMs, + (unsigned long)transitions, (unsigned long)estBps, + (unsigned long)dutyPct, (double)capFreq); + return String(buf); +} + // Manually probe a CC1101 via raw SPI to verify bus connectivity. // Reads the VERSION register (0xF1 = burst read of reg 0x31). // Returns the raw byte, or 0xFF if bus appears dead. @@ -801,6 +956,41 @@ pre{margin:0;padding:8px;background:#020504;border:1px solid #122814;height:28vh +