Files
shikigami/src/main.cpp
T

770 lines
27 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#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
}
}
}