Compare commits

..
4 Commits
8 changed files with 1438 additions and 0 deletions

No files matched your search

+5
View File
@@ -0,0 +1,5 @@
.pio
.vscode/.browse.c_cpp.db*
.vscode/c_cpp_properties.json
.vscode/launch.json
.vscode/ipch
+10
View File
@@ -0,0 +1,10 @@
{
// See http://go.microsoft.com/fwlink/?LinkId=827846
// for the documentation about the extensions.json format
"recommendations": [
"platformio.platformio-ide"
],
"unwantedRecommendations": [
"ms-vscode.cpptools-extension-pack"
]
}
+114
View File
@@ -0,0 +1,114 @@
#!/usr/bin/env python3
"""
Generate a VLW font file compatible with LovyanGFX / M5GFX.
VLW format (all big-endian int32), verified against lgfx_fonts.cpp:
Header (6 x int32):
[0] glyph_count
[1] vlw_encoder_version (write 8, matches Processing default)
[2] font_size (points)
[3] reserved / discard
[4] ascent
[5] descent
Per glyph (7 x int32), at offset 24:
[0] codepoint
[1] height
[2] width
[3] xAdvance
[4] dY (y delta from baseline: ascent_px - bitmap_top)
[5] gdX (x delta from cursor: bitmap_left / lsb)
[6] reserved (0)
Bitmaps (at offset 24 + gCount*28):
width * height bytes each, 1 byte per pixel (0=off, 255=on)
"""
import struct
import math
import freetype
FONT_PATH = "/usr/share/fonts/Unifont/Unifont.ttf"
SIZE_PT = 24
DPI = 96
GLYPHS = "ʘ⌐■★()>_<Ox "
OUT = "unifont24.vlw"
OUT_H = "../src/unifont24_vlw.h"
face = freetype.Face(FONT_PATH)
face.set_char_size(SIZE_PT * 64, 0, DPI, 0)
metrics_face = face.size
ascent_px = math.ceil(metrics_face.ascender / 64)
descent_px = math.ceil(-metrics_face.descender / 64) # positive
glyphs_data = [] # (codepoint, height, width, xAdvance, dY, gdX, pixels)
for ch in GLYPHS:
cp = ord(ch)
# Grayscale render — gives 1 byte per pixel output
face.load_char(cp, freetype.FT_LOAD_RENDER)
slot = face.glyph
bitmap = slot.bitmap
w = bitmap.width
h = bitmap.rows
gdX = slot.bitmap_left # x offset from cursor
dY = slot.bitmap_top # y delta from baseline (above baseline = positive)
xAdv = math.ceil(slot.advance.x / 64)
# Grayscale pitch may be wider than width — trim to width bytes per row
# FreeType renders top-to-bottom in FT_PIXEL_MODE_GRAY — no reversal needed
pitch = bitmap.pitch
raw = bytes(bitmap.buffer)
pixels = bytearray()
for row in range(h):
pixels += raw[row * pitch: row * pitch + w]
glyphs_data.append((cp, h, w, xAdv, dY, gdX, bytes(pixels)))
print(f" U+{cp:04X} '{ch}' adv={xAdv} {w}x{h} gdX={gdX} dY={dY}")
# MUST be sorted ascending by codepoint — getUnicodeIndex uses lower_bound
glyphs_data.sort(key=lambda g: g[0])
with open(OUT, "wb") as f:
# Header — 6 int32s
f.write(struct.pack(">iiiiii",
len(glyphs_data), # [0] glyph count
8, # [1] vlw encoder version
SIZE_PT, # [2] font size in points
0, # [3] reserved
ascent_px, # [4] ascent
descent_px, # [5] descent
))
# Glyph records
for cp, h, w, xAdv, dY, gdX, _ in glyphs_data:
f.write(struct.pack(">iiiiiii", cp, h, w, xAdv, dY, gdX, 0))
# Bitmaps (1 byte per pixel)
for _, _, _, _, _, _, pixels in glyphs_data:
f.write(pixels)
print(f"\nWrote '{OUT}' ({len(glyphs_data)} glyphs, size={SIZE_PT}pt @ {DPI}dpi)")
print(f"Ascent={ascent_px} Descent={descent_px}")
# C header for embedding in firmware flash
vlw_bytes = open(OUT, "rb").read()
with open(OUT_H, "w") as h_file:
h_file.write("#pragma once\n")
h_file.write("#include <stdint.h>\n\n")
h_file.write(f"// Auto-generated by gen_vlw.py — {len(glyphs_data)} glyphs, {SIZE_PT}pt\n")
h_file.write(f"// Usage: M5.Display.loadFont(unifont24_vlw);\n\n")
h_file.write(f"static const uint8_t unifont24_vlw[] = {{\n")
for i, b in enumerate(vlw_bytes):
if i % 16 == 0:
h_file.write(" ")
h_file.write(f"0x{b:02X},")
if i % 16 == 15:
h_file.write("\n")
else:
h_file.write(" ")
h_file.write("\n};\n")
print(f"Wrote '{OUT_H}' ({len(vlw_bytes)} bytes)")
+42
View File
@@ -0,0 +1,42 @@
[env:m5stack-papers3]
platform = espressif32
board = esp32s3box
framework = arduino
src_dir = src
; Exclude the built-in Arduino WebServer framework library — it depends on
; WiFiServer.h which was moved in ESP32 Arduino 3.x. We use ESPAsyncWebServer
; instead, so the built-in WebServer is never needed.
lib_ignore = WebServer
; M5Stack PaperS3: ESP32-S3FN16R8 (16MB Flash, 8MB OPI PSRAM)
board_build.flash_size = 16MB
board_build.partitions = default_16MB.csv
board_build.arduino.memory_type = qio_opi
board_build.f_cpu = 240000000L
build_flags =
-DARDUINO_USB_MODE=1
-DARDUINO_USB_CDC_ON_BOOT=1
-DCORE_DEBUG_LEVEL=0
; NimBLE config - minimise memory footprint per CLAUDE.md
-DCONFIG_BT_NIMBLE_ROLE_BROADCASTER=0
-DCONFIG_BT_NIMBLE_ROLE_PERIPHERAL=0
-DCONFIG_BT_NIMBLE_MAX_CONNECTIONS=1
lib_deps =
; Display & Core System
m5stack/M5Unified
m5stack/M5GFX
; Bluetooth (low-memory NimBLE)
h2zero/NimBLE-Arduino
; GPS Parsing
mikalhart/TinyGPSPlus
; Web Server (ESP32-S3 compatible fork + async TCP)
mathieucarbou/ESPAsyncWebServer
mathieucarbou/AsyncTCP
; FTP server for management AP file access
xreef/SimpleFTPServer
monitor_speed = 115200
monitor_filters = esp32_exception_decoder
+770
View File
@@ -0,0 +1,770 @@
#include <M5Unified.h>
#include <cstdio>
#include <esp_sleep.h>
#include <esp_wifi.h>
#include <esp_netif.h>
#include <string.h>
#include <SimpleFTPServer.h>
#include "wifi_scanner.h"
#include "unifont24_vlw.h"
// ============================================================
// Face state enum — drives eye/mouth expressions
// ============================================================
enum FaceState {
FACE_IDLE, // (ʘ_ʘ) default, waiting
FACE_SCANNING, // (⌐■_■) active WiFi/BLE sniff
FACE_HANDSHAKE, // (★_★) valuable PCAP captured
FACE_ERROR, // (>_<) SD failure or system error
FACE_HTTP, // (O_O) hosting FTP server + management AP
FACE_SLEEP // (x_x) deep-sleep transition
};
// ============================================================
// System state
// ============================================================
bool scanActive = false;
bool bleActive = false;
bool gpsActive = false;
bool autowActive = false;
bool ftpActive = false;
bool int5Active = false;
static const uint8_t INT_INTERVALS[] = {5, 10, 20};
uint8_t intIntervalIdx = 0; // index into INT_INTERVALS
FaceState currentFace = FACE_IDLE;
char faceLabel[32] = "IDLE";
char faceCaption[32] = "";
char discoveredStr[64] = "";
uint8_t battPct = 0;
unsigned long runStart = 0;
bool runActive = false;
// ============================================================
// Management AP + FTP server
// ============================================================
static const char FTP_AP_SSID[] = "Shikigami";
static const char FTP_AP_PASS[] = "wardriving"; // WPA2 min-8
static const char FTP_USER[] = "admin";
static const char FTP_PASS[] = "shiki1337";
static const char FTP_AP_IP[] = "192.168.4.1";
static FtpServer ftpSrv;
// ============================================================
// Auto-wake cycle state — persisted across deep sleep via RTC RAM
// ============================================================
// Maximum BSSIDs tracked across sleep/wake cycles (~2400 bytes RTC RAM)
static const int AUTOWAKE_MAX_SEEN = 400;
RTC_DATA_ATTR static bool rAutoWakeActive = false;
RTC_DATA_ATTR static uint8_t rAutoWakeIntervalMins = 5;
RTC_DATA_ATTR static int rSeenCount = 0;
RTC_DATA_ATTR static uint8_t rSeenBSSIDs[AUTOWAKE_MAX_SEEN][6];
// Run timer — elapsed ms at time of last sleep, so it survives deep-sleep cycles.
RTC_DATA_ATTR static uint32_t rRunElapsedMs = 0;
RTC_DATA_ATTR static bool rRunWasActive = false;
static bool bssidIsSeen(const uint8_t* b) {
for (int i = 0; i < rSeenCount; i++) {
if (memcmp(rSeenBSSIDs[i], b, 6) == 0) return true;
}
return false;
}
static void bssidMarkSeen(const uint8_t* b) {
if (rSeenCount < AUTOWAKE_MAX_SEEN) {
memcpy(rSeenBSSIDs[rSeenCount++], b, 6);
}
}
// ============================================================
// Layout constants — 540 × 960 display
// ============================================================
// Header bar (full width)
static const int HDR_X = 0;
static const int HDR_Y = 5;
static const int HDR_W = 540;
static const int HDR_H = 45;
// Version string
static const int VER_X = 10;
static const int VER_Y = 65;
// Side indicators (GPS left / ACCEL right)
static const int IND_Y = 98;
// Face region bounding box — partial-refresh target
static const int FACE_BOX_X = 70;
static const int FACE_BOX_Y = 115;
static const int FACE_BOX_W = 400;
static const int FACE_BOX_H = 285;
static const int FACE_CX = 270; // horizontal centre of display
static const int FACE_CY = 245; // vertical centre of face bbox
// Labels above / below face
static const int FLAVOR_Y = 123;
static const int FLAVOR2_Y = 408;
// POWER OFF bar (full width)
static const int PWROFF_X = 0;
static const int PWROFF_Y = 425;
static const int PWROFF_W = 540;
static const int PWROFF_H = 50;
// Discovered device text
static const int DISC_Y = 502;
// Button grid
static const int BTN_W = 270;
static const int BTN_H = 80;
static const int COL1_X = 0;
static const int COL2_X = 270;
static const int ROW1_Y = 640;
static const int ROW2_Y = 720;
static const int ROW3_Y = 800;
static const int ROW4_Y = 880;
static const int FULLBTN_W = 540;
// ============================================================
// Forward declarations
// ============================================================
void drawFullUI();
void updateHeader();
void drawVersionLine();
void drawIndicators();
void updateFaceRegion();
void drawFaceGraphic();
void drawPowerOffBar();
void drawDiscoveredText();
void updateButtonGrid();
void drawButton(int x, int y, int w, int h, const char* label, bool isActive);
void handleTouch(int x, int y);
// ============================================================
// drawButton — inverted when active, outlined when inactive
// ============================================================
void drawButton(int x, int y, int w, int h, const char* label, bool isActive) {
if (isActive) {
M5.Display.fillRect(x, y, w, h, TFT_BLACK);
M5.Display.setTextColor(TFT_WHITE, TFT_BLACK);
} else {
M5.Display.fillRect(x, y, w, h, TFT_WHITE);
// 3-pixel thick border for inactive buttons
M5.Display.drawRect(x, y, w, h, TFT_BLACK);
M5.Display.drawRect(x+1, y+1, w - 2, h - 2, TFT_BLACK);
M5.Display.drawRect(x+2, y+2, w - 4, h - 4, TFT_BLACK);
M5.Display.setTextColor(TFT_BLACK, TFT_WHITE);
}
M5.Display.setTextDatum(middle_center);
M5.Display.setTextSize(2);
M5.Display.drawString(label, x + w / 2, y + h / 2);
}
// ============================================================
// Header bar — STAT / RUN timer / battery
// ============================================================
void updateHeader() {
M5.Display.fillRect(HDR_X, HDR_Y, HDR_W, HDR_H, TFT_WHITE);
// Battery — right-aligned, nub on LEFT facing the line
const int BW = 48, BH = 20, R = 3;
const int NUB_W = 5, NUB_H = 10;
const int BX = HDR_W - 6 - BW;
const int LINE_Y = HDR_Y + HDR_H - BH / 2 - 2; // leave 2px gap below battery
const int BY = LINE_Y - BH / 2; // battery centered on the line
const int NUB_X = BX - NUB_W;
const int NUB_Y = LINE_Y - NUB_H / 2; // centered on the line
// Nub (solid, left of body)
M5.Display.fillRect(NUB_X, NUB_Y, NUB_W, NUB_H, TFT_BLACK);
// Battery body
M5.Display.fillRoundRect(BX, BY, BW, BH, R, TFT_WHITE);
M5.Display.drawRoundRect(BX, BY, BW, BH, R, TFT_BLACK);
M5.Display.drawRoundRect(BX + 1, BY + 1, BW - 2, BH - 2, R, TFT_BLACK);
// Fill bar
const int fillW = (BW - 6) * battPct / 100;
if (fillW > 0)
M5.Display.fillRoundRect(BX + 3, BY + 3, fillW, BH - 6, 1, TFT_BLACK);
// Percent label
char batBuf[8];
snprintf(batBuf, sizeof(batBuf), "%d%%", battPct);
M5.Display.setTextDatum(middle_center);
M5.Display.setTextSize(1);
M5.Display.setTextColor(fillW > 0 && battPct > 50 ? TFT_WHITE : TFT_BLACK, TFT_WHITE);
M5.Display.drawString(batBuf, BX + BW / 2, BY + BH / 2);
// Bottom line — left edge to 4px before nub
M5.Display.drawFastHLine(HDR_X, LINE_Y, NUB_X - 4, TFT_BLACK);
M5.Display.drawFastHLine(HDR_X, LINE_Y - 1, NUB_X - 4, TFT_BLACK);
M5.Display.drawFastHLine(HDR_X, LINE_Y - 2, NUB_X - 4, TFT_BLACK);
M5.Display.setTextColor(TFT_BLACK, TFT_WHITE);
M5.Display.setTextSize(2);
// Left — status label reflects current operating state
const char* statStr;
if (currentFace == FACE_SLEEP) statStr = "STAT: SLEEP";
else if (ftpActive) statStr = "STAT: FTP";
else if (scanActive) statStr = "STAT: SCAN";
else statStr = "STAT: WAIT";
M5.Display.setTextDatum(middle_left);
M5.Display.drawString(statStr, HDR_X + 8, HDR_Y + HDR_H / 2);
// Right-aligned — run timer
char timeBuf[20];
if (runActive) {
unsigned long e = (millis() - runStart) / 1000;
snprintf(timeBuf, sizeof(timeBuf), "RUN: %02lu:%02lu:%02lu",
e / 3600, (e % 3600) / 60, e % 60);
} else {
snprintf(timeBuf, sizeof(timeBuf), "RUN: 00:00:00");
}
M5.Display.setTextDatum(middle_right);
M5.Display.drawString(timeBuf, NUB_X - 8, HDR_Y + HDR_H / 2);
}
// ============================================================
// Version string
// ============================================================
void drawVersionLine() {
M5.Display.fillRect(0, VER_Y - 12, 540, 24, TFT_WHITE);
M5.Display.setTextColor(TFT_BLACK, TFT_WHITE);
M5.Display.setTextDatum(middle_left);
M5.Display.setTextSize(1);
M5.Display.drawString("Shikigami v0.1-{git}", VER_X, VER_Y);
M5.Display.setTextSize(2);
}
// ============================================================
// Side indicators — GPS (left) / ACCEL (right)
// ============================================================
void drawIndicators() {
M5.Display.fillRect(0, IND_Y - 12, 540, 24, TFT_WHITE);
M5.Display.setTextColor(TFT_BLACK, TFT_WHITE);
M5.Display.setTextSize(1);
M5.Display.setTextDatum(middle_left);
M5.Display.drawString("GPS", 10, IND_Y);
M5.Display.setTextDatum(middle_right);
M5.Display.drawString("ACCEL", 530, IND_Y);
M5.Display.setTextSize(2);
}
// ============================================================
// Face graphic — drawn into FACE_BOX bounding box
// Eye geometry shared across all states; only eye/mouth style changes.
// ============================================================
void drawFaceGraphic() {
static const char* const FACE_STR[] = {
"(\xCA\x98_\xCA\x98)", // FACE_IDLE (ʘ_ʘ)
"(\xE2\x8C\x90\xE2\x96\xA0_\xE2\x96\xA0)", // FACE_SCANNING (⌐■_■)
"(\xE2\x98\x85_\xE2\x98\x85)", // FACE_HANDSHAKE (★_★)
"(>_<)", // FACE_ERROR
"(O_O)", // FACE_HTTP
"(x_x)", // FACE_SLEEP
};
M5.Display.loadFont(unifont24_vlw);
M5.Display.setTextSize(4);
M5.Display.setTextDatum(middle_center);
M5.Display.setTextColor(TFT_BLACK, TFT_WHITE);
M5.Display.drawString(FACE_STR[currentFace], FACE_CX, FACE_CY);
M5.Display.unloadFont();
}
// Clears the face bounding box and redraws labels + graphic.
// When FTP is active, replaces the face with connection info instead.
void updateFaceRegion() {
M5.Display.fillRect(FACE_BOX_X, FACE_BOX_Y, FACE_BOX_W, FACE_BOX_H, TFT_WHITE);
M5.Display.setTextColor(TFT_BLACK, TFT_WHITE);
M5.Display.setTextDatum(middle_center);
M5.Display.setTextSize(1);
M5.Display.drawString(faceLabel, FACE_CX, FLAVOR_Y);
if (ftpActive) {
// Replace face graphic with FTP/AP connection details
char line1[48], line2[48], line3[48];
snprintf(line1, sizeof(line1), "SSID: %s", FTP_AP_SSID);
snprintf(line2, sizeof(line2), "ftp://%s", FTP_AP_IP);
snprintf(line3, sizeof(line3), "%s / %s", FTP_USER, FTP_PASS);
const int infoSpacing = 45;
const int infoTop = FACE_CY - infoSpacing;
M5.Display.setTextSize(2);
M5.Display.drawString(line1, FACE_CX, infoTop);
M5.Display.drawString(line2, FACE_CX, infoTop + infoSpacing);
M5.Display.drawString(line3, FACE_CX, infoTop + infoSpacing * 2);
} else {
drawFaceGraphic();
}
M5.Display.setTextSize(1);
M5.Display.setTextColor(TFT_BLACK, TFT_WHITE);
M5.Display.setTextDatum(middle_center);
M5.Display.drawString(faceCaption, FACE_CX, FLAVOR2_Y);
M5.Display.setTextSize(2);
}
// ============================================================
// POWER OFF bar — full-width inverted
// ============================================================
void drawPowerOffBar() {
M5.Display.fillRect(PWROFF_X, PWROFF_Y, PWROFF_W, PWROFF_H, TFT_BLACK);
M5.Display.setTextColor(TFT_WHITE, TFT_BLACK);
M5.Display.setTextDatum(middle_center);
M5.Display.setTextSize(2);
M5.Display.drawString("POWER OFF", PWROFF_X + PWROFF_W / 2, PWROFF_Y + PWROFF_H / 2);
}
// ============================================================
// Discovered device / status text
// ============================================================
void drawDiscoveredText() {
M5.Display.fillRect(0, DISC_Y - 20, 540, 40, TFT_WHITE);
M5.Display.setTextColor(TFT_BLACK, TFT_WHITE);
M5.Display.setTextDatum(middle_center);
M5.Display.setTextSize(1);
M5.Display.drawString(discoveredStr, 270, DISC_Y);
M5.Display.setTextSize(2);
}
// ============================================================
// Button grid — 2×2 split + 2 full-width rows
// ============================================================
void updateButtonGrid() {
drawButton(COL1_X, ROW1_Y, BTN_W, BTN_H, "SCAN", scanActive);
drawButton(COL2_X, ROW1_Y, BTN_W, BTN_H, "BLE", bleActive);
drawButton(COL1_X, ROW2_Y, BTN_W, BTN_H, "GPS", gpsActive);
drawButton(COL2_X, ROW2_Y, BTN_W, BTN_H, "AUTOW", autowActive);
drawButton(COL1_X, ROW3_Y, FULLBTN_W, BTN_H, "FTP", ftpActive);
char intLabel[16];
if (int5Active) {
snprintf(intLabel, sizeof(intLabel), "INT %d", INT_INTERVALS[intIntervalIdx]);
} else {
snprintf(intLabel, sizeof(intLabel), "INT 5");
}
drawButton(COL1_X, ROW4_Y, FULLBTN_W, BTN_H, intLabel, int5Active);
}
// ============================================================
// Full-screen draw — used on boot and scheduled full refresh
// ============================================================
void drawFullUI() {
M5.Display.fillScreen(TFT_WHITE);
updateHeader();
drawVersionLine();
drawIndicators();
updateFaceRegion();
if (currentFace == FACE_SLEEP) {
drawPowerOffBar();
} else {
M5.Display.fillRect(PWROFF_X, PWROFF_Y, PWROFF_W, PWROFF_H, TFT_WHITE);
}
drawDiscoveredText();
updateButtonGrid();
}
// ============================================================
// Deep-sleep entry — draw sleep UI, flush, start timer, sleep
// ============================================================
static void enterDeepSleep(uint8_t minutes) {
wifiScanner_stop();
// Snapshot run-timer elapsed before millis() disappears in deep sleep
rRunWasActive = runActive;
rRunElapsedMs = runActive ? static_cast<uint32_t>(millis() - runStart) : rRunElapsedMs;
// Draw sleep state — switch to quality mode so white-on-black text renders
// correctly (epd_fast can't resolve the non-monotone transition in one pass)
currentFace = FACE_SLEEP;
strncpy(faceLabel, "SLEEPING", sizeof(faceLabel) - 1);
snprintf(faceCaption, sizeof(faceCaption), "Wake in %d min", minutes);
M5.Display.setEpdMode(epd_mode_t::epd_quality);
M5.Display.fillScreen(TFT_WHITE);
updateHeader();
updateFaceRegion();
drawPowerOffBar();
M5.Display.display(); // blocking full refresh before power-down
esp_sleep_enable_timer_wakeup((uint64_t)minutes * 60ULL * 1000000ULL);
esp_deep_sleep_start();
// never returns
}
// ============================================================
// Auto-wake scan cycle — runs in setup() on timer wakeup,
// never returns (re-enters deep sleep when done).
// ============================================================
static const unsigned long AUTOWAKE_SCAN_MS = 30000UL;
static void autoWakeScanCycle() {
int rawBatt = M5.Power.getBatteryLevel();
battPct = (uint8_t)(rawBatt > 0 ? rawBatt : 0);
wifiScanner_init();
wifiScanner_startTask();
wifiScanner_start();
// Show minimal scanning UI — e-ink retains POWER OFF bar from sleep screen,
// only partial-refresh the header and face region
currentFace = FACE_SCANNING;
strncpy(faceLabel, "AUTO-SCAN", sizeof(faceLabel) - 1);
strncpy(faceCaption, "Scanning...", sizeof(faceCaption) - 1);
updateHeader();
updateFaceRegion();
M5.Display.display();
// Scan for AUTOWAKE_SCAN_MS, draining packet queue to avoid overflow
const unsigned long scanEnd = millis() + AUTOWAKE_SCAN_MS;
while (millis() < scanEnd) {
WifiPacket pkt;
while (xQueueReceive(wifiPacketQueue, &pkt, 0) == pdTRUE) {
(void)pkt; // TODO: pass to pcapWriter_enqueue()
}
vTaskDelay(100 / portTICK_PERIOD_MS);
}
wifiScanner_stop();
// Count new APs not seen in previous cycles
const WifiAP* apList = nullptr;
int apCount = 0;
wifiScanner_getAPList(&apList, &apCount);
int newCount = 0;
if (apList) {
for (int i = 0; i < apCount; i++) {
if (!bssidIsSeen(apList[i].bssid)) {
bssidMarkSeen(apList[i].bssid);
newCount++;
}
}
}
// Show results briefly before sleeping again
if (newCount > 0) {
currentFace = FACE_HANDSHAKE;
snprintf(faceCaption, sizeof(faceCaption), "+%d new AP%s", newCount, newCount == 1 ? "" : "s");
} else {
currentFace = FACE_IDLE;
strncpy(faceCaption, "No new APs", sizeof(faceCaption) - 1);
}
snprintf(faceLabel, sizeof(faceLabel), "Seen: %d total", rSeenCount);
// Partial refresh only — POWER OFF bar already on screen from sleep render
updateFaceRegion();
M5.Display.display();
vTaskDelay(2500 / portTICK_PERIOD_MS);
enterDeepSleep(rAutoWakeIntervalMins);
// never returns
}
// ============================================================
// Management AP helpers
// ============================================================
// AP netif handle — created once, reused on subsequent enable/disable cycles.
static esp_netif_t* s_apNetif = nullptr;
// Start softAP using raw IDF so we stay consistent with the scanner's
// esp_wifi_* setup and avoid Arduino WiFi library reinitialising the stack.
static void startManagementAP() {
if (!s_apNetif) {
s_apNetif = esp_netif_create_default_wifi_ap();
}
// Switch to AP-only mode (scanner is stopped before this is called)
esp_wifi_set_mode(WIFI_MODE_AP);
wifi_config_t ap_cfg = {};
memcpy(ap_cfg.ap.ssid, FTP_AP_SSID, strlen(FTP_AP_SSID));
memcpy(ap_cfg.ap.password, FTP_AP_PASS, strlen(FTP_AP_PASS));
ap_cfg.ap.ssid_len = static_cast<uint8_t>(strlen(FTP_AP_SSID));
ap_cfg.ap.channel = 6;
ap_cfg.ap.max_connection = 2;
ap_cfg.ap.authmode = WIFI_AUTH_WPA2_PSK;
esp_wifi_set_config(WIFI_IF_AP, &ap_cfg);
// ftpSrv.begin(FTP_USER, FTP_PASS); // TODO: enable once SD card is mounted
}
static void stopManagementAP() {
// Return to STA mode so the scanner can re-enable promiscuous
esp_wifi_set_mode(WIFI_MODE_STA);
}
// ============================================================
// Run-timer helpers
// ============================================================
// Start the run timer the first time any capture feature is enabled;
// stop it when all capture features are disabled.
static void updateRunTimer() {
bool anyCapture = scanActive || bleActive;
if (anyCapture && !runActive) {
runActive = true;
runStart = millis();
} else if (!anyCapture) {
runActive = false;
}
}
// ============================================================
// Touch handling — each button does its own partial redraw
// ============================================================
void handleTouch(int tx, int ty) {
// Row 1 — SCAN / BLE
if (ty >= ROW1_Y && ty < ROW1_Y + BTN_H) {
if (tx >= COL1_X && tx < COL1_X + BTN_W) {
scanActive = !scanActive;
if (scanActive) {
wifiScanner_start();
currentFace = FACE_SCANNING;
strncpy(faceLabel, "SCANNING", sizeof(faceLabel) - 1);
snprintf(faceCaption, sizeof(faceCaption), "APs: %d", rSeenCount);
strncpy(discoveredStr, "Listening...", sizeof(discoveredStr) - 1);
} else {
wifiScanner_stop();
currentFace = FACE_IDLE;
strncpy(faceLabel, "IDLE", sizeof(faceLabel) - 1);
faceCaption[0] = '\0';
discoveredStr[0] = '\0';
}
updateRunTimer();
drawButton(COL1_X, ROW1_Y, BTN_W, BTN_H, "SCAN", scanActive);
updateHeader();
updateFaceRegion();
drawDiscoveredText();
} else if (tx >= COL2_X && tx < COL2_X + BTN_W) {
bleActive = !bleActive;
updateRunTimer();
drawButton(COL2_X, ROW1_Y, BTN_W, BTN_H, "BLE", bleActive);
updateHeader();
}
}
// Row 2 — GPS / AUTOW
else if (ty >= ROW2_Y && ty < ROW2_Y + BTN_H) {
if (tx >= COL1_X && tx < COL1_X + BTN_W) {
gpsActive = !gpsActive;
drawButton(COL1_X, ROW2_Y, BTN_W, BTN_H, "GPS", gpsActive);
} else if (tx >= COL2_X && tx < COL2_X + BTN_W) {
autowActive = !autowActive;
drawButton(COL2_X, ROW2_Y, BTN_W, BTN_H, "AUTOW", autowActive);
}
}
// Row 3 — FTP management AP (full width)
// Disables scanner while active; scanner is re-enabled on toggle-off.
else if (ty >= ROW3_Y && ty < ROW3_Y + BTN_H) {
if (tx >= COL1_X && tx < COL1_X + FULLBTN_W) {
ftpActive = !ftpActive;
if (ftpActive) {
// Stop scanning before switching WiFi mode
if (scanActive) {
wifiScanner_stop();
scanActive = false;
updateRunTimer();
drawButton(COL1_X, ROW1_Y, BTN_W, BTN_H, "SCAN", false);
}
startManagementAP();
currentFace = FACE_HTTP;
strncpy(faceLabel, "FTP SERVER", sizeof(faceLabel) - 1);
strncpy(faceCaption, FTP_AP_SSID, sizeof(faceCaption) - 1);
} else {
stopManagementAP();
currentFace = FACE_IDLE;
strncpy(faceLabel, "IDLE", sizeof(faceLabel) - 1);
faceCaption[0] = '\0';
}
drawButton(COL1_X, ROW3_Y, FULLBTN_W, BTN_H, "FTP", ftpActive);
updateFaceRegion();
}
}
// Row 4 — INT interval: first tap enables auto-wake and sleeps immediately.
// While active, subsequent taps cycle the interval and re-sleep.
// Final cycle (wraps back to 0) turns auto-wake off.
else if (ty >= ROW4_Y && ty < ROW4_Y + BTN_H) {
if (tx >= COL1_X && tx < COL1_X + FULLBTN_W) {
if (!int5Active) {
// Turn on — start at 5 min and enter sleep cycle
int5Active = true;
intIntervalIdx = 0;
rAutoWakeActive = true;
rAutoWakeIntervalMins = INT_INTERVALS[intIntervalIdx];
char intLabel[16];
snprintf(intLabel, sizeof(intLabel), "INT %d", rAutoWakeIntervalMins);
drawButton(COL1_X, ROW4_Y, FULLBTN_W, BTN_H, intLabel, true);
vTaskDelay(1200 / portTICK_PERIOD_MS);
enterDeepSleep(rAutoWakeIntervalMins); // never returns
} else {
// Cycle interval; wrapping to index 0 turns it off
intIntervalIdx = (intIntervalIdx + 1) % 3;
if (intIntervalIdx == 0) {
int5Active = false;
rAutoWakeActive = false;
drawButton(COL1_X, ROW4_Y, FULLBTN_W, BTN_H, "INT 5", false);
} else {
rAutoWakeIntervalMins = INT_INTERVALS[intIntervalIdx];
char intLabel[16];
snprintf(intLabel, sizeof(intLabel), "INT %d", rAutoWakeIntervalMins);
drawButton(COL1_X, ROW4_Y, FULLBTN_W, BTN_H, intLabel, true);
vTaskDelay(1200 / portTICK_PERIOD_MS);
enterDeepSleep(rAutoWakeIntervalMins); // never returns
}
}
}
}
// POWER OFF bar
else if (ty >= PWROFF_Y && ty < PWROFF_Y + PWROFF_H) {
// TODO: initiate graceful power-off sequence (flush SD, draw sleep face)
}
}
// ============================================================
// Setup
// ============================================================
void setup() {
auto cfg = M5.config();
M5.begin(cfg);
// Initial battery reading
int rawBatt = M5.Power.getBatteryLevel();
battPct = (uint8_t)(rawBatt > 0 ? rawBatt : 0);
M5.Display.setEpdMode(epd_mode_t::epd_fast);
M5.Display.setTextDatum(middle_center);
M5.Display.setTextSize(2);
// ---- Woke from timer: run auto-scan cycle and go back to sleep ----
if (esp_sleep_get_wakeup_cause() == ESP_SLEEP_WAKEUP_TIMER && rAutoWakeActive) {
autoWakeScanCycle(); // never returns
}
// ---- Normal boot (first power-on or manual reset) ----
// Default to INT 5 active: the device is a wardriving appliance —
// auto-wake is on from the start.
if (esp_sleep_get_wakeup_cause() == ESP_SLEEP_WAKEUP_UNDEFINED) {
rAutoWakeActive = true;
rAutoWakeIntervalMins = INT_INTERVALS[0]; // 5 min
intIntervalIdx = 0;
}
// Restore INT button UI state from RTC vars
if (rAutoWakeActive) {
int5Active = true;
for (uint8_t i = 0; i < 3; i++) {
if (INT_INTERVALS[i] == rAutoWakeIntervalMins) { intIntervalIdx = i; break; }
}
}
// Restore run timer — offset runStart so elapsed continues from where it paused
if (rRunWasActive) {
runActive = true;
runStart = millis() - rRunElapsedMs;
}
wifiScanner_init();
wifiScanner_startTask();
drawFullUI();
}
// ============================================================
// Loop
// ============================================================
void loop() {
M5.update();
// Drain WiFi packet queue on Core 1 — hand off to PCAP writer (future module).
{
WifiPacket pkt;
while (xQueueReceive(wifiPacketQueue, &pkt, 0) == pdTRUE) {
// TODO: pass pkt to pcapWriter_enqueue()
(void)pkt;
}
}
// Refresh scanner-driven UI fields every 2 s while scanning is active.
if (scanActive) {
static unsigned long lastScanUIUpdate = 0;
const unsigned long now = millis();
if (now - lastScanUIUpdate >= 2000UL) {
lastScanUIUpdate = now;
const WifiAP* apList = nullptr;
int apCount = 0;
wifiScanner_getAPList(&apList, &apCount);
// Absorb any new BSSIDs into RTC lifetime tally
if (apList) {
for (int i = 0; i < apCount; i++) {
if (!bssidIsSeen(apList[i].bssid)) {
bssidMarkSeen(apList[i].bssid);
}
}
}
// faceCaption — lifetime unique AP count across all sessions
snprintf(faceCaption, sizeof(faceCaption), "APs: %d", rSeenCount);
// discoveredStr — most recently seen AP (last entry in the list)
if (apList && apCount > 0) {
const WifiAP& newest = apList[apCount - 1];
const uint8_t* b = newest.bssid;
if (newest.ssid[0] != '\0') {
snprintf(discoveredStr, sizeof(discoveredStr),
"%.32s [%02X:%02X:%02X:%02X:%02X:%02X]",
newest.ssid, b[0], b[1], b[2], b[3], b[4], b[5]);
} else {
snprintf(discoveredStr, sizeof(discoveredStr),
"[%02X:%02X:%02X:%02X:%02X:%02X] (hidden)",
b[0], b[1], b[2], b[3], b[4], b[5]);
}
}
updateFaceRegion();
drawDiscoveredText();
}
}
auto t = M5.Touch.getDetail();
if (t.wasPressed()) {
handleTouch(t.x, t.y);
}
// Service FTP server on Core 1 when active.
if (ftpActive) {
ftpSrv.handleFTP();
}
// Header refresh — tick the run timer every second; poll battery every 30s.
{
static unsigned long lastHeaderTick = 0;
static uint8_t battTickCount = 0;
const unsigned long now = millis();
if (now - lastHeaderTick >= 1000UL) {
lastHeaderTick = now;
if (++battTickCount >= 30) {
battTickCount = 0;
int rawBatt = M5.Power.getBatteryLevel();
battPct = (uint8_t)(rawBatt > 0 ? rawBatt : 0);
}
updateHeader();
}
}
// Auto-sleep watchdog — if INT mode is active and we've been awake for the
// full interval, go to sleep now (kicks in whether or not the user is scanning).
if (int5Active && !ftpActive) {
const unsigned long intervalMs = (unsigned long)rAutoWakeIntervalMins * 60UL * 1000UL;
if (millis() >= intervalMs) {
enterDeepSleep(rAutoWakeIntervalMins); // never returns
}
}
}
+183
View File
@@ -0,0 +1,183 @@
#pragma once
#include <stdint.h>
// Auto-generated by gen_vlw.py — 12 glyphs, 24pt
// Usage: M5.Display.loadFont(unifont24_vlw);
static const uint8_t unifont24_vlw[] = {
0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x06,
0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x16, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x29, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x10,
0x00, 0x00, 0x00, 0x16, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3C,
0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x0A, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x12,
0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x12,
0x00, 0x00, 0x00, 0x0A, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x02,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4F, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x0C,
0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x5F, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x10,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x78,
0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x10,
0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x98, 0x00, 0x00, 0x00, 0x14,
0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x02,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x23, 0x10, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x0C,
0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x25, 0xA0, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x10,
0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x26, 0x05,
0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x1A,
0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00,
0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00,
0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00,
0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF,
0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00,
0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00,
0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00,
0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00,
0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00,
0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF,
0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00,
0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00,
0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00,
0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00,
0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF,
0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF,
0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF,
0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF,
0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF,
0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF,
0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00,
0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00,
0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF,
0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF,
0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF,
0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00,
0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF,
0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF,
0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF,
0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00,
0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF,
0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00,
};
+251
View File
@@ -0,0 +1,251 @@
#include "wifi_scanner.h"
#include <nvs_flash.h>
#include <esp_netif.h>
#include <esp_event.h>
#include <esp_wifi.h>
#include <freertos/semphr.h>
#include <freertos/task.h>
#include <string.h>
#include <Arduino.h> // millis()
// -------------------------------------------------------
// Configuration
// -------------------------------------------------------
#define QUEUE_DEPTH 32
#define CHANNEL_DWELL_MS 200 // ms to dwell on each channel before hopping
// -------------------------------------------------------
// Module-private state
// -------------------------------------------------------
QueueHandle_t wifiPacketQueue = nullptr;
static WifiAP s_apList[WIFI_MAX_APS];
static int s_apCount = 0;
static SemaphoreHandle_t s_apMutex = nullptr;
static TaskHandle_t s_taskHandle = nullptr;
static volatile bool s_running = false;
static bool s_initialized = false;
// -------------------------------------------------------
// Beacon parsing helpers
// -------------------------------------------------------
// Walk an IE block to determine the strongest security mode present.
static wifi_auth_mode_t parseAuthMode(const uint8_t* ies, int ies_len) {
bool has_rsn = false;
bool has_wpa = false;
int pos = 0;
while (pos + 2 <= ies_len) {
uint8_t tag = ies[pos];
uint8_t len = ies[pos + 1];
if (pos + 2 + len > ies_len) break;
const uint8_t* data = ies + pos + 2;
if (tag == 48) { // RSN → WPA2/WPA3
has_rsn = true;
} else if (tag == 221 && len >= 4) { // Vendor-specific
// WPA1: OUI 00:50:F2, type 0x01
if (data[0] == 0x00 && data[1] == 0x50 &&
data[2] == 0xF2 && data[3] == 0x01) {
has_wpa = true;
}
}
pos += 2 + len;
}
if (has_rsn) return WIFI_AUTH_WPA2_PSK;
if (has_wpa) return WIFI_AUTH_WPA_PSK;
return WIFI_AUTH_OPEN;
}
// Insert or update an entry in the AP list. Non-blocking: drops the update
// if the mutex is held by the reader to avoid stalling the sniffer.
static void updateAPList(const uint8_t* bssid, const char* ssid,
int8_t rssi, uint8_t channel, wifi_auth_mode_t auth) {
if (xSemaphoreTake(s_apMutex, 0) != pdTRUE) return;
for (int i = 0; i < s_apCount; i++) {
if (memcmp(s_apList[i].bssid, bssid, 6) == 0) {
s_apList[i].rssi = rssi;
s_apList[i].channel = channel;
xSemaphoreGive(s_apMutex);
return;
}
}
if (s_apCount < WIFI_MAX_APS) {
WifiAP& ap = s_apList[s_apCount++];
strncpy(ap.ssid, ssid, 32);
ap.ssid[32] = '\0';
memcpy(ap.bssid, bssid, 6);
ap.rssi = rssi;
ap.channel = channel;
ap.auth_mode = auth;
}
xSemaphoreGive(s_apMutex);
}
// -------------------------------------------------------
// Promiscuous receive callback — runs in WiFi task (Core 0)
// -------------------------------------------------------
static void promiscuousCallback(void* buf, wifi_promiscuous_pkt_type_t type) {
if (!s_running) return;
if (type != WIFI_PKT_MGMT && type != WIFI_PKT_DATA) return;
const wifi_promiscuous_pkt_t* pkt = static_cast<const wifi_promiscuous_pkt_t*>(buf);
const wifi_pkt_rx_ctrl_t& rx = pkt->rx_ctrl;
const uint8_t* frame = pkt->payload;
const int flen = rx.sig_len;
// ---- Parse beacon and probe-response frames for AP discovery ----
if (type == WIFI_PKT_MGMT && flen >= 36) {
// 802.11 frame control: little-endian 16-bit field at offset 0
const uint16_t fc = static_cast<uint16_t>(frame[0]) | (static_cast<uint16_t>(frame[1]) << 8);
const uint8_t ftype = (fc >> 2) & 0x03; // 0 = Management
const uint8_t fsubtype = (fc >> 4) & 0x0F; // 8 = Beacon, 5 = Probe Response
if (ftype == 0 && (fsubtype == 8 || fsubtype == 5)) {
// BSSID field is at offset 16 in the 802.11 management frame header
const uint8_t* bssid = frame + 16;
// Fixed-length fields: 24-byte MAC header + 8 ts + 2 interval + 2 cap = 36
// Information Elements start at offset 36
const uint8_t* ies = frame + 36;
const int ies_len = flen - 36;
char ssid[33] = {0};
uint8_t chan = static_cast<uint8_t>(rx.channel);
int pos = 0;
while (pos + 2 <= ies_len) {
const uint8_t tag = ies[pos];
const uint8_t len = ies[pos + 1];
if (pos + 2 + len > ies_len) break;
if (tag == 0) { // SSID IE
const int copy = len < 32 ? len : 32;
memcpy(ssid, ies + pos + 2, copy);
ssid[copy] = '\0';
} else if (tag == 3 && len == 1) { // DS Parameter Set (channel)
chan = ies[pos + 2];
}
pos += 2 + len;
}
const wifi_auth_mode_t auth = parseAuthMode(ies, ies_len);
updateAPList(bssid, ssid, static_cast<int8_t>(rx.rssi), chan, auth);
}
}
// ---- Enqueue raw packet for Core 1 → PCAP writer ----
WifiPacket wpkt;
wpkt.timestamp_ms = static_cast<uint32_t>(millis());
wpkt.channel = static_cast<uint8_t>(rx.channel);
wpkt.rssi = static_cast<int8_t>(rx.rssi);
wpkt.len = static_cast<uint16_t>(flen < WIFI_PKT_BUF_SIZE ? flen : WIFI_PKT_BUF_SIZE);
memcpy(wpkt.buf, frame, wpkt.len);
// Non-blocking send: drop the packet if the queue is full rather than
// stall the WiFi receive task and risk missing subsequent frames.
xQueueSend(wifiPacketQueue, &wpkt, 0);
}
// -------------------------------------------------------
// Channel-hop task — pinned to Core 0
// -------------------------------------------------------
static void wifiScannerTask(void*) {
for (;;) {
if (!s_running) {
// Block until wifiScanner_start() sends a task notification.
// Task notifications are "sticky": if the notify arrives before
// we block, ulTaskNotifyTake() returns immediately — no race.
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
continue;
}
for (uint8_t ch = 1; ch <= 13 && s_running; ch++) {
esp_wifi_set_channel(ch, WIFI_SECOND_CHAN_NONE);
vTaskDelay(pdMS_TO_TICKS(CHANNEL_DWELL_MS));
}
}
}
// -------------------------------------------------------
// Public API
// -------------------------------------------------------
void wifiScanner_init() {
if (s_initialized) return;
s_initialized = true;
// NVS may need erasing after a failed write or version mismatch.
esp_err_t ret = nvs_flash_init();
if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
nvs_flash_erase();
nvs_flash_init();
}
// The Arduino ESP32 core initialises netif and the event loop in
// initArduino(). Calling them again returns ESP_ERR_INVALID_STATE,
// which is non-fatal — we just continue.
esp_netif_init();
esp_event_loop_create_default();
esp_netif_create_default_wifi_sta();
wifi_init_config_t wificfg = WIFI_INIT_CONFIG_DEFAULT();
esp_wifi_init(&wificfg);
esp_wifi_set_mode(WIFI_MODE_STA);
esp_wifi_start();
wifiPacketQueue = xQueueCreate(QUEUE_DEPTH, sizeof(WifiPacket));
s_apMutex = xSemaphoreCreateMutex();
}
void wifiScanner_startTask() {
xTaskCreatePinnedToCore(
wifiScannerTask, // task function
"wifi_scan", // name
4096, // stack words
nullptr, // parameter
5, // priority
&s_taskHandle, // handle out
0 // Core 0
);
}
void wifiScanner_start() {
if (s_running) return;
s_running = true;
esp_wifi_set_promiscuous_rx_cb(promiscuousCallback);
esp_wifi_set_promiscuous(true);
if (s_taskHandle) {
xTaskNotifyGive(s_taskHandle);
}
}
void wifiScanner_stop() {
if (!s_running) return;
s_running = false;
esp_wifi_set_promiscuous(false);
// Channel-hop task checks s_running at each iteration and self-suspends
// via ulTaskNotifyTake() on the next loop pass.
}
bool wifiScanner_isRunning() {
return s_running;
}
void wifiScanner_getAPList(const WifiAP** list, int* count) {
if (xSemaphoreTake(s_apMutex, pdMS_TO_TICKS(10)) == pdTRUE) {
*list = s_apList;
*count = s_apCount;
xSemaphoreGive(s_apMutex);
} else {
*list = nullptr;
*count = 0;
}
}
+63
View File
@@ -0,0 +1,63 @@
#pragma once
#include <freertos/FreeRTOS.h>
#include <freertos/queue.h>
#include <esp_wifi_types.h>
#include <stdint.h>
// Maximum raw 802.11 frame payload buffered per packet
#define WIFI_PKT_BUF_SIZE 512
// Maximum tracked access points
#define WIFI_MAX_APS 64
// -------------------------------------------------------
// Packet pushed onto wifiPacketQueue (Core 0 → Core 1)
// Core 1 drains this queue and hands off to PCAP writer.
// -------------------------------------------------------
struct WifiPacket {
uint32_t timestamp_ms;
uint8_t channel;
int8_t rssi;
uint16_t len; // actual bytes copied (≤ WIFI_PKT_BUF_SIZE)
uint8_t buf[WIFI_PKT_BUF_SIZE];
};
// -------------------------------------------------------
// Access point discovered via beacon / probe-response
// -------------------------------------------------------
struct WifiAP {
char ssid[33]; // null-terminated, empty string for hidden APs
uint8_t bssid[6];
int8_t rssi;
uint8_t channel;
wifi_auth_mode_t auth_mode;
};
// -------------------------------------------------------
// Shared packet queue — extern so Core 1 can drain it
// -------------------------------------------------------
extern QueueHandle_t wifiPacketQueue;
// -------------------------------------------------------
// Public API
// -------------------------------------------------------
// Call once from setup() — initialises WiFi stack, creates queue and AP mutex.
void wifiScanner_init();
// Spawn the channel-hop FreeRTOS task pinned to Core 0.
// Call after wifiScanner_init(), before the first button press.
void wifiScanner_startTask();
// Enable promiscuous mode and resume the channel-hop task.
void wifiScanner_start();
// Disable promiscuous mode; channel-hop task suspends itself.
void wifiScanner_stop();
bool wifiScanner_isRunning();
// Obtain a read-only snapshot of the current AP list.
// Returns nullptr/0 if the mutex is unavailable.
void wifiScanner_getAPList(const WifiAP** list, int* count);