Implement main application logic and UI for Shikigami device
This commit is contained in:
1 parent
10e2f921ed
commit
1e9f7ea47f
1 file changed
+770
+770
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user