Phone browsers do a captive portal check (DNS + HTTP) when joining a
new WiFi AP. Without a DNS server the DNS query hangs forever and the
phone blocks all HTTP traffic to the network — page never loads.
Added DNSServer resolving all queries to 192.168.4.1. handleNotFound
now 302-redirects to / so captive portal probes get the main page.
Replaced single 15 KB send_P() with chunked transfer encoding in 512B
pieces with yield() between each chunk, keeping the WiFi stack responsive.
Made-with: Cursor
The setContentLength + send(200,type,"") + sendContent() three-step
pattern was unreliable — WebServer::send() with an empty String body
can mark the response complete internally, causing sendContent() to
be silently dropped. send_P() handles Content-Length, header prep,
and chunked body delivery from const flash data in a single call
with zero heap allocation.
Made-with: Cursor
At 1 MHz the timer ISR fired every 240 CPU cycles (ESP32-S3 @ 240 MHz).
ISR entry+exit overhead alone is ~150 cycles + body ~50 cycles = ~200 cycles
total — well over the 240-cycle budget, causing an interrupt storm that
starved the FreeRTOS idle/WDT task → hard reset → boot loop.
500 kHz gives a 2 µs / 480-cycle period, ~60% headroom over ISR time.
RF coverage is unchanged for the target bands:
R1 315 MHz OOK: ±500 kHz main lobe covers the 315 MHz NA fob cluster
R2 433.46 MHz 2-FSK 380 kHz dev: tone pair 433.08–433.84 + sidebands
fully spans 433–433.92 MHz
Made-with: Cursor
- JAM_LFSR_KEY_HZ: 100 kHz → 1 MHz; OOK main lobe now spans ±1 MHz
from 315 MHz → covers entire 314–316 MHz NA fob band at full power
- JAM_LOCK_FREQ_2_MHZ: 433.92 → 433.46 MHz (midpoint of 433–433.92);
with 380 kHz 2-FSK deviation + 1 MHz LFSR sidebands the noise blankets
432.1–434.8 MHz — all of 433–433.92 MHz covered at max output
- JAM_DEV_KHZ_R1_WIDE: 200 → 380 kHz (max CC1101 FSK deviation on R1)
- Boot default: SPECIAL → DIRECT (~100% duty cycle hardware LFSR,
no RadioLib packet timing, widest possible spectral splatter)
- handleRoot: sendContent(kHtml) → sendContent(kHtml, sizeof-1) to use
the length-aware overload and avoid a 15 KB String heap allocation
that was silently failing on a fragmented heap, breaking the web UI
Made-with: Cursor
Root cause: transmit() wraps startTransmit()+finishTransmit(). RadioLib's
finishTransmit() has a timeout of (1/bitRate)*128 = 0.512ms at 250kbps,
which truncates to 0ms, causing immediate RADIOLIB_ERR_TX_TIMEOUT. The
CC1101 was transmitting its 2.59ms packet correctly, but the 10ms inter-
packet guard meant 7.41ms of dead air — only 26% duty cycle. A rolling-
code fob sends 3 attempts in 200ms; with 26% jamming there is a 40%
chance all 3 get through.
Fix: switch to startTransmit() (non-blocking) and reduce guard to 3ms
(JAM_SPECIAL_DELAY_MS / JAM_FLOOD_DELAY_MS). CC1101 finishes the 2.59ms
packet autonomously and returns to IDLE. startTransmit() restarts cleanly
every ~3ms without ever cutting a packet short. Duty cycle: ~85%.
Made-with: Cursor
Bug 1 — FLOOD mode TXFIFO_UNDERFLOW (config.h):
JAM_FLOOD_PKT_BYTES was 64. RadioLib variable-length mode writes
one length byte to the FIFO first (dataSent=1), then MIN(len, FIFO_SIZE-1)=63
data bytes. CC1101 was told to send 64 bytes but only 63 were in the FIFO,
causing TXFIFO_UNDERFLOW every packet. Changed to 61 bytes (safe margin).
Bug 2 — STATUS register read protocol (main.cpp):
cc1101ReadRegister sent 0x80|addr for all addresses. Per SWRS061 §10.2,
for addresses 0x30-0x3D burst-bit=0 selects a command strobe, NOT a
register read. VERSION register (0x31) was actually triggering an SIDLE
strobe and returning the status byte. Fixed: use 0xC0|addr (read+burst)
for addr >= 0x30 to correctly access status registers.
Made-with: Cursor
startTransmit+next tick standby aborted packets before air complete.
ISR must not read jamMode; use s_noiseIsPulse set in noiseGenStart.
Made-with: Cursor
- Broadcast beacons with magic KLNK; no MAC pairing
- Track up to 8 peer MACs; stale after 12s
- Telemetry/health JSON: espnow_ok, espnow_peers
- Web metric + OLED health line
- README: ESP-NOW section and channel requirement
Made-with: Cursor
- encISR: now uses direct REG_READ instead of digitalRead for massive speedup
- capAnalyze: fixed off-by-one where bit 0 wasn't counted in duty cycle
- tickSweepFast: added step boundary guard (< 100)
- main loop/OLED: volatile capture counters (capIdx, capTransitions, capLongRuns) are now read inside noInterrupts()/interrupts() blocks to prevent race conditions
Made-with: Cursor
Pure CSS visual upgrades — zero JS overhead, no feature changes:
- CRT scanline overlay via body::after repeating gradient
- Boot-in animation (brightness flash + blur fade)
- Title flicker on load, persistent text-shadow glow
- Pulsing glow on active radio dots and connection indicator
- Shimmer gradient on progress bar
- Canvas sweep cursor and sparkline glow via shadowBlur
- Gradient fills on sparkline charts
- Hover states on cards, metric tiles, buttons (glow + scale)
- Custom thin scrollbar on log panel
- Tabular-nums for jitter-free metric updates
- Version badge in header
Made-with: Cursor
Memory/Performance:
- handleHealth: replaced String += with static snprintf buffer
- handleCaptureStatus: replaced String += with static snprintf buffer,
inlined capAnalyze() to eliminate intermediate String allocation
- handleCaptureWave: replaced 256-iteration String += loop with static
1280-byte char buffer and snprintf — eliminates ~256 heap allocs per call
- handleRoot: added ETag based on compile timestamp so the browser caches
the ~15 KB HTML page and revalidates with If-None-Match; returns 304
Not Modified on subsequent loads instead of re-transmitting the full page
RF Replay:
- PATABLE OOK pulse shaping: before replay in OOK mode, writes PATABLE[0]=0x00
(full off) and PATABLE[1]=0xC0 (max +10 dBm) via SPI burst write. This gives
the sharpest possible on/off keying contrast, eliminates residual carrier
leakage during OFF bits, and maximizes effective replay range.
Made-with: Cursor
Implemented military-grade fast sweeping by caching the CC1101 Phase-Locked
Loop (PLL) calibration registers during initialization.
- Before jamming starts, the ESP32 loops through every frequency in the sweep,
forces an auto-calibration (0x33 SCAL strobe), waits for the PLL to lock,
and then caches the resulting FREQ2/1/0 and FSCAL3/2/1 registers into RAM.
- Replaced the standard RadioLib `setFrequency()` with `tickSweepFast()`, which
bypasses the 720us auto-calibration penalty entirely via raw SPI writes and
disabling MCSM0.FS_AUTOCAL.
- Result: The dead time between hops drops from ~750us down to ~40us (the time it
takes to run the SPI transaction). Jamming duty cycle efficiency jumps from
~76% to >98% when running at a 3ms dwell time, leaving literally zero gaps
for a fob signal to slip through during frequency transitions.
Made-with: Cursor
- Added OOK / 2-FSK toggle for capture and replay to properly capture
and replay 90% of legacy car key fobs (which use OOK).
- Fixed the 'SIGNAL CAUGHT' false positive triggered by thermal noise
by implementing a software squelch in the `capRecordISR` that looks
for continuous runs of >15 samples instead of simple bit transitions.
- Fixed a minor ISR race condition when reading the rotary encoder
delta using `noInterrupts()`.
- Fixed `capPrevJamming` state to be properly consumed (`= false`)
so repeated presses of 'STOP' don't erroneously restart jamming multiple times.
Made-with: Cursor