From cc4aef445ab90152caedb84b3a77edc2beb69f35 Mon Sep 17 00:00:00 2001 From: drjones Date: Wed, 1 Apr 2026 19:20:28 -0700 Subject: [PATCH] Jam modes: CW, Pulse, Special (cypher-pulse style); GDO pin fixes; flood startTransmit - Module gpio=GDO0 for RadioLib packet timing; pinMode vs gpio matrix - SPECIAL: 60-byte blocking transmit @ 10ms like cypher-pulse - FLOOD: startTransmit; timing after burst - CW/PULSE modes; replay GDO drive before async TX - Web UI: mode buttons and telemetry jam_mode mapping Made-with: Cursor --- include/config.h | 5 ++ src/main.cpp | 188 +++++++++++++++++++++++++++++++++++++---------- 2 files changed, 153 insertions(+), 40 deletions(-) diff --git a/include/config.h b/include/config.h index 5d46c74..85722ff 100644 --- a/include/config.h +++ b/include/config.h @@ -54,11 +54,16 @@ #define JAM_PRECISION_DEV_R2_KHZ 55.0f #define JAM_PRECISION_LFSR_HZ 40000 +// Pulse jam: slow square wave (spoofs preamble AGC) +#define JAM_PULSE_HZ 2000 + // Flood jam: packet-mode random payloads (mcore1976-style bursty TX); deviation for symbol spread. #define JAM_FLOOD_PKT_BYTES 64 #define JAM_FLOOD_DEV_R1_KHZ 140.0f #define JAM_FLOOD_DEV_R2_KHZ 200.0f +// Special jam: 60-byte random payloads with 10ms delay (cypher-pulse exact clone). + // SmartRF-style dump: config space only (TI SWRS061); PATABLE/ strobes not included. #define CC1101_CFG_REG_LAST 0x2E diff --git a/src/main.cpp b/src/main.cpp index e2b8304..085c067 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -24,10 +24,12 @@ // Shared SPI; each Module uses its own CS. // Must pass SPIClass explicitly so RadioLib uses our configured pins. +// Fifth arg (gpio) must be the wired GDO0: CC1101::transmit() waits on getGpio() for packet +// timing; RADIOLIB_NC makes digitalRead always 0 → TX timeout → flood jam silently did nothing. static SPIClass spi(FSPI); static ArduinoHal hal(spi, SPISettings(SPI_SPEED_HZ, MSBFIRST, SPI_MODE0)); -static Module mod1(&hal, CC1101_1_CS, CC1101_1_GDO0, RADIOLIB_NC, RADIOLIB_NC); -static Module mod2(&hal, CC1101_2_CS, CC1101_2_GDO0, RADIOLIB_NC, RADIOLIB_NC); +static Module mod1(&hal, CC1101_1_CS, CC1101_1_GDO0, RADIOLIB_NC, CC1101_1_GDO0); +static Module mod2(&hal, CC1101_2_CS, CC1101_2_GDO0, RADIOLIB_NC, CC1101_2_GDO0); CC1101 radio1(&mod1); CC1101 radio2(&mod2); @@ -46,7 +48,7 @@ static int8_t jamPower = kPowerTable[DEFAULT_JAM_POWER_IDX]; // Policy: jam always starts after boot. NVS autoStartJam is forced true on each boot (no “boot jam off”). static bool autoStartJam = true; -enum class JamMode : uint8_t { DIRECT = 0, PRECISION = 1, FLOOD = 2 }; +enum class JamMode : uint8_t { DIRECT = 0, PRECISION = 1, FLOOD = 2, CW = 3, PULSE = 4, SPECIAL = 5 }; static JamMode jamMode = JamMode::DIRECT; static bool s_floodJamActive = false; static uint32_t s_jamLfsrKeyHz = JAM_LFSR_KEY_HZ; @@ -55,12 +57,15 @@ static const char* jamModeToCstr(JamMode m) { switch (m) { case JamMode::PRECISION: return "precision"; case JamMode::FLOOD: return "flood"; + case JamMode::CW: return "cw"; + case JamMode::PULSE: return "pulse"; + case JamMode::SPECIAL: return "special"; default: return "direct"; } } static JamMode jamModeFromU8(uint8_t u) { - if (u <= (uint8_t)JamMode::FLOOD) return (JamMode)u; + if (u <= (uint8_t)JamMode::SPECIAL) return (JamMode)u; return JamMode::DIRECT; } @@ -442,7 +447,7 @@ static void startCapture(float freq, uint8_t radioNum, bool isOOK) { 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); + pinMode(capGdoPin, INPUT); capTimerStop(); noInterrupts(); @@ -492,9 +497,11 @@ static void startReplay(uint8_t radioNum) { spi.endTransaction(); } - radio.transmitDirectAsync(); // GDO0 becomes data input to CC1101 + // Drive GDO0 output low before switching CC1101 to async TX to avoid glitching + pinMode(capGdoPin, OUTPUT); + digitalWrite(capGdoPin, LOW); - gpio_set_direction(capGdoPin, GPIO_MODE_OUTPUT); + radio.transmitDirectAsync(); // GDO0 becomes data input to CC1101 capTimerStop(); noInterrupts(); @@ -534,8 +541,8 @@ static void stopCapture() { oledNotify("REPLAY", "STOPPED", 2500); } // Restore pin direction then restart jamming if it was active before capture - gpio_set_direction(capGdoPin, GPIO_MODE_OUTPUT); - gpio_set_level(capGdoPin, 0); + pinMode(capGdoPin, OUTPUT); + digitalWrite(capGdoPin, LOW); if (capPrevJamming) { capPrevJamming = false; jammingEnabled = true; @@ -772,8 +779,9 @@ static void startJamming() { timerEnd(s_noiseTimer); s_noiseTimer = nullptr; } - gpio_set_level((gpio_num_t)CC1101_1_GDO0, 0); - gpio_set_level((gpio_num_t)CC1101_2_GDO0, 0); + // Default: inputs so begin() matches RadioLib; flood/packet TX will keep INPUT, direct jam sets OUTPUT later. + pinMode(CC1101_1_GDO0, INPUT); + pinMode(CC1101_2_GDO0, INPUT); radio1Status = 0; radio2Status = 0; @@ -847,9 +855,17 @@ static void startJamming() { radio1.setFrequencyDeviation(JAM_PRECISION_DEV_R1_KHZ); break; case JamMode::FLOOD: + case JamMode::SPECIAL: (void)radio1.setOOK(false); radio1.setFrequencyDeviation(JAM_FLOOD_DEV_R1_KHZ); break; + case JamMode::CW: + (void)radio1.setOOK(true); + break; + case JamMode::PULSE: + (void)radio1.setOOK(false); + radio1.setFrequencyDeviation(JAM_DEV_KHZ_R1_WIDE); + break; case JamMode::DIRECT: default: #if JAM_R1_USE_OOK @@ -871,13 +887,21 @@ static void startJamming() { if (radio2Status == 1) { switch (jamMode) { case JamMode::PRECISION: + (void)radio2.setOOK(false); radio2.setFrequencyDeviation(JAM_PRECISION_DEV_R2_KHZ); break; case JamMode::FLOOD: + case JamMode::SPECIAL: + (void)radio2.setOOK(false); radio2.setFrequencyDeviation(JAM_FLOOD_DEV_R2_KHZ); break; + case JamMode::CW: + (void)radio2.setOOK(true); + break; + case JamMode::PULSE: case JamMode::DIRECT: default: + (void)radio2.setOOK(false); radio2.setFrequencyDeviation(JAM_DEV_KHZ_R2_WIDE); break; } @@ -887,18 +911,65 @@ static void startJamming() { int stTx1 = RADIOLIB_ERR_NONE; int stTx2 = RADIOLIB_ERR_NONE; - if (jamMode == JamMode::FLOOD) { - logLine(String("[JAM] Mode: flood (packet PRNG, ") + String(JAM_FLOOD_PKT_BYTES) + " B) @ " + + if (jamMode == JamMode::FLOOD || jamMode == JamMode::SPECIAL) { + logLine(String("[JAM] Mode: ") + jamModeToCstr(jamMode) + " (packet PRNG) @ " + String(JAM_LOCK_FREQ_1_MHZ, 2) + " / " + String(JAM_LOCK_FREQ_2_MHZ, 2) + " MHz"); + // Packet TX: RadioLib polls GDO0 for timing — must be MCU input (not OUTPUT from LFSR path). + pinMode(CC1101_1_GDO0, INPUT); + pinMode(CC1101_2_GDO0, INPUT); if (radio1Status == 1) radio1Status = 2; if (radio2Status == 1) radio2Status = 2; s_floodJamActive = (radio1Status == 2 || radio2Status == 2); + s_noiseEn1 = false; + s_noiseEn2 = false; + } else if (jamMode == JamMode::CW) { + logLine(String("[JAM] Mode: cw (unmodulated carrier) @ ") + + String(JAM_LOCK_FREQ_1_MHZ, 2) + " / " + String(JAM_LOCK_FREQ_2_MHZ, 2) + " MHz"); + + // Async TX with GDO0 held HIGH to generate a continuous wave in OOK mode + pinMode(CC1101_1_GDO0, OUTPUT); + pinMode(CC1101_2_GDO0, OUTPUT); + digitalWrite(CC1101_1_GDO0, HIGH); + digitalWrite(CC1101_2_GDO0, HIGH); + + if (radio1Status == 1) { + stTx1 = radio1.transmitDirectAsync(); + if (stTx1 != RADIOLIB_ERR_NONE) { + radio1Status = -1; + radio1Error = "Transmit failed: " + String(stTx1); + logLine("[R1] transmitDirectAsync failed: " + String(stTx1)); + } else { + radio1Status = 2; + } + } + + if (radio2Status == 1) { + stTx2 = radio2.transmitDirectAsync(); + if (stTx2 != RADIOLIB_ERR_NONE) { + radio2Status = -1; + radio2Error = "Transmit failed: " + String(stTx2); + logLine("[R2] transmitDirectAsync failed: " + String(stTx2)); + } else { + radio2Status = 2; + } + } + s_noiseEn1 = false; s_noiseEn2 = false; } else { - s_jamLfsrKeyHz = (jamMode == JamMode::PRECISION) ? (uint32_t)JAM_PRECISION_LFSR_HZ : (uint32_t)JAM_LFSR_KEY_HZ; + if (jamMode == JamMode::PULSE) { + s_jamLfsrKeyHz = JAM_PULSE_HZ; + } else { + s_jamLfsrKeyHz = (jamMode == JamMode::PRECISION) ? (uint32_t)JAM_PRECISION_LFSR_HZ : (uint32_t)JAM_LFSR_KEY_HZ; + } logLine(String("[JAM] Mode: ") + jamModeToCstr(jamMode) + " | LFSR " + String(s_jamLfsrKeyHz) + " Hz on GDO0"); + // Async TX: CC1101 samples modulator data from GDO0 — drive the pin before CMD_TX. + pinMode(CC1101_1_GDO0, OUTPUT); + pinMode(CC1101_2_GDO0, OUTPUT); + digitalWrite(CC1101_1_GDO0, LOW); + digitalWrite(CC1101_2_GDO0, LOW); + if (radio1Status == 1) { stTx1 = radio1.transmitDirectAsync(); if (stTx1 != RADIOLIB_ERR_NONE) { @@ -956,8 +1027,8 @@ static void stopJamming() { timerEnd(s_noiseTimer); s_noiseTimer = nullptr; } - gpio_set_level((gpio_num_t)CC1101_1_GDO0, 0); - gpio_set_level((gpio_num_t)CC1101_2_GDO0, 0); + pinMode(CC1101_1_GDO0, INPUT); + pinMode(CC1101_2_GDO0, INPUT); if (radio1Status == 2) { int st1 = radio1.standby(); @@ -990,18 +1061,34 @@ static void jamFloodTick() { if (!s_floodJamActive || !jammingEnabled) return; static uint8_t pkt[JAM_FLOOD_PKT_BYTES]; static uint32_t lastMs; - const uint32_t now = millis(); - if ((uint32_t)(now - lastMs) < 2u) return; - lastMs = now; + + if (jamMode == JamMode::SPECIAL) { + // Exact cypher-pulse clone: 60-byte payload, 10ms wait, blocking transmit. + if ((uint32_t)(millis() - lastMs) < 10u) return; - if (radio1Status == 2) { - esp_fill_random(pkt, sizeof(pkt)); - (void)radio1.transmit(pkt, sizeof(pkt)); - } - if (radio2Status == 2) { - esp_fill_random(pkt, sizeof(pkt)); - (void)radio2.transmit(pkt, sizeof(pkt)); + if (radio1Status == 2) { + esp_fill_random(pkt, 60); + (void)radio1.transmit(pkt, 60); + } + if (radio2Status == 2) { + esp_fill_random(pkt, 60); + (void)radio2.transmit(pkt, 60); + } + } else { + // Flood mode: 64-byte payload, 2ms wait, async transmit. + if ((uint32_t)(millis() - lastMs) < 2u) return; + + if (radio1Status == 2) { + esp_fill_random(pkt, sizeof(pkt)); + (void)radio1.startTransmit(pkt, sizeof(pkt)); + } + if (radio2Status == 2) { + esp_fill_random(pkt, sizeof(pkt)); + (void)radio2.startTransmit(pkt, sizeof(pkt)); + } } + + lastMs = millis(); // record time AFTER transmit completes } static int hexNibble(char c) { @@ -1022,6 +1109,12 @@ static void handleJamMode() { newM = JamMode::FLOOD; } else if (body.indexOf("\"mode\":\"precision\"") >= 0) { newM = JamMode::PRECISION; + } else if (body.indexOf("\"mode\":\"cw\"") >= 0) { + newM = JamMode::CW; + } else if (body.indexOf("\"mode\":\"pulse\"") >= 0) { + newM = JamMode::PULSE; + } else if (body.indexOf("\"mode\":\"special\"") >= 0) { + newM = JamMode::SPECIAL; } else if (body.indexOf("\"mode\":\"direct\"") >= 0) { newM = JamMode::DIRECT; } else { @@ -1464,12 +1557,21 @@ static inline IRAM_ATTR uint32_t lfsrStep(uint32_t s) { } static void IRAM_ATTR noiseISR() { - const uint32_t s = lfsrStep(s_lfsr); - s_lfsr = s; - if (s_noiseEn1) gpio_set_level((gpio_num_t)CC1101_1_GDO0, (s >> 0) & 1u); - else gpio_set_level((gpio_num_t)CC1101_1_GDO0, 0); - if (s_noiseEn2) gpio_set_level((gpio_num_t)CC1101_2_GDO0, (s >> 7) & 1u); - else gpio_set_level((gpio_num_t)CC1101_2_GDO0, 0); + if (jamMode == JamMode::PULSE) { + static uint32_t pulseToggle = 0; + pulseToggle ^= 1; + if (s_noiseEn1) gpio_set_level((gpio_num_t)CC1101_1_GDO0, pulseToggle); + else gpio_set_level((gpio_num_t)CC1101_1_GDO0, 0); + if (s_noiseEn2) gpio_set_level((gpio_num_t)CC1101_2_GDO0, pulseToggle); + else gpio_set_level((gpio_num_t)CC1101_2_GDO0, 0); + } else { + const uint32_t s = lfsrStep(s_lfsr); + s_lfsr = s; + if (s_noiseEn1) gpio_set_level((gpio_num_t)CC1101_1_GDO0, (s >> 0) & 1u); + else gpio_set_level((gpio_num_t)CC1101_1_GDO0, 0); + if (s_noiseEn2) gpio_set_level((gpio_num_t)CC1101_2_GDO0, (s >> 7) & 1u); + else gpio_set_level((gpio_num_t)CC1101_2_GDO0, 0); + } } static void noiseGenStart() { @@ -1483,8 +1585,8 @@ static void noiseGenStart() { s_lfsr = esp_random(); if (s_lfsr == 0) s_lfsr = 0xDEADBEEFu; // LFSR must never be zero - gpio_set_direction((gpio_num_t)CC1101_1_GDO0, GPIO_MODE_OUTPUT); - gpio_set_direction((gpio_num_t)CC1101_2_GDO0, GPIO_MODE_OUTPUT); + pinMode(CC1101_1_GDO0, OUTPUT); + pinMode(CC1101_2_GDO0, OUTPUT); // Hardware timer at JAM_LFSR_KEY_HZ — true ISR, no jitter, no FreeRTOS overhead. // prescaler 80 → 1 MHz tick; alarm period = 1e6 / JAM_LFSR_KEY_HZ µs. @@ -1682,11 +1784,14 @@ textarea:focus{border-color:#4fbf59;outline:none}

Controls

TX power is fixed at +10 dBm (CC1101 max, TI SWRS061). Radiated level includes your antenna gain only — no external PA.

Jamming starts automatically on every power-up. Press Stop Jamming to go standby for capture (REC), then start jam again when done.

-

Jam strategy — Direct: wide LFSR + async TX. Precision: narrower deviation / key rate. Flood: bursty random packets (hypothesis for burst-sensitive links; verify in your chamber).

+

Jam strategy — Direct: wide LFSR + async TX. Precision: narrower deviation / key rate. Flood: bursty random packets (hypothesis for burst-sensitive links; verify in your chamber). CW: unmodulated carrier. Pulse: slow square wave (spoofs preamble AGC). Special: 60-byte blocking packet TX with 10ms delay (cypher-pulse clone).

+ + +
@@ -1901,7 +2006,7 @@ function applyTelemetry(t){ if(!ok2)parts.push('R2 FAULT now'); if(jam&&(tx1||tx2)){ const b=[];if(tx1)b.push('315 TX');if(tx2)b.push('433.92 TX'); - const jm=t.jam_mode_str||({0:'direct',1:'precision',2:'flood'}[t.jam_mode])||'direct'; + const jm=t.jam_mode_str||({0:'direct',1:'precision',2:'flood',3:'cw',4:'pulse',5:'special'}[t.jam_mode])||'direct'; parts.push(jm+' | '+b.join(' + ')); } document.getElementById('bs').textContent=parts.length?parts.join(' | ')+' | '+t.jam_power+' dBm chip (ERP + antenna only)' @@ -1958,8 +2063,8 @@ function applyTelemetry(t){ drawSp('cT',hT,'#86f28a',20,90); drawSp('cH',hH,'#4fbf59',0,320); const cd=document.getElementById('connDot');cd.style.background='#86f28a';cd.style.boxShadow='0 0 8px rgba(134,242,138,.7)'; - const jmStr=(t.jam_mode_str||({0:'direct',1:'precision',2:'flood'}[t.jam_mode])||'direct').toLowerCase(); - [['jmDirect','direct'],['jmPrecision','precision'],['jmFlood','flood']].forEach(([id,exp])=>{ + const jmStr=(t.jam_mode_str||({0:'direct',1:'precision',2:'flood',3:'cw',4:'pulse',5:'special'}[t.jam_mode])||'direct').toLowerCase(); + [['jmDirect','direct'],['jmPrecision','precision'],['jmFlood','flood'],['jmCw','cw'],['jmPulse','pulse'],['jmSpecial','special']].forEach(([id,exp])=>{ const el=document.getElementById(id);if(!el)return; el.classList.toggle('jm-active',jmStr===exp); }); @@ -2018,6 +2123,9 @@ document.getElementById('dl').addEventListener('click',()=>{const a=document.cre document.getElementById('jmDirect').addEventListener('click',()=>setJamMode('direct')); document.getElementById('jmPrecision').addEventListener('click',()=>setJamMode('precision')); document.getElementById('jmFlood').addEventListener('click',()=>setJamMode('flood')); +document.getElementById('jmCw').addEventListener('click',()=>setJamMode('cw')); +document.getElementById('jmPulse').addEventListener('click',()=>setJamMode('pulse')); +document.getElementById('jmSpecial').addEventListener('click',()=>setJamMode('special')); async function regExport(radio){ try{ @@ -2663,8 +2771,8 @@ void loop() { capBufFull = false; capMode = CapMode::RECORDED; interrupts(); - gpio_set_direction(capGdoPin, GPIO_MODE_OUTPUT); - gpio_set_level(capGdoPin, 0); + pinMode(capGdoPin, OUTPUT); + digitalWrite(capGdoPin, LOW); 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());