/* * WiFiSense ESP32-S3 — OLED + WiFiManager Edition * ───────────────────────────────────────────────── * OLED : 0.96" SSD1306 128×64 I2C SDA→GPIO6 SCL→GPIO7 * LED : GPIO 2 * * Thư viện cần cài qua Library Manager: * • Adafruit SSD1306 (by Adafruit) * • Adafruit GFX Library (by Adafruit) * • Preferences (built-in ESP32 core) * • DNSServer (built-in ESP32 core) * • WebServer (built-in ESP32 core) * * Luồng WiFi: * 1. Khởi động → đọc credentials từ NVS * 2. Thử kết nối (30 s timeout) * 3. Nếu thất bại → mở AP "WiFiSense-Setup" (không mật khẩu) * OLED hiển thị IP captive portal (192.168.4.1) * 4. Người dùng kết nối AP, mở trình duyệt → trang cấu hình: * quét danh sách SSID, chọn mạng, nhập mật khẩu, Submit * 5. Credentials được lưu NVS → ESP32 restart → kết nối thành công * 6. Trong loop(): nếu mất kết nối → tự reconnect, sau 3 lần thất bại * mở lại captive portal. */ // ═══════════════════════════════════════════════════════════════════════════════ // Includes // ═══════════════════════════════════════════════════════════════════════════════ #include #include #include #include #include #include #include // ═══════════════════════════════════════════════════════════════════════════════ // Hằng số cấu hình // ═══════════════════════════════════════════════════════════════════════════════ // ─── OLED ──────────────────────────────────────────────────────────────────── #define OLED_WIDTH 128 #define OLED_HEIGHT 64 #define OLED_RESET -1 #define OLED_ADDRESS 0x3C #define OLED_SDA_PIN 6 #define OLED_SCL_PIN 7 // ─── LED ──────────────────────────────────────────────────────────────────── constexpr int LED_PIN = 2; // ─── WiFiManager AP ────────────────────────────────────────────────────────── #define WIFI_MANAGER_SSID "WiFiSense-Setup" #define WIFI_MANAGER_PASS "" // để trống = không mật khẩu #define DNS_PORT 53 #define HTTP_PORT 80 #define AP_IP_STR "192.168.4.1" constexpr uint32_t WIFI_CONNECT_TIMEOUT_MS = 30'000; constexpr uint8_t MAX_RECONNECT_ATTEMPTS = 3; // ─── Cảm biến ──────────────────────────────────────────────────────────────── constexpr size_t WINDOW_SIZE = 40; constexpr size_t LONG_WINDOW = 100; constexpr uint16_t SAMPLE_INTERVAL_MS = 500; constexpr uint16_t CALIBRATION_SAMPLES = 200; constexpr float PROCESS_NOISE = 0.15f; constexpr float MEASUREMENT_NOISE = 0.3f; constexpr float SMOOTH_ALPHA = 0.15f; constexpr float ADAPTIVE_ALPHA = 0.008f; constexpr float ADAPTIVE_BETA = 0.005f; constexpr float SLOW_MOVEMENT_THRESHOLD = 1.8f; constexpr float FAST_MOVEMENT_THRESHOLD = 3.5f; constexpr float Z_SCORE_THRESHOLD = 2.5f; constexpr float PEAK_THRESHOLD = 2.0f; constexpr uint8_t PERSISTENCE_REQUIRED = 3; // ═══════════════════════════════════════════════════════════════════════════════ // Đối tượng toàn cục // ═══════════════════════════════════════════════════════════════════════════════ Adafruit_SSD1306 display(OLED_WIDTH, OLED_HEIGHT, &Wire, OLED_RESET); Preferences prefs; WebServer webServer(HTTP_PORT); DNSServer dnsServer; // ─── Biến sensing ──────────────────────────────────────────────────────────── float rssiWindow[WINDOW_SIZE]; float rssiLongWindow[LONG_WINDOW]; size_t windowIndex = 0; size_t longWindowIndex = 0; size_t sampleCount = 0; float kalmanEstimate = -55.0f; float kalmanError = 1.0f; float smoothedRssi = -55.0f; float baseline = -55.0f; float baselineVariance = 1.0f; uint8_t disturbanceCounter = 0; uint32_t totalDetections = 0; // ─── Biến WiFiManager ──────────────────────────────────────────────────────── bool apMode = false; uint8_t reconnectAttempts = 0; String savedSsid; String savedPass; // ─── Log OLED cuộn ─────────────────────────────────────────────────────────── #define LOG_LINES 8 char logBuf[LOG_LINES][22]; uint8_t logHead = 0; // ═══════════════════════════════════════════════════════════════════════════════ // Tiện ích chung // ═══════════════════════════════════════════════════════════════════════════════ float clampValue(float v, float lo, float hi) { return v < lo ? lo : (v > hi ? hi : v); } size_t latestWindowIndex(size_t offset) { return (windowIndex + WINDOW_SIZE - offset) % WINDOW_SIZE; } void setDetectionLed(bool on) { digitalWrite(LED_PIN, on ? HIGH : LOW); } // ═══════════════════════════════════════════════════════════════════════════════ // OLED helpers // ═══════════════════════════════════════════════════════════════════════════════ // ── Log cuộn (dùng khi connect / calibrate / AP mode) ──────────────────────── void logPush(const char* line) { strncpy(logBuf[logHead], line, 21); logBuf[logHead][21] = '\0'; logHead = (logHead + 1) % LOG_LINES; } void logRender() { display.clearDisplay(); display.setTextSize(1); display.setTextColor(WHITE); for (int i = 0; i < LOG_LINES; ++i) { display.setCursor(0, i * 8); display.print(logBuf[(logHead + i) % LOG_LINES]); } display.display(); } // ── Splash ──────────────────────────────────────────────────────────────────── void oledSplash() { display.clearDisplay(); display.setTextSize(1); display.setTextColor(WHITE); display.setCursor(16, 8); display.print("WiFiSense v3"); display.setCursor(10, 20); display.print("ESP32-S3 + OLED"); display.setCursor(22, 32); display.print("WiFiManager"); display.setCursor(22, 44); display.print("orangepi.vn"); display.display(); delay(1500); } // ── Màn hình AP (Captive Portal) ───────────────────────────────────────────── /* * ┌──────────────────────────────┐ * │ ** SETUP MODE ** │ * │ Connect to WiFi: │ * │ WiFiSense-Setup │ * │ Open browser: │ * │ 192.168.4.1 │ * └──────────────────────────────┘ */ void oledShowApMode() { display.clearDisplay(); display.setTextSize(1); display.setTextColor(WHITE); display.setCursor(14, 0); display.print("** SETUP MODE **"); display.drawFastHLine(0, 10, 128, WHITE); display.setCursor(0, 14); display.print("1. Ket noi WiFi:"); display.setCursor(4, 24); display.print(WIFI_MANAGER_SSID); display.setCursor(0, 36); display.print("2. Mo trinh duyet:"); display.setCursor(4, 46); display.print(AP_IP_STR); // Nhấp nháy dấu chấm cuối để cho thấy đang chờ static bool blink = false; blink = !blink; if (blink) { display.setCursor(120, 56); display.print("."); } display.display(); } // ── Thanh ngang ────────────────────────────────────────────────────────────── void drawBar(int x, int y, int maxW, int h, int val) { display.drawRect(x, y, maxW, h, WHITE); int fill = (int)((float)val / 100.0f * (float)(maxW - 2)); if (fill > 0) display.fillRect(x + 1, y + 1, fill, h - 2, WHITE); } // ── Trang chính (duy nhất) ──────────────────────────────────────────────────── /* * ┌──────────────────────────────┐ y=0 * │ WiFiSense [ MOTION ] │ tiêu đề + badge * ├──────────────────────────────┤ y=9 * │ RSSI: -67 dBm Conf: 82% │ y=11 * │ Conf [████████████░░░░░░░░] │ y=22 * │ Qual [█████████░░░░░░░░░░░] │ y=32 * ├──────────────────────────────┤ * │ Base:-66.1 Var:2.3 │ y=43 * │ Detect:#15 WALKING │ y=54 * └──────────────────────────────┘ */ void drawPageMain(int rawRssi, int confidence, int quality, float variance, float rateOfChange, bool detected) { display.clearDisplay(); display.setTextSize(1); display.setTextColor(WHITE); // ── Dòng 1: tiêu đề + badge trạng thái ────────────────────────────────── display.setCursor(0, 0); display.print("WiFiSense"); const char* badge = detected ? " MOTION " : " CLEAR "; int badgeLen = (int)strlen(badge); int bx = 128 - badgeLen * 6; if (detected) { display.fillRect(bx - 1, 0, 128 - bx + 1, 8, WHITE); display.setTextColor(BLACK); } display.setCursor(bx, 0); display.print(badge); display.setTextColor(WHITE); // ── Đường kẻ ngang ─────────────────────────────────────────────────────── display.drawFastHLine(0, 9, 128, WHITE); // ── Dòng 2: RSSI + Confidence số ───────────────────────────────────────── display.setCursor(0, 11); display.printf("RSSI:%4ddBm", rawRssi); display.setCursor(78, 11); display.printf("C:%3d%%", confidence); // ── Thanh Confidence ───────────────────────────────────────────────────── display.setCursor(0, 22); display.print("Conf"); drawBar(26, 22, 102, 7, confidence); // ── Thanh Quality ──────────────────────────────────────────────────────── display.setCursor(0, 32); display.print("Qual"); drawBar(26, 32, 102, 7, quality); // ── Đường kẻ ngang ─────────────────────────────────────────────────────── display.drawFastHLine(0, 41, 128, WHITE); // ── Dòng 4: Baseline + Variance ────────────────────────────────────────── display.setCursor(0, 43); display.printf("Base:%.1f Var:%.1f", baseline, variance); // ── Dòng 5: Số lần phát hiện + cường độ chuyển động ───────────────────── display.setCursor(0, 54); display.printf("#%lu", (unsigned long)totalDetections); // Nhãn cường độ bên phải const char* intensity = "CALM"; if (variance > FAST_MOVEMENT_THRESHOLD && rateOfChange > 2.5f) intensity = "SPRINT"; else if (variance > FAST_MOVEMENT_THRESHOLD) intensity = "FAST"; else if (variance > SLOW_MOVEMENT_THRESHOLD && rateOfChange > 1.0f) intensity = "WALKING"; else if (variance > SLOW_MOVEMENT_THRESHOLD) intensity = "SLOW"; int lx = 128 - (int)strlen(intensity) * 6; display.setCursor(lx, 54); display.print(intensity); display.display(); } // ═══════════════════════════════════════════════════════════════════════════════ // Thuật toán WiFi sensing // ═══════════════════════════════════════════════════════════════════════════════ float kalmanFilter(float m) { const float pe = kalmanError + PROCESS_NOISE; const float gain = pe / (pe + MEASUREMENT_NOISE); kalmanEstimate += gain * (m - kalmanEstimate); kalmanError = (1.0f - gain) * pe; return kalmanEstimate; } float exponentialSmoothing(float m) { smoothedRssi = SMOOTH_ALPHA * m + (1.0f - SMOOTH_ALPHA) * smoothedRssi; return smoothedRssi; } float standardDeviation(const float* v, size_t n) { if (n == 0) return 0.0f; float mean = 0.0f; for (size_t i = 0; i < n; ++i) mean += v[i]; mean /= (float)n; float var = 0.0f; for (size_t i = 0; i < n; ++i) { float d = v[i] - mean; var += d * d; } return sqrtf(var / (float)n); } float analyzeWindowVariance() { return standardDeviation(rssiWindow, WINDOW_SIZE); } float analyzeLongTermVariance() { return standardDeviation(rssiLongWindow, LONG_WINDOW); } float detectRateOfChange() { if (sampleCount < 10) return 0.0f; float r = 0.0f, o = 0.0f; for (size_t i = 1; i <= 5; ++i) { r += rssiWindow[latestWindowIndex(i)]; o += rssiWindow[latestWindowIndex(i + 5)]; } return fabsf((r - o) / 5.0f); } float detectPeak() { if (sampleCount < 3) return 0.0f; float cur = rssiWindow[latestWindowIndex(1)]; float prv = rssiWindow[latestWindowIndex(2)]; float old = rssiWindow[latestWindowIndex(3)]; float cd = fabsf(cur - prv), pd = fabsf(prv - old); return cd > pd + 1.0f ? cd : 0.0f; } float calculateZScore() { if (sampleCount < WINDOW_SIZE) return 0.0f; float mean = 0.0f; for (size_t i = 0; i < WINDOW_SIZE; ++i) mean += rssiWindow[i]; mean /= (float)WINDOW_SIZE; float sd = fmaxf(analyzeWindowVariance(), 0.1f); return fabsf((rssiWindow[latestWindowIndex(1)] - mean) / sd); } int calculateConfidence(float var, float roc, float peak, float z) { float c = var > FAST_MOVEMENT_THRESHOLD ? 75.0f : var > SLOW_MOVEMENT_THRESHOLD ? 50.0f : 15.0f; if (roc > 2.0f) c += 15.0f; if (peak > PEAK_THRESHOLD) c += 10.0f; if (z > Z_SCORE_THRESHOLD) c += 10.0f; return (int)clampValue(c, 0.0f, 100.0f); } void initializeRssiWindows(float v) { for (int i = 0; i < (int)WINDOW_SIZE; ++i) rssiWindow[i] = v; for (int i = 0; i < (int)LONG_WINDOW; ++i) rssiLongWindow[i] = v; windowIndex = longWindowIndex = 0; sampleCount = WINDOW_SIZE; } // ═══════════════════════════════════════════════════════════════════════════════ // NVS helpers // ═══════════════════════════════════════════════════════════════════════════════ void loadCredentials() { prefs.begin("wifisense", true); // read-only savedSsid = prefs.getString("ssid", ""); savedPass = prefs.getString("pass", ""); prefs.end(); Serial.printf("[NVS] ssid='%s'\n", savedSsid.c_str()); } void saveCredentials(const String& ssid, const String& pass) { prefs.begin("wifisense", false); prefs.putString("ssid", ssid); prefs.putString("pass", pass); prefs.end(); Serial.printf("[NVS] Saved ssid='%s'\n", ssid.c_str()); } void clearCredentials() { prefs.begin("wifisense", false); prefs.clear(); prefs.end(); } // ═══════════════════════════════════════════════════════════════════════════════ // Captive Portal (WiFiManager tự cài) // ═══════════════════════════════════════════════════════════════════════════════ // ── HTML trang cấu hình ────────────────────────────────────────────────────── // Được sinh động: quét SSID rồi điền vào )rawhtml"; for (int i = 0; i < networkCount; ++i) { int rssi = WiFi.RSSI(i); String bar = rssi > -60 ? "▊▊▊" : rssi > -75 ? "▊▊░" : "▊░░"; html += "\n"; } html += R"rawhtml(

