handshake-capture-c6: SPI bus lock, SD v1.0.2 diagnostics, MUST_DO review

- SPI_Bus_Lock: serialize LCD + SD on shared SPI; pause display during SD
- SD_Init after WiFi scan; SD_IsReady gating; serial [SD]/[CAP] diagnostics
- MUST_DO_IMPROVEMENTS.md full firmware review backlog; README link
- Bump HANDSHAKE_FIRMWARE_VERSION to 1.0.2 (header)

Made-with: Cursor
This commit is contained in:
drjones
2026-03-21 17:59:45 -07:00
parent 804e71e175
commit 7e93324288
12 changed files with 331 additions and 71 deletions

View File

@@ -1,46 +1,56 @@
#include "Display_ST7789.h"
#include "SPI_Bus_Lock.h"
#define SPI_WRITE(_dat) SPI.transfer(_dat)
#define SPI_WRITE_Word(_dat) SPI.transfer16(_dat)
void SPI_Init() {
SPIBus_Init();
SPI.begin(EXAMPLE_PIN_NUM_SCLK, EXAMPLE_PIN_NUM_MISO, EXAMPLE_PIN_NUM_MOSI);
}
void LCD_WriteCommand(uint8_t Cmd) {
SPIBus_Lock();
SPI.beginTransaction(SPISettings(SPIFreq, MSBFIRST, SPI_MODE0));
digitalWrite(EXAMPLE_PIN_NUM_LCD_CS, LOW);
digitalWrite(EXAMPLE_PIN_NUM_LCD_DC, LOW);
SPI_WRITE(Cmd);
digitalWrite(EXAMPLE_PIN_NUM_LCD_CS, HIGH);
SPI.endTransaction();
SPIBus_Unlock();
}
void LCD_WriteData(uint8_t Data) {
SPIBus_Lock();
SPI.beginTransaction(SPISettings(SPIFreq, MSBFIRST, SPI_MODE0));
digitalWrite(EXAMPLE_PIN_NUM_LCD_CS, LOW);
digitalWrite(EXAMPLE_PIN_NUM_LCD_DC, HIGH);
SPI_WRITE(Data);
digitalWrite(EXAMPLE_PIN_NUM_LCD_CS, HIGH);
SPI.endTransaction();
SPIBus_Unlock();
}
void LCD_WriteData_Word(uint16_t Data) {
SPIBus_Lock();
SPI.beginTransaction(SPISettings(SPIFreq, MSBFIRST, SPI_MODE0));
digitalWrite(EXAMPLE_PIN_NUM_LCD_CS, LOW);
digitalWrite(EXAMPLE_PIN_NUM_LCD_DC, HIGH);
SPI_WRITE_Word(Data);
digitalWrite(EXAMPLE_PIN_NUM_LCD_CS, HIGH);
SPI.endTransaction();
SPIBus_Unlock();
}
void LCD_WriteData_nbyte(uint8_t* SetData, uint8_t* ReadData, uint32_t Size) {
SPIBus_Lock();
SPI.beginTransaction(SPISettings(SPIFreq, MSBFIRST, SPI_MODE0));
digitalWrite(EXAMPLE_PIN_NUM_LCD_CS, LOW);
digitalWrite(EXAMPLE_PIN_NUM_LCD_DC, HIGH);
SPI.transferBytes(SetData, ReadData, Size);
digitalWrite(EXAMPLE_PIN_NUM_LCD_CS, HIGH);
SPI.endTransaction();
SPIBus_Unlock();
}
void LCD_Reset(void) {
@@ -158,11 +168,13 @@ void LCD_SetCursor(uint16_t Xstart, uint16_t Ystart, uint16_t Xend, uint16_t Yen
}
void LCD_addWindow(uint16_t Xstart, uint16_t Ystart, uint16_t Xend, uint16_t Yend, uint16_t* color) {
SPIBus_Lock();
uint16_t Show_Width = Xend - Xstart + 1;
uint16_t Show_Height = Yend - Ystart + 1;
uint32_t numBytes = Show_Width * Show_Height * sizeof(uint16_t);
LCD_SetCursor(Xstart, Ystart, Xend, Yend);
LCD_WriteData_nbyte((uint8_t*)color, nullptr, numBytes);
SPIBus_Unlock();
}
void Backlight_Init(void) {

View File

@@ -4,11 +4,6 @@
#include <algorithm>
#include <cstring>
// Set 0 to use STA-only (test deauth + promiscuous on your core first).
#ifndef USE_SOFTAP
#define USE_SOFTAP 1
#endif
// ─── Internal prototypes ───
static void promiscuousRxCallback(void* buf, wifi_promiscuous_pkt_type_t type);
@@ -33,18 +28,10 @@ CapturePhase capturePhase = PHASE_OBSERVING;
// ─── Phase timing ───
// Longer windows: handshakes often arrive a few seconds after deauth; sweep all BSSIDs over time.
#define OBSERVE_DURATION_MS 8000
#define CAPTURE_TIMEOUT_MS 20000
#define DEAUTH_BURST_COUNT 5
#define DEAUTH_BURST_DELAY 2 // ms between frames in a burst
#define MAX_OBSERVE_CYCLES 5 // observe→deauth→capture rounds per channel visit
#define DEAUTH_GAP_AFTER_CLIENT_MS 80 // RX window between client bursts
#define DEAUTH_GAP_AFTER_SLOT_MS 120 // RX window between AP slots
// EAPOL frames from the same slot/session append to one buffer across cycles (same AP visit).
static unsigned long phase_timer = 0;
// Counts completed observe windows that led to a deauth burst (1..MAX_OBSERVE_CYCLES).
// Counts completed observe windows that led to a deauth burst (1..HANDSHAKE_MAX_OBSERVE_CYCLES).
static uint8_t deauth_cycle_count = 0;
static int last_channel = 0;
@@ -146,7 +133,7 @@ void handshakeCaptureProcessPending() {
SD_Init();
}
if (!SD_IsReady()) {
Serial.println("SD not ready — discarding pending PCAP");
HANDSHAKE_LOGLN("SD not ready — discarding pending PCAP");
Ui_SetWifiStatus("SD: insert card");
discardSlotCapture(s, i);
continue;
@@ -162,21 +149,13 @@ void handshakeCaptureProcessPending() {
filename.replace(" ", "_");
sanitizeFilenameForFat(filename);
File file = SD.open(filename.c_str(), FILE_WRITE);
bool ok = false;
if (file) {
if (file.write(s->pcap_buffer, s->pcap_size) == s->pcap_size) {
Serial.printf("Saved 4-way %s (%d bytes)\n", filename.c_str(), (int)s->pcap_size);
Ui_SetWifiStatus("4-way saved!");
ok = true;
} else {
Serial.println("SD write failed — discarding buffer");
Ui_SetWifiStatus("SD write err");
}
file.close();
bool ok = SD_WriteFileAtomic(filename.c_str(), s->pcap_buffer, s->pcap_size);
if (ok) {
HANDSHAKE_LOGF("Saved 4-way %s (%d bytes)\n", filename.c_str(), (int)s->pcap_size);
Ui_SetWifiStatus("4-way saved!");
} else {
Serial.println("SD open failed — discarding buffer");
Ui_SetWifiStatus("SD open err");
HANDSHAKE_LOGLN("SD save failed — discarding buffer");
Ui_SetWifiStatus("SD save err");
}
if (ok) {
@@ -236,14 +215,22 @@ bool compareRSSI(const WifiNetwork& a, const WifiNetwork& b) {
return a.rssi > b.rssi;
}
bool isCaptureable(int index) {
if (index < 0 || index >= MAX_NETWORKS || networks[index].ssid.isEmpty()) return false;
const String& enc = networks[index].encryption;
bool isSupportedCaptureAuth(const String& enc) {
if (enc == "WPA" || enc == "WPA2" || enc == "WPA/WPA2") return true;
if (enc == "WPA3" || enc == "WPA2/WPA3") return true;
return false;
}
bool isSupportedCaptureChannel(int channel) {
return channel >= HANDSHAKE_MIN_CHANNEL && channel <= HANDSHAKE_MAX_CHANNEL;
}
bool isCaptureable(int index) {
if (index < 0 || index >= MAX_NETWORKS || networks[index].ssid.isEmpty()) return false;
return isSupportedCaptureAuth(networks[index].encryption) &&
isSupportedCaptureChannel(networks[index].ch);
}
int getCaptureableCount() {
int n = 0;
for (int i = 0; i < MAX_NETWORKS; i++) {
@@ -254,19 +241,23 @@ int getCaptureableCount() {
}
static int getNextChannelSweep() {
int ch_counts[15] = {0};
int ch_counts[HANDSHAKE_MAX_CHANNEL + 1] = {0};
for (int i = 0; i < MAX_NETWORKS; i++) {
if (networks[i].ssid.isEmpty()) break;
if (networks[i].handshake_captured || !isCaptureable(i)) continue;
int ch = networks[i].ch;
if (ch >= 1 && ch <= 14) ch_counts[ch - 1]++;
if (isSupportedCaptureChannel(ch)) ch_counts[ch]++;
}
// Simple circular sweep across channels that still have targets.
int start = last_channel;
for (int step = 0; step < 14; step++) {
int ch = ((start + step) % 14) + 1;
if (ch_counts[ch - 1] > 0) return ch;
const int channel_count = HANDSHAKE_MAX_CHANNEL - HANDSHAKE_MIN_CHANNEL + 1;
int start_index = 0;
if (isSupportedCaptureChannel(last_channel)) {
start_index = (last_channel - HANDSHAKE_MIN_CHANNEL + 1) % channel_count;
}
for (int step = 0; step < channel_count; step++) {
int ch = HANDSHAKE_MIN_CHANNEL + ((start_index + step) % channel_count);
if (ch_counts[ch] > 0) return ch;
}
return 0;
}
@@ -396,9 +387,9 @@ static void sendDeauthToClient(const uint8_t* bssid, const uint8_t* client_mac)
memcpy(&deauth[10], bssid, 6);
memcpy(&deauth[16], bssid, 6);
for (int i = 0; i < DEAUTH_BURST_COUNT; i++) {
for (int i = 0; i < HANDSHAKE_DEAUTH_BURST_COUNT; i++) {
esp_wifi_80211_tx(WIFI_IF_STA, deauth, sizeof(deauth), false);
if (i < DEAUTH_BURST_COUNT - 1) delay(DEAUTH_BURST_DELAY);
if (i < HANDSHAKE_DEAUTH_BURST_COUNT - 1) delay(HANDSHAKE_DEAUTH_BURST_DELAY_MS);
}
}
@@ -408,17 +399,17 @@ static void sendDeauthBurstAll() {
if (!s->active) continue;
if (s->client_count > 0) {
Serial.printf("[%s] deauth -> %d client(s)\n", s->ssid, s->client_count);
HANDSHAKE_LOGF("[%s] deauth -> %d client(s)\n", s->ssid, s->client_count);
for (uint8_t c = 0; c < s->client_count; c++) {
sendDeauthToClient(s->bssid, s->clients[c].mac);
if (c + 1 < s->client_count) delay(DEAUTH_GAP_AFTER_CLIENT_MS);
if (c + 1 < s->client_count) delay(HANDSHAKE_DEAUTH_GAP_AFTER_CLIENT_MS);
}
} else {
Serial.printf("[%s] deauth -> broadcast (no clients mapped)\n", s->ssid);
HANDSHAKE_LOGF("[%s] deauth -> broadcast (no clients mapped)\n", s->ssid);
uint8_t bcast[6] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
sendDeauthToClient(s->bssid, bcast);
}
delay(DEAUTH_GAP_AFTER_SLOT_MS);
delay(HANDSHAKE_DEAUTH_GAP_AFTER_SLOT_MS);
}
}
@@ -460,7 +451,7 @@ void startMultiCapture() {
for (int k = 0; k < MAX_SLOTS; k++) latest_eapol_count[k] = 0;
esp_wifi_set_channel(current_channel, WIFI_SECOND_CHAN_NONE);
Serial.printf("Phase 2: observing ch %d (%d target(s))\n", current_channel, filled);
HANDSHAKE_LOGF("Phase 2: observing ch %d (%d target(s))\n", current_channel, filled);
}
void stopCapture() {
@@ -488,31 +479,31 @@ void handshakeCaptureLoop() {
// Phase 2 → Phase 3 (transient burst) → Phase 4
if (capturePhase == PHASE_OBSERVING) {
if (now - phase_timer >= OBSERVE_DURATION_MS) {
if (now - phase_timer >= HANDSHAKE_OBSERVE_DURATION_MS) {
int total = 0;
for (int i = 0; i < MAX_SLOTS; i++) {
if (slots[i].active) {
Serial.printf(" [%s] %d client(s)\n", slots[i].ssid, slots[i].client_count);
HANDSHAKE_LOGF(" [%s] %d client(s)\n", slots[i].ssid, slots[i].client_count);
total += slots[i].client_count;
}
}
Serial.printf("Observation done: %d client(s). Sending deauth burst.\n", total);
HANDSHAKE_LOGF("Observation done: %d client(s). Sending deauth burst.\n", total);
sendDeauthBurstAll();
capturePhase = PHASE_CAPTURING;
phase_timer = now;
deauth_cycle_count++;
Serial.println("Phase 4: capturing handshakes...");
HANDSHAKE_LOGLN("Phase 4: capturing handshakes...");
}
}
// Phase 4 timeout → re-observe, or end session after MAX_OBSERVE_CYCLES deauth bursts
if (capturePhase == PHASE_CAPTURING) {
if (now - phase_timer >= CAPTURE_TIMEOUT_MS) {
if (deauth_cycle_count < MAX_OBSERVE_CYCLES) {
Serial.printf("Capture timeout. Re-observing (cycle %d/%d)...\n",
(int)deauth_cycle_count + 1, MAX_OBSERVE_CYCLES);
if (now - phase_timer >= HANDSHAKE_CAPTURE_TIMEOUT_MS) {
if (deauth_cycle_count < HANDSHAKE_MAX_OBSERVE_CYCLES) {
HANDSHAKE_LOGF("Capture timeout. Re-observing (cycle %d/%d)...\n",
(int)deauth_cycle_count + 1, HANDSHAKE_MAX_OBSERVE_CYCLES);
portENTER_CRITICAL(&capture_mux);
for (int i = 0; i < MAX_SLOTS; i++) {
if (slots[i].active) slots[i].client_count = 0;
@@ -522,7 +513,7 @@ void handshakeCaptureLoop() {
capturePhase = PHASE_OBSERVING;
phase_timer = now;
} else {
Serial.println("Max observe/deauth cycles done; stopping capture on this channel.");
HANDSHAKE_LOGLN("Max observe/deauth cycles done; stopping capture on this channel.");
stopCapture();
}
}
@@ -639,7 +630,7 @@ static void promiscuousRxCallback(void* buf, wifi_promiscuous_pkt_type_t type) {
}
if (is_eapol) {
if (pcapAppendSlot(s, payload, len)) {
if (s->eapol_count < HANDSHAKE_EAPOL_FRAMES && pcapAppendSlot(s, payload, len)) {
s->eapol_count++;
latest_eapol_count[hit_slot] = s->eapol_count;
if (s->eapol_count >= HANDSHAKE_EAPOL_FRAMES) {

View File

@@ -6,9 +6,36 @@
#include <SD.h>
/** Semantic version (Serial banner + support). */
#define HANDSHAKE_FIRMWARE_VERSION "1.0.1"
#define HANDSHAKE_FIRMWARE_VERSION "1.0.3"
/** SD write only after this many EAPOL-Key frames (full WPA/WPA2 4-way). */
#ifndef HANDSHAKE_DEBUG
#define HANDSHAKE_DEBUG 0
#endif
#ifndef USE_SOFTAP
#define USE_SOFTAP 0
#endif
#define HANDSHAKE_MIN_CHANNEL 1
#define HANDSHAKE_MAX_CHANNEL 14
#define HANDSHAKE_OBSERVE_DURATION_MS 8000UL
#define HANDSHAKE_CAPTURE_TIMEOUT_MS 20000UL
#define HANDSHAKE_CHANNEL_TIMEOUT_MS 150000UL
#define HANDSHAKE_DEAUTH_BURST_COUNT 5
#define HANDSHAKE_DEAUTH_BURST_DELAY_MS 2
#define HANDSHAKE_MAX_OBSERVE_CYCLES 5
#define HANDSHAKE_DEAUTH_GAP_AFTER_CLIENT_MS 80
#define HANDSHAKE_DEAUTH_GAP_AFTER_SLOT_MS 120
#if HANDSHAKE_DEBUG
#define HANDSHAKE_LOGF(...) Serial.printf(__VA_ARGS__)
#define HANDSHAKE_LOGLN(msg) Serial.println(msg)
#else
#define HANDSHAKE_LOGF(...) do { } while (0)
#define HANDSHAKE_LOGLN(msg) do { } while (0)
#endif
/** Heuristic threshold only; frames are not validated as distinct 4-way steps. */
#define HANDSHAKE_EAPOL_FRAMES 4
// Two APs at once: less on-air contention, more time per target before rotating.
@@ -82,6 +109,8 @@ bool wasBssidCaptured(const uint8_t* bssid);
void scanNetworksSortedByRSSI();
void startMultiCapture();
void stopCapture();
bool isSupportedCaptureAuth(const String& encryption);
bool isSupportedCaptureChannel(int channel);
bool isCaptureable(int index);
void handshakeCaptureLoop();
int getCaptureableCount();

View File

@@ -1,4 +1,5 @@
#include "LVGL_Driver.h"
#include "SPI_Bus_Lock.h"
static lv_disp_draw_buf_t draw_buf;
static lv_color_t buf1[LVGL_BUF_LEN];
@@ -187,5 +188,6 @@ void Lvgl_Init(void) {
}
void Timer_Loop(void) {
if (SPIBus_IsDisplayPaused()) return;
lv_timer_handler();
}

View File

@@ -0,0 +1,97 @@
# Must-do improvements (firmware review)
Full pass over `handshake-capture-c6`: capture logic, SD, UI, display SPI, and main loop. Items below are **simple in concept** (some need careful implementation). Ordered by **severity**.
---
## 1. **Serialize shared SPI (LCD + SD)** — *critical*
**What:** ST7789 and the SD card share `SPI` (MOSI/SCLK/MISO; separate CS). **LVGL** flushes the display from `Timer_Loop()``LCD_*` → SPI, while **`SD.open` / `write` / `close`** use the same bus.
**Risk:** Interleaved transactions corrupt SD I/O or garble a display frame → failed mounts, bogus PCAP writes, or rare crashes.
**Do:** Add a single **mutex** (e.g. `portMUX_TYPE` or FreeRTOS mutex) that **every** LCD SPI transfer and **every** SD filesystem call takes around the full operation (including `SD.begin` path if the core does SPI under the hood). Alternatively: **pause LVGL** (skip `lv_timer_handler` / block flush) for the duration of each SD block in `handshakeCaptureProcessPending()`.
**Files:** `Display_ST7789.cpp`, `SD_Card.cpp`, `handshake-capture-c6.ino` (or one small `spi_bus_lock.h` used by both).
---
## 2. **Cap or validate `eapol_count` semantics** — *high*
**What:** Any unencrypted EAPOL-shaped frame increments the counter. **Four frames ≠ guaranteed one valid 4-way** (could be duplicates, other STAs, or rekeys).
**Do (minimal):** Document the limitation in `README.md`. **Better (still small):** stop incrementing after `HANDSHAKE_EAPOL_FRAMES` (saves buffer space and makes UI honest). **Better later:** parse EAPOL Key Info / replay counter to count **distinct** steps of the handshake (more code).
**Files:** `HandshakeCapture.cpp` (promisc callback EAPOL branch).
---
## 3. **2.4 GHzonly channel sweep** — *high for real environments*
**What:** `getNextChannelSweep()` only considers channels **114**. Scan results on **5 GHz** (or 6 GHz, if ever reported) are never visited, so those BSSIDs are never captured.
**Do:** Either **exclude** non-2.4G networks from the UI / `isCaptureable` path with a clear status, or **extend** sweep to supported `WiFi.channel()` values for your core/regulatory config (larger change: channel hop rules, dwell time).
**Files:** `HandshakeCapture.cpp` (`getNextChannelSweep`, `startMultiCapture`).
---
## 4. **Production vs debug serial** — *medium*
**What:** `[SD]`, `[CAP]`, `[SETUP]` strings and deauth logs are useful on the bench but noisy and slightly slow in the field.
**Do:** Wrap verbose `Serial.printf` / `println` in `#ifdef HANDSHAKE_DEBUG` (or a build flag) default **off** for “release” builds; keep boot **version line** always.
**Files:** `SD_Card.cpp`, `HandshakeCapture.cpp`, `handshake-capture-c6.ino`.
---
## 5. **`USE_SOFTAP` default** — *medium*
**What:** `WiFi.mode(WIFI_AP_STA)` runs a soft AP **without** a clear use case in this sketch (capture is STA + promisc). Extra RF/channel behavior and attack surface.
**Do:** Default `#define USE_SOFTAP 0` unless you explicitly need AP mode; document how to enable for lab tests.
**Files:** `HandshakeCapture.cpp`, `README.md`.
---
## 6. **Long SD write vs watchdog / UI** — *medium*
**What:** Writing ~4 KB PCAP can block the main loop for tens of ms. LVGL stalls; on some configs the **task watchdog** could trip if SD is slow or the card hangs.
**Do:** After large `file.write`, optional `yield()`; if WDT issues appear, `esp_task_wdt_reset()` in the SD path **only** with measured care, or increase WDT timeout in `setup`. Prefer fixing SPI contention (#1) first.
**Files:** `HandshakeCapture.cpp` (`handshakeCaptureProcessPending`), `setup()`.
---
## 7. **Rescan invalidates “ready” list vs slots** — *low*
**What:** During capture, the network list is static. If user triggers rescan (or you add a button later), `network_index` in slots could desync from rebuilt `networks[]`.
**Do:** Today there is no mid-capture rescan from UI — **document** that. If rescan is added, **stop capture** first and clear slots.
**Files:** `README.md` / future UI code.
---
## 8. **Magic numbers centralization** — *low (maintainability)*
**What:** Timing (`OBSERVE_DURATION_MS`, `CAPTURE_TIMEOUT_MS`, …) and `CHANNEL_TIMEOUT_MS` in the `.ino` are related but defined in two places; easy to desync.
**Do:** Move all timing `#define`s to one header (e.g. `HandshakeCapture.h` or `capture_config.h`) and include from the sketch.
**Files:** `HandshakeCapture.cpp`, `handshake-capture-c6.ino`.
---
### Already in decent shape
- Spinlock around promiscuous vs main-loop slot state; SD I/O only on main loop.
- `SD_IsReady()` vs flaky `cardType()` after WiFi; FAT filename sanitization; discard on failed save (no pending deadlock).
- PCAP fixed buffer; append failure doesnt increment EAPOL count.
---
*Add new items here as you close these; bump `HANDSHAKE_FIRMWARE_VERSION` when behavior changes.*

View File

@@ -4,15 +4,19 @@ Production-oriented WiFi 4-way handshake capture for Waveshare ESP32-C6-LCD-1.47
**Firmware version:** `HANDSHAKE_FIRMWARE_VERSION` in `HandshakeCapture.h` (also printed on serial at boot).
**Review backlog:** prioritized fixes from a full firmware pass → **`MUST_DO_IMPROVEMENTS.md`**.
## Features
- **SD required for capture:** No card, or open/write failure → pending PCAP is **discarded** and the slot freed (no infinite pending). Hot-insert: sketch retries `SD_Init()` before each capture round.
- **SD storage:** Saves **only complete 4-way handshakes** as `.pcap` (LINKTYPE_IEEE802_11): `4way_<ssid>_<ms>_s<n>.pcap`. Filenames are **FAT-sanitized**. No PMKID-only or partial EAPOL dumps.
- **Auto-deauth:** Bursts toward mapped clients to provoke a full 4-way.
- **Display:** **Green** = pending, **red** = 4-way saved to SD.
- **Display:** **Green** = targetable 2.4 GHz WPA/WPA2/WPA3 AP, **gray** = shown but skipped, **red** = 4-way saved to SD.
- **No web UI:** Capture-only device.
- **WPA / WPA2 / WPA3 PSK / mixed** (when the core reports them). Skips Open, WEP, WPA2-Enterprise.
- **2.4 GHz only capture sweep:** channels `1-14` are targeted. Secure APs seen on `5/6 GHz` stay visible in the list but are marked `5G+` and skipped.
- **BSSID cache:** Captured BSSIDs stay red across rescans.
- **Shared SPI protection:** LCD flushes and SD filesystem calls now serialize on one bus lock; SD writes also pause LVGL updates for the duration of the transaction.
**Convert:** `hcxpcapngtool` / Wireshark / aircrack-ng expect PCAP with EAPOL-Key frames; a full 4-way is included in each saved file (plus one target beacon when available).
@@ -20,7 +24,8 @@ Production-oriented WiFi 4-way handshake capture for Waveshare ESP32-C6-LCD-1.47
- **Not mergeable across random reconnects:** EAPOL messages from *different* 4-way runs (different nonces / MIC context) usually **cannot** be stitched into one crackable handshake. Tools need a **coherent** set for that AP↔STA association attempt.
- **Within one visit, frames can trickle in:** While tuned to a channel, the device keeps **one PCAP buffer per target AP**. If the client reconnects several times during the same session, **all EAPOL frames append** until either four are seen (then SD save) or the channel round ends without four (buffer dropped — no file).
- **Fewer than four frames:** Some attacks (e.g. **PMKID in message 1**, or classic **M1+M2** with beacon) need fewer frames, but this firmware **only saves on four** so every file is a clean full 4-way for your earlier requirement.
- **EAPOL counting is still heuristic:** The firmware saves after **four unencrypted EAPOL-shaped frames** for that AP buffer, then stops counting. It still does **not** validate distinct message numbers, replay counters, or rekeys.
- **Fewer than four frames:** Some attacks (e.g. **PMKID in message 1**, or classic **M1+M2** with beacon) need fewer frames, but this firmware **only saves on four** so every file aims to be a clean full 4-way for your earlier requirement.
## Hardware
@@ -49,13 +54,16 @@ arduino-cli upload \
Use `arduino-cli board list` to find the correct port.
For bench builds, set `HANDSHAKE_DEBUG 1` in `HandshakeCapture.h` to restore verbose serial logs. `USE_SOFTAP` defaults to `0`; only turn it on for lab experiments where you explicitly want `WIFI_AP_STA`.
## Flow
1. Boot → scan → display networks (green)
1. Boot → scan → display networks (`green` targetable, `gray` skipped)
2. Channel sweep + **up to 2 WPA APs at a time** on that channel; **map clients → deauth burst → listen** (several rounds per visit)
3. PCAP grows in RAM; **SD write only when 4 EAPOL-Key frames** are seen for that AP
4. On save → mark red; **refill slots** with other networks on the same channel, then **next channel** until everyone captured or timed out
5. If a visit ends without 4 EAPOL for an AP, that buffer is discarded (no partial PCAP); the next sweep can try again
6. Mid-capture rescans are intentionally not exposed in the UI; if you add one later, stop capture first and clear active slots before rebuilding `networks[]`
## Production checklist

View File

@@ -1,30 +1,80 @@
#include "SD_Card.h"
#include "Display_ST7789.h"
#include "HandshakeCapture.h"
#include "SPI_Bus_Lock.h"
uint16_t SDCard_Size = 0;
static bool g_sd_ready = false;
static bool s_sd_vfs_was_open = false;
static void beginSDOperation() {
SPIBus_SetDisplayPaused(true);
SPIBus_Lock();
}
static void endSDOperation() {
SPIBus_Unlock();
SPIBus_SetDisplayPaused(false);
}
bool SD_IsReady() {
return g_sd_ready;
}
bool SD_Init() {
beginSDOperation();
g_sd_ready = false;
pinMode(SD_CS, OUTPUT);
digitalWrite(SD_CS, HIGH);
// Shared SPI with ST7789: ensure display CS idle so SD sees a clean bus.
digitalWrite(EXAMPLE_PIN_NUM_LCD_CS, HIGH);
if (s_sd_vfs_was_open) {
SD.end();
s_sd_vfs_was_open = false;
HANDSHAKE_LOGLN("[SD] previous mount closed before re-begin");
}
HANDSHAKE_LOGF("[SD] begin(cs=%d)...\n", SD_CS);
if (!SD.begin(SD_CS, SPI)) {
Serial.println("SD init failed");
HANDSHAKE_LOGLN("[SD] FAIL: SD.begin() returned false");
endSDOperation();
return false;
}
uint8_t ct = (uint8_t)SD.cardType();
HANDSHAKE_LOGF("[SD] cardType=%u (0=NONE)\n", (unsigned)ct);
if (SD.cardType() == CARD_NONE) {
Serial.println("No SD card");
HANDSHAKE_LOGLN("[SD] FAIL: CARD_NONE after begin");
SD.end();
s_sd_vfs_was_open = false;
endSDOperation();
return false;
}
g_sd_ready = true;
s_sd_vfs_was_open = true;
SDCard_Size = SD.totalBytes() / (1024 * 1024);
HANDSHAKE_LOGF("[SD] OK size=%u MiB ready=%d\n", (unsigned)SDCard_Size, (int)g_sd_ready);
endSDOperation();
return true;
}
bool SD_WriteFileAtomic(const char* path, const uint8_t* data, size_t len) {
if (path == nullptr || data == nullptr || len == 0) return false;
beginSDOperation();
bool ok = false;
File file = SD.open(path, FILE_WRITE);
if (file) {
ok = (file.write(data, len) == len);
file.close();
}
if (!ok) {
g_sd_ready = false;
}
endSDOperation();
if (len >= 1024) yield();
return ok;
}

View File

@@ -15,3 +15,4 @@ bool SD_Init();
* with LCD+SD sharing SPI, cardType() can spuriously read CARD_NONE and falsely show "insert SD".
*/
bool SD_IsReady();
bool SD_WriteFileAtomic(const char* path, const uint8_t* data, size_t len);

View File

@@ -0,0 +1,37 @@
#include "SPI_Bus_Lock.h"
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
static SemaphoreHandle_t s_spi_bus_mutex = nullptr;
static volatile bool s_display_paused = false;
static void ensureMutex() {
if (s_spi_bus_mutex == nullptr) {
s_spi_bus_mutex = xSemaphoreCreateRecursiveMutex();
}
}
void SPIBus_Init() {
ensureMutex();
}
void SPIBus_Lock() {
ensureMutex();
if (s_spi_bus_mutex != nullptr) {
xSemaphoreTakeRecursive(s_spi_bus_mutex, portMAX_DELAY);
}
}
void SPIBus_Unlock() {
if (s_spi_bus_mutex != nullptr) {
xSemaphoreGiveRecursive(s_spi_bus_mutex);
}
}
void SPIBus_SetDisplayPaused(bool paused) {
s_display_paused = paused;
}
bool SPIBus_IsDisplayPaused() {
return s_display_paused;
}

View File

@@ -0,0 +1,9 @@
#pragma once
#include <Arduino.h>
void SPIBus_Init();
void SPIBus_Lock();
void SPIBus_Unlock();
void SPIBus_SetDisplayPaused(bool paused);
bool SPIBus_IsDisplayPaused();

View File

@@ -10,9 +10,17 @@ void WiFiScanner_Refresh(void) {
scanNetworksSortedByRSSI();
int count = 0;
int captureable = 0;
int offband_secure = 0;
for (int i = 0; i < MAX_NETWORKS; i++) {
if (networks[i].ssid.isEmpty()) break;
count++;
if (isCaptureable(i)) {
captureable++;
} else if (isSupportedCaptureAuth(networks[i].encryption) &&
!isSupportedCaptureChannel(networks[i].ch)) {
offband_secure++;
}
}
if (count <= 0) {
@@ -22,20 +30,33 @@ void WiFiScanner_Refresh(void) {
}
static char status_text[64];
snprintf(status_text, sizeof(status_text), "%d networks", count);
if (offband_secure > 0) {
snprintf(status_text, sizeof(status_text), "%d nets, %d tgt, %d off-band",
count, captureable, offband_secure);
} else {
snprintf(status_text, sizeof(status_text), "%d nets, %d tgt", count, captureable);
}
Ui_SetWifiStatus(status_text);
for (int i = 0; i < count; i++) {
char line[48];
snprintf(line, sizeof(line), "%02d. %s ch%d",
const char* suffix = "";
if (isSupportedCaptureAuth(networks[i].encryption) &&
!isSupportedCaptureChannel(networks[i].ch)) {
suffix = " 5G+";
}
snprintf(line, sizeof(line), "%02d. %s ch%d%s",
i + 1,
networks[i].ssid.c_str(),
networks[i].ch);
networks[i].ch,
suffix);
Ui_AddNetwork(line, i);
if (networks[i].handshake_captured) {
Ui_SetNetworkColor(i, lv_palette_main(LV_PALETTE_RED));
} else {
} else if (isCaptureable(i)) {
Ui_SetNetworkColor(i, lv_palette_main(LV_PALETTE_GREEN));
} else {
Ui_SetNetworkColor(i, lv_palette_main(LV_PALETTE_GREY));
}
}

View File

@@ -17,8 +17,6 @@ enum CaptureState {
CaptureState captureState = STATE_SCANNING;
unsigned long capture_start_time = 0;
// Hard cap per channel visit: should exceed MAX_OBSERVE_CYCLES * (observe + capture).
const unsigned long CHANNEL_TIMEOUT_MS = 150000UL;
// ─── Live capture visual update ───
@@ -119,14 +117,17 @@ void setup() {
Set_Backlight(100);
Lvgl_Init();
// WiFi + scan before SD: shared SPI with LCD; radio init can affect first SD.begin timing.
handshakeCaptureInit();
WiFiScanner_Refresh();
HANDSHAKE_LOGLN("[SETUP] mounting SD after WiFi scan");
if (!SD_Init()) {
Ui_SetWifiStatus("SD: no card");
} else {
Ui_SetWifiStatus("SD OK");
}
handshakeCaptureInit();
WiFiScanner_Refresh();
captureState = STATE_SCANNING;
}
@@ -147,8 +148,10 @@ void loop() {
break;
}
if (!SD_IsReady()) {
HANDSHAKE_LOGLN("[CAP] SD not ready — calling SD_Init before capture");
SD_Init();
if (!SD_IsReady()) {
HANDSHAKE_LOGLN("[CAP] BLOCKED: SD_IsReady still false — UI: Insert SD card");
Ui_SetWifiStatus("Insert SD card");
delay(800);
break;
@@ -186,7 +189,7 @@ void loop() {
} else if (!any_active) {
stopCapture();
captureState = STATE_SCANNING;
} else if (millis() - capture_start_time > CHANNEL_TIMEOUT_MS) {
} else if (millis() - capture_start_time > HANDSHAKE_CHANNEL_TIMEOUT_MS) {
Ui_SetWifiStatus("Ch timeout");
stopCapture();
captureState = STATE_SCANNING;