project // hardware
about
The ESP32 DIV v2.1 is an open-source multi-band wireless testing toolkit by CiferTech, built around the ESP32-S3. It supports WiFi, BLE, 2.4GHz and Sub-GHz frequencies — designed for wireless testing, signal analysis, jamming research and protocol exploration. The v2.1 is modular — a stable core board with a stackable shield via pogo-pin headers, keeping the device thin and expandable.
hardware
features
WiFi
Bluetooth (BLE)
2.4GHz (NRF24 ×3)
Sub-GHz (CC1101)
EAPOL / WPA
Firmware / Hardware
images
// WiFi — packet monitor, real-time waterfall, channel hop 1–13
// BLE — spoofer, scan + clone target advert payload
// Sub-GHz CC1101 — spectrum analyser, 300–930 MHz sweep
// Sub-GHz — jammer, noise burst across 315 / 433 / 868 / 915 MHz
// splash screen — segfault.solutions logo, NeoPixel cyan glow
// stock firmware — main menu
// ProtoPirate port — main menu
// ProtoPirate — packet list view
// ProtoPirate — packet detail view
halehound-div firmware
Custom firmware ported from HaleHound-CYD to the ESP32-DIV v2.1 (ESP32-S3). Touch input replaced with PCF8574 GPIO expander at confirmed I2C address 0x27 (M5Shark V2.0 specific — not 0x20 as documented). Display running in portrait 240×320 (rotation=2). SPI bus arbitrated across the display (HSPI dedicated), 3× NRF24 modules, CC1101, and SD card. Boot splash displays for 10 seconds with NeoPixel cyan glow, then serves as the menu background image with transparent item overlay.
huge_app partition scheme, no PSRAMpio run -e esp32s3-div --target uploadpin map
| peripheral | signal | gpio | notes |
|---|---|---|---|
| display | SCK | 36 | HSPI dedicated |
| display | MOSI | 35 | |
| display | MISO | 37 | |
| display | CS | 17 | |
| display | DC | 16 | |
| display | BL | 7 | |
| SDA | 8 | PCF8574 @ 0x27 | |
| SCL | 9 | UP=P7 DOWN=P5 LEFT=P3 RIGHT=P4 CENTER=P6 | |
| NRF24 #1 | CSN | 4 | |
| NRF24 #1 | CE | 15 | |
| NRF24 #2 | CSN | 48 | |
| NRF24 #2 | CE | 47 | |
| NRF24 #3 | CSN | 21 | ⚠ shared with IR RX — held HIGH during capture |
| NRF24 #3 | CE | 14 | ⚠ shared with IR TX — mutex required |
| CC1101 | CS | 5 | |
| CC1101 | GDO0 | 6 | |
| CC1101 | GDO2 | 3 | spi renamed to avoid TFT_eSPI collision |
| SD card | CS | 10 | |
| SD card | Detect | 38 | |
| IR | TX | 14 | ⚠ shared with NRF24 #3 CE |
| IR | RX | 21 | ⚠ shared with NRF24 #3 CSN |
| NeoPixel | DATA | 1 | cyan glow on splash, status indicators |
todo
SPI object instead of dedicated radioSPI HSPI bus
bjRadio local RF24 object per namespace, jamSPI core 0 task uses radioSPI, SELECT/UP/DOWN buttons mapped to toggle/mode change
nrf24_config.cpp SPI claim/release cleaned up — removed SPI.end()/SPI.begin() pattern, replaced with CS pin management only on shared radioSPI bus
esp_wifi_80211_tx() blocked at driver level for management frame type 0xC0
PCF_BTN_BACK_PIN duplicate with PCF_BTN_LEFT_PIN on P3 — back button not independent
changelog
esp_wifi_80211_tx() blocked at driver level for management frame type 0xC0handleTouch() rewritten from touch-only to full button nav — SELECT=scan toggle, UP/DOWN=scroll, LEFT/RIGHT=filter), SubGHz Jammer (SELECT=toggle, LEFT/RIGHT=freq, UP=sweep, DOWN=mode), EAPOL Capture (UP/DOWN=scroll AP list with cursor highlight, SELECT=pick AP, SELECT in capture=deauth toggle). Added touchButtonsUpdate() where missing.touch_buttons.h — BTN_BACK check consumed the event before capture-phase buttons saw it. Fix: added input_read_now() to input.cpp — direct PCF8574 raw state read bypassing debounce, stored in file-level _capEv for isDeauthTapped() and isSaveTapped(). Deauth toggle (SELECT) and save (UP) now working in capture phase.nrf24_attacks.cpp was calling SPI.begin() (global bus) and radio.begin() with no SPIClass pointer. Updated to radio.begin(&radioSPI). Added shared.h include. All NRF24 attack modules now use the correct shared radio SPI bus.neo_attack() (RED) / neo_idle() (OFF) API to shared.h. Hooked into: BLE Jammer (startJamming/stopJamming), SubGHz Jammer (start/stop), WiFi Deauther (initAttackMode/exitAttackMode), EAPOL deauth toggle, NRF24 jammer (nrf_jammer/nrf_stop). NeoPixels go solid red when any attack is active, off when idle.esp_wifi_80211_tx() returns ESP_ERR_WIFI_IF (0x102) with unsupport frame type: 0c0 on all platform versions tested (espressif32 5.3.0, 6.3.2, 6.13.0). ESP32-S3 WiFi driver blocks raw 802.11 management frame injection at the driver level regardless of IDF version. Affects WiFi Deauther and EAPOL deauth burst. Workaround under investigation (association flood, beacon duplication).draw_header(). PP_LOGO_H=28, PP_CONTENT_Y updated to account for logo height. Splash screen removed — logo is always visible in the UI.btn_pressed() from btn_input.h which wasn't calling touchButtonsUpdate(). Fixed btn_update() in btn_input.h to call touchButtonsUpdate(). Added BLE Predator, Lunatic Fringe, Phantom Flood, AirTag Replay to Bluetooth menu — were declared in switch cases but missing from the items array so unreachable. BLE scan stop added before GATT connect to prevent connect failures.ble_predator.h, flock_you.h, captive_portal.h, iot_recon.h were calling SD.begin() on an already-mounted card, causing silent failures. Added extern bool sd_available to shared.h. Modules now use the global sd_available flag instead of re-initialising SD. ProtoPirate packet saves to /protopirate/<protocol>_<timestamp>.txt confirmed working.pp_save_packet(): auto-saves every decoded packet to SD. Raw pulse snapshot added to PPPacket struct (up to 256 pulses). Added pp_radio_tx_pulses() to pp_radio.cpp — switches CC1101 to TX mode, bit-bangs OOK pulses on GDO0, returns to RX. In packet detail view: SELECT = save to SD, RIGHT = replay captured signal 3× via CC1101. Signal replay confirmed working on real hardware.draw_header(). PP_LOGO_H=28, PP_CONTENT_H updated to account for logo. Splash screen removed.raw_pulses[256] per packet to PPPacket caused 40KB RAM spike (32 history entries × 1280 bytes each). NRF24 and SD card both failed at boot due to heap corruption. Fixed by reducing to raw_pulses[64] (64 pulses sufficient for all tested SubGHz protocols) and PP_HISTORY_MAX from 32 to 16. RAM back to 45%. All radios and SD confirmed passing radio test.bjRadio local RF24 object per namespace, jamSPI core 0 task uses radioSPI, SELECT/UP/DOWN buttons mapped to toggle/mode changenrf24_config.cpp SPI claim/release cleaned up — removed SPI.end()/SPI.begin() pattern, replaced with CS pin management only on shared radioSPI busPCF_BTN_BACK_PIN duplicate with PCF_BTN_LEFT_PIN on P3 — back button not independentDIV_HAS_IR=0) to free pins for NRF3. (2) NRF2 and NRF3 had no SPI.begin() call — radio test only initialised bus for NRF1. (3) SD passed global SPI object to SD.begin() which was initialised with NRF1 CSN as default CS — caused silent SD failure.SPIClass radioSPI(HSPI) started once in setup() with CS=-1. All three NRF24 modules call RF24::begin(&radioSPI) with their own CE/CSN. SD card uses SD.begin(SD_CS, radioSPI, SD_SPI_FREQ). Ran fix_sd.sh patching 9 files: HaleHound-DIV.ino, eapol_capture.cpp, saved_captures.cpp, wifi_attacks.cpp, ble_predator.h, captive_portal.h, flock_you.h, iot_recon.h, pp_settings.cpp. Radio test result: NRF1 ✅ NRF2 ✅ NRF3 ✅ SD ✅ CC1101 ✅DIV_* aliases in shim. Added RADIO_SPI_SCK/MISO/MOSI and NRF24_CE/CSN aliases required by nrf24_config.cpp and bluetooth_attacks.cpp. Cleaned up nrf24ClaimSPI() / nrf24ReleaseSPI() — replaced SPI.end()/SPI.begin() bus restart pattern with CS pin management only. nrf24Radio.begin(&radioSPI) called once per use. Added shared.h include to pp_settings.cpp and nrf24_config.cpp.nrf24Radio object shared between BleJammer namespace (core 1) and jam task (core 0) with two different SPIClass instances (radioSPI and local jamSPI) causing RF24 internal state corruption. Fix: declared static RF24 bjRadio(NRF24_CE, NRF24_CSN) local to BleJammer namespace, core 0 task uses radioSPI directly. Added PCF8574 button bindings: SELECT = toggle jam on/off, UP/DOWN = cycle mode, BACK = exit. CP2102 UART port confirmed for serial monitor (separate from native USB flashing port).spi_manager per-module exclusive access, CC1101 setGDO() standardisation, detachInterrupt() cleanup pattern, and div_config.h as single pin definition source. NRF24 #3 hardware investigation still open.SPI object instead of dedicated radioSPI HSPI bussetGDO() call, and interrupt leakage causing "works once" failures. Established div_config.h as single source of truth for all pin definitions with cyd_config_div.h as compatibility shim.spi_manager — exclusive per-module bus access (one device on the bus at a time) to stop modules crashing each other. Standardised CC1101 init pattern: setSpiPin() → setGDO(GDO0, GDO2) → Init() on every module entry. Missing setGDO() was root cause of CC1101 being deaf after first module use. Added detachInterrupt() + buffer flush on every module exit — fixed "works once then breaks" failure across all interrupt-driven modules.SPIClass radioSPI(HSPI) global for all radios and SD card. All RF24::begin() calls updated to pass &radioSPI. SD card re-initialised with SD.begin(SD_CS, radioSPI, SD_SPI_FREQ). Previously using global SPI object caused SD to silently fail after any radio init.DIV_* aliases, RADIO_SPI_* defines, and NRF24_* pin mappings required by ProtoPirate modules. Fixed pp_settings.cpp missing shared.h include. Ran fix_sd.sh — patched 9 files across codebase to use radioSPI.SPI (VSPI) vs dedicated radioSPI (HSPI). NRF24 #1 ✅ NRF24 #2 ✅ NRF24 #3 ❌ — failure isolated to GPIO 41/42 assignment or pogo-pin contact on shield.btn_update() cache fix. PCF8574 UP/DOWN/LEFT/RIGHT/SELECT inputs stable. Began on-device validation of WiFi pack (Auth Flood, EAPOL Capture, Karma, Captive Portal) and BT+NRF24 pack (BLE Predator, Lunatic Fringe, MouseJack). SD loot saves under test (EAPOL .hc22000, BLE Predator recon log)./eapol/<SSID>.hc22000 (hashcat -m 22000 ready). Karma auto-deauths every 30s if no handshake. Credential logging to SD.WiFi.BSSID() API mismatch, BLE getProperties() rename, getNative()->val pointer cast, CC1101 SetMHZ→setMHZ case, MouseJack HID keymap C99 designated initialiser incompatibility with Xtensa GCC 8, btn_input.h redefinition conflict resolved by bridging to existing input_read() API. Button event consumed-before-read bug fixed with btn_update() cache. Clean build: 33.8% RAM / 55.6% flash. Firmware flashed via UART USB-C port.IRremoteESP8266. Raw capture and replay implemented. GPIO14 mutex handles NRF24 #3 CE conflict — NRF24 #3 deselected before IR acquire, restored on exit. RX requires NRF24 #3 CSN HIGH (GPIO21 shared).spi symbol — renamed CC1101 libdep). Added -DUSE_HSPI_PORT=1 to give TFT dedicated SPI instance. Moved tft.init() before SPI.begin() for radios. Display boots cleanly.USER_SETUP_LOADED flag blocking font loading in TFT_eSPI.-DBOARD_HAS_PSRAM flag — board has no PSRAM despite product page claim.pin conflicts & resolutions
Several GPIOs are shared between peripherals. Each conflict is handled in firmware — modules check out exclusive access before use and restore state on exit.
| gpio | peripheral A | peripheral B | resolution |
|---|---|---|---|
| 14 | NRF24 #3 CE | IR TX | GPIO14 mutex — NRF24 #3 deselected (CE LOW, CSN HIGH) before IR acquire. IR release restores NRF24 CE. Cannot use NRF24 #3 and IR simultaneously. → ir_acquire() / ir_release() in firmware |
| 21 | NRF24 #3 CSN | IR RX | NRF24 #3 CSN held HIGH during IR RX capture so the radio is deselected and the pin floats for the IR receiver. → digitalWrite(NRF3_CSN, HIGH) before IR capture |
| HSPI bus | TFT display (SCK=36, MOSI=35, MISO=37) | NRF24 ×3, CC1101, SD card (SCK=36, MOSI=35, MISO=37) | Dedicated SPIClass radioSPI(HSPI) instance for all radios and SD. TFT uses the same HSPI pins but with -DUSE_HSPI_PORT=1 giving it a separate SPI class instance. Each device selected via individual CS pin.→ radioSPI.begin() separate from TFT_eSPI init |
| CC1101 spi symbol | TFT_eSPI library (global spi) |
CC1101 library (global spi) |
CC1101 libdep spi symbol renamed in dependency to avoid link-time collision with TFT_eSPI's global spi object.→ CC1101 library patched in lib/ |
links