#include #include #include #include #include #include #include #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(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(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 } } }