ESP32 sẽ khởi động lại sau khi lưu

)rawhtml"; return html; } static String buildSavedHtml(const String& ssid) { return String("" "" "Đã lưu" "" "
" "

✓ Đã lưu!

" "

Đang kết nối tới " + ssid + "

" "

ESP32 sẽ khởi động lại trong vài giây...

" "
"); } // ── Khởi động AP + DNS + WebServer ─────────────────────────────────────────── void startCaptivePortal() { apMode = true; WiFi.mode(WIFI_AP); WiFi.softAP(WIFI_MANAGER_SSID, WIFI_MANAGER_PASS[0] ? WIFI_MANAGER_PASS : nullptr); IPAddress apIP(192, 168, 4, 1); // DNS: mọi domain trỏ về AP IP → trình duyệt tự mở captive portal dnsServer.start(DNS_PORT, "*", apIP); // Quét WiFi trước khi khởi webserver logPush("Scanning WiFi..."); logRender(); int n = WiFi.scanNetworks(); char tmp[22]; snprintf(tmp, sizeof(tmp), "Found %d networks", n); logPush(tmp); logPush("AP: " WIFI_MANAGER_SSID); logPush("IP: " AP_IP_STR); logRender(); // Route: GET / → trang chọn WiFi webServer.on("/", HTTP_GET, [n]() { webServer.send(200, "text/html", buildHtml(n)); }); // Route: POST /save → lưu NVS, restart webServer.on("/save", HTTP_POST, []() { String newSsid = webServer.arg("ssid"); String newPass = webServer.arg("pass"); if (newSsid.isEmpty()) { webServer.send(400, "text/plain", "SSID khong duoc de trong"); return; } saveCredentials(newSsid, newPass); webServer.send(200, "text/html", buildSavedHtml(newSsid)); logPush("Saved! Restarting..."); logRender(); delay(2000); ESP.restart(); }); // Captive portal redirect: mọi URL khác → trang chính webServer.onNotFound([]() { webServer.sendHeader("Location", "http://" AP_IP_STR, true); webServer.send(302, "text/plain", ""); }); webServer.begin(); Serial.printf("[AP] SSID=%s IP=%s\n", WIFI_MANAGER_SSID, AP_IP_STR); } // ── Dừng AP ─────────────────────────────────────────────────────────────────── void stopCaptivePortal() { webServer.stop(); dnsServer.stop(); WiFi.softAPdisconnect(true); apMode = false; } // ═══════════════════════════════════════════════════════════════════════════════ // Kết nối WiFi station // ═══════════════════════════════════════════════════════════════════════════════ bool connectToWifi(const String& ssid, const String& pass) { if (ssid.isEmpty()) return false; WiFi.mode(WIFI_STA); WiFi.begin(ssid.c_str(), pass.isEmpty() ? nullptr : pass.c_str()); char tmp[22]; snprintf(tmp, sizeof(tmp), "Connecting..."); logPush(tmp); logRender(); Serial.printf("[WiFi] Connecting to '%s'\n", ssid.c_str()); const uint32_t t0 = millis(); while (WiFi.status() != WL_CONNECTED) { if (millis() - t0 >= WIFI_CONNECT_TIMEOUT_MS) { logPush("Timeout!"); logRender(); Serial.println("[WiFi] Timeout."); return false; } delay(500); if ((millis() - t0) % 5000 < 500) { snprintf(tmp, sizeof(tmp), "Wait %lus", (millis() - t0) / 1000UL); logPush(tmp); logRender(); } } snprintf(tmp, sizeof(tmp), "IP:%s", WiFi.localIP().toString().c_str()); logPush(tmp); snprintf(tmp, sizeof(tmp), "RSSI:%d dBm", WiFi.RSSI()); logPush(tmp); logRender(); Serial.printf("[WiFi] OK. IP=%s\n", WiFi.localIP().toString().c_str()); delay(600); return true; } // ═══════════════════════════════════════════════════════════════════════════════ // Calibrate // ═══════════════════════════════════════════════════════════════════════════════ void calibrate() { char tmp[22]; snprintf(tmp, sizeof(tmp), "Cal %u samples", CALIBRATION_SAMPLES); logPush(tmp); logPush("Keep room EMPTY!"); logRender(); float mean = 0.0f; for (uint16_t i = 0; i < CALIBRATION_SAMPLES; ++i) { mean += (float)WiFi.RSSI(); delay(30); if (i % 40 == 0) { snprintf(tmp, sizeof(tmp), "Cal %d/%d", i, CALIBRATION_SAMPLES); logPush(tmp); logRender(); } } baseline = mean / (float)CALIBRATION_SAMPLES; kalmanEstimate = baseline; smoothedRssi = baseline; initializeRssiWindows(baseline); snprintf(tmp, sizeof(tmp), "Base:%.2f dBm", baseline); logPush(tmp); logPush("Done! Starting..."); logRender(); delay(1000); Serial.printf("[Cal] Baseline=%.2f dBm\n", baseline); } // ═══════════════════════════════════════════════════════════════════════════════ // setup() // ═══════════════════════════════════════════════════════════════════════════════ void setup() { Serial.begin(115200); pinMode(LED_PIN, OUTPUT); setDetectionLed(false); // OLED Wire.begin(OLED_SDA_PIN, OLED_SCL_PIN); if (!display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDRESS)) { Serial.println("[OLED] Init failed — check wiring!"); } display.cp437(true); display.setTextWrap(false); oledSplash(); Serial.println("\n===== WiFiSense ESP32-S3 v3 ====="); // Đọc credentials từ NVS loadCredentials(); // Thử kết nối với credentials đã lưu bool connected = connectToWifi(savedSsid, savedPass); if (!connected) { // Mở captive portal startCaptivePortal(); // Loop trong AP mode — chờ người dùng cấu hình while (apMode) { dnsServer.processNextRequest(); webServer.handleClient(); oledShowApMode(); delay(300); } // Sau khi /save gọi ESP.restart() → không bao giờ thoát vòng while ở đây } stopCaptivePortal(); // đảm bảo AP tắt nếu không cần calibrate(); Serial.println("[Main] Sensing started."); } // ═══════════════════════════════════════════════════════════════════════════════ // loop() // ═══════════════════════════════════════════════════════════════════════════════ void loop() { // ── Reconnect nếu mất kết nối ────────────────────────────────────────── if (WiFi.status() != WL_CONNECTED) { setDetectionLed(false); reconnectAttempts++; logPush("WiFi lost!"); char tmp[22]; snprintf(tmp, sizeof(tmp), "Retry %d/%d", reconnectAttempts, MAX_RECONNECT_ATTEMPTS); logPush(tmp); logRender(); bool ok = connectToWifi(savedSsid, savedPass); if (ok) { reconnectAttempts = 0; calibrate(); // tái calibrate sau khi reconnect } else if (reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) { // Thất bại nhiều lần → mở portal để người dùng đổi mạng logPush("Open portal..."); logRender(); delay(1000); clearCredentials(); ESP.restart(); // restart sẽ vào setup() → portal } return; } reconnectAttempts = 0; // ── Lấy mẫu ───────────────────────────────────────────────────────────── const int rawRssi = WiFi.RSSI(); const float kalmanFiltered = kalmanFilter((float)rawRssi); const float smoothFiltered = exponentialSmoothing(kalmanFiltered); rssiWindow[windowIndex] = smoothFiltered; rssiLongWindow[longWindowIndex] = smoothFiltered; windowIndex = (windowIndex + 1) % WINDOW_SIZE; longWindowIndex = (longWindowIndex + 1) % LONG_WINDOW; ++sampleCount; // ── Tính toán ──────────────────────────────────────────────────────────── const float variance = analyzeWindowVariance(); const float longVariance = analyzeLongTermVariance(); const float rateOfChange = detectRateOfChange(); const float peak = detectPeak(); const float zScore = calculateZScore(); const int confidence = calculateConfidence(variance, rateOfChange, peak, zScore); const float signalQuality = clampValue(100.0f - longVariance * 20.0f, 0.0f, 100.0f); baseline = baseline * (1.0f - ADAPTIVE_ALPHA) + smoothFiltered * ADAPTIVE_ALPHA; baselineVariance = baselineVariance * (1.0f - ADAPTIVE_BETA) + variance * ADAPTIVE_BETA; const bool motion = variance > SLOW_MOVEMENT_THRESHOLD || (zScore > Z_SCORE_THRESHOLD && peak > PEAK_THRESHOLD); disturbanceCounter = motion ? disturbanceCounter + 1 : 0; const bool detected = disturbanceCounter >= PERSISTENCE_REQUIRED; if (detected && disturbanceCounter == PERSISTENCE_REQUIRED) ++totalDetections; setDetectionLed(detected); // ── Hiển thị ───────────────────────────────────────────────────────────── drawPageMain(rawRssi, confidence, (int)signalQuality, variance, rateOfChange, detected); // ── Serial debug ───────────────────────────────────────────────────────── Serial.printf("Raw:%d K:%.2f S:%.2f Base:%.2f Var:%.2f Z:%.2f Conf:%d Det:%lu\n", rawRssi, kalmanFiltered, smoothFiltered, baseline, variance, zScore, confidence, (unsigned long)totalDetections); delay(SAMPLE_INTERVAL_MS); }