Files
WifiSense-ESP32/WiFiSense.ino
T
2026-08-30 20:44:35 +07:00

687 lines
32 KiB
Arduino
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.
/*
* 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 <WiFi.h>
#include <WebServer.h>
#include <DNSServer.h>
#include <Preferences.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
// ═══════════════════════════════════════════════════════════════════════════════
// 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 <select>
static String buildHtml(int networkCount) {
String html = R"rawhtml(<!DOCTYPE html>
<html lang='vi'>
<head>
<meta charset='UTF-8'>
<meta name='viewport' content='width=device-width,initial-scale=1'>
<title>WiFiSense Setup</title>
<style>
body{font-family:sans-serif;background:#111;color:#eee;display:flex;
justify-content:center;align-items:center;min-height:100vh;margin:0}
.card{background:#1e1e1e;border:1px solid #333;border-radius:12px;
padding:28px 24px;width:320px;box-shadow:0 4px 24px #0008}
h2{margin:0 0 6px;font-size:1.3rem;color:#4fc3f7}
p{margin:0 0 20px;font-size:.85rem;color:#888}
label{font-size:.85rem;color:#aaa;display:block;margin-bottom:4px}
select,input{width:100%;box-sizing:border-box;padding:9px 10px;
border-radius:6px;border:1px solid #444;background:#2a2a2a;
color:#eee;font-size:.95rem;margin-bottom:16px}
button{width:100%;padding:11px;border:none;border-radius:6px;
background:#4fc3f7;color:#111;font-size:1rem;font-weight:700;cursor:pointer}
button:active{background:#0288d1}
.note{margin-top:14px;font-size:.75rem;color:#555;text-align:center}
</style>
</head>
<body>
<div class='card'>
<h2>WiFiSense Setup</h2>
<p>Chọn mạng WiFi nhập mật khẩu</p>
<form method='POST' action='/save'>
<label>Mạng WiFi</label>
<select name='ssid'>
)rawhtml";
for (int i = 0; i < networkCount; ++i) {
int rssi = WiFi.RSSI(i);
String bar = rssi > -60 ? "▊▊▊" : rssi > -75 ? "▊▊░" : "▊░░";
html += "<option value='" + WiFi.SSID(i) + "'>" +
WiFi.SSID(i) + " " + bar + " (" + String(rssi) + " dBm)</option>\n";
}
html += R"rawhtml(
</select>
<label>Mật khẩu</label>
<input type='password' name='pass' placeholder='(để trống nếu mạng mở)'>
<button type='submit'>Kết nối &amp; Lưu</button>
</form>
<p class='note'>ESP32 sẽ khởi động lại sau khi lưu</p>
</div>
</body>
</html>)rawhtml";
return html;
}
static String buildSavedHtml(const String& ssid) {
return String("<!DOCTYPE html><html lang='vi'><head><meta charset='UTF-8'>"
"<meta name='viewport' content='width=device-width,initial-scale=1'>"
"<title>Đã lưu</title>"
"<style>body{font-family:sans-serif;background:#111;color:#eee;"
"display:flex;justify-content:center;align-items:center;min-height:100vh}"
".card{background:#1e1e1e;border:1px solid #333;border-radius:12px;"
"padding:28px 24px;width:300px;text-align:center}"
"h2{color:#81c784}p{color:#888;font-size:.9rem}</style></head>"
"<body><div class='card'>"
"<h2>✓ Đã lưu!</h2>"
"<p>Đang kết nối tới <b>" + ssid + "</b></p>"
"<p>ESP32 sẽ khởi động lại trong vài giây...</p>"
"</div></body></html>");
}
// ── 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);
}