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
This commit is contained in:
drjones
2026-04-01 19:20:28 -07:00
parent 2143981110
commit cc4aef445a
2 changed files with 153 additions and 40 deletions

View File

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

View File

@@ -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}
<h2>Controls</h2>
<p style="font-size:10px;color:#3a7a3e;margin-bottom:8px">TX power is fixed at +10 dBm (CC1101 max, TI SWRS061). Radiated level includes your antenna gain only no external PA.</p>
<p style="font-size:10px;color:#4fbf59;margin-bottom:8px">Jamming starts automatically on every power-up. Press <strong>Stop Jamming</strong> to go standby for capture (REC), then start jam again when done.</p>
<p style="font-size:10px;color:#3a7a3e;margin-bottom:6px"><strong>Jam strategy</strong> Direct: wide LFSR + async TX. Precision: narrower deviation / key rate. Flood: bursty random packets (hypothesis for burst-sensitive links; verify in your chamber).</p>
<p style="font-size:10px;color:#3a7a3e;margin-bottom:6px"><strong>Jam strategy</strong> 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).</p>
<div style="display:flex;flex-wrap:wrap;gap:6px;margin-bottom:10px">
<button type="button" class="jm" id="jmDirect">Direct</button>
<button type="button" class="jm" id="jmPrecision">Precision</button>
<button type="button" class="jm" id="jmFlood">Flood</button>
<button type="button" class="jm" id="jmCw">CW</button>
<button type="button" class="jm" id="jmPulse">Pulse</button>
<button type="button" class="jm" id="jmSpecial">Special</button>
</div>
<div class="row" style="margin-bottom:10px">
<div class="col" style="display:flex;flex-direction:column;gap:6px">
@@ -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());