/* * WiFiSense ESP32-S3 — OLED Edition * OLED: 0.96" SSD1306 128×64 I2C * SDA → GPIO 6 * SCL → GPIO 7 * * Thư viện cần cài (Library Manager): * • Adafruit SSD1306 (by Adafruit) * • Adafruit GFX Library (by Adafruit) */ #include #include #include #include // ─── Cấu hình Wi-Fi ────────────────────────────────────────────────────────── const char* ssid = "OrangePiVietnam"; const char* password = "orangepi.vn"; // ─── OLED ──────────────────────────────────────────────────────────────────── #define OLED_WIDTH 128 #define OLED_HEIGHT 64 #define OLED_RESET -1 // Dùng chân RESET của ESP32 #define OLED_ADDRESS 0x3C #define OLED_SDA_PIN 6 #define OLED_SCL_PIN 7 Adafruit_SSD1306 display(OLED_WIDTH, OLED_HEIGHT, &Wire, OLED_RESET); // ─── LED ───────────────────────────────────────────────────────────────────── constexpr int LED_PIN = 2; constexpr uint8_t LED_ON_LEVEL = HIGH; constexpr uint8_t LED_OFF_LEVEL = LOW; // ─── Thuật toá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 uint32_t WIFI_CONNECT_TIMEOUT_MS = 30'000; 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; // ─── Biến toàn cục ─────────────────────────────────────────────────────────── 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; // ─── Trạng thái màn hình ───────────────────────────────────────────────────── // Xoay vòng giữa hai trang: trang 0 = chỉ số chính, trang 1 = chi tiết enum DisplayPage { PAGE_MAIN = 0, PAGE_DETAIL = 1 }; DisplayPage currentPage = PAGE_MAIN; uint32_t lastPageSwitch = 0; constexpr uint32_t PAGE_DURATION_MS = 3000; // chuyển trang mỗi 3 giây // ═════════════════════════════════════════════════════════════════════════════ // Hàm tiện ích // ═════════════════════════════════════════════════════════════════════════════ float clampValue(float value, float lower, float upper) { return value < lower ? lower : (value > upper ? upper : value); } size_t latestWindowIndex(size_t offset) { return (windowIndex + WINDOW_SIZE - offset) % WINDOW_SIZE; } // ─── Kalman & Smoothing ─────────────────────────────────────────────────────── float kalmanFilter(float measurement) { const float priorError = kalmanError + PROCESS_NOISE; const float gain = priorError / (priorError + MEASUREMENT_NOISE); kalmanEstimate += gain * (measurement - kalmanEstimate); kalmanError = (1.0f - gain) * priorError; return kalmanEstimate; } float exponentialSmoothing(float measurement) { smoothedRssi = SMOOTH_ALPHA * measurement + (1.0f - SMOOTH_ALPHA) * smoothedRssi; return smoothedRssi; } // ─── Thống kê ──────────────────────────────────────────────────────────────── float standardDeviation(const float* values, size_t count) { if (count == 0) return 0.0f; float mean = 0.0f; for (size_t i = 0; i < count; ++i) mean += values[i]; mean /= static_cast(count); float variance = 0.0f; for (size_t i = 0; i < count; ++i) { const float d = values[i] - mean; variance += d * d; } return sqrtf(variance / static_cast(count)); } float analyzeWindowVariance() { return standardDeviation(rssiWindow, WINDOW_SIZE); } float analyzeLongTermVariance() { return standardDeviation(rssiLongWindow, LONG_WINDOW); } float detectRateOfChange() { if (sampleCount < 10) return 0.0f; float recentMean = 0.0f, oldMean = 0.0f; for (size_t i = 1; i <= 5; ++i) { recentMean += rssiWindow[latestWindowIndex(i)]; oldMean += rssiWindow[latestWindowIndex(i + 5)]; } return fabsf((recentMean - oldMean) / 5.0f); } float detectPeak() { if (sampleCount < 3) return 0.0f; const float current = rssiWindow[latestWindowIndex(1)]; const float previous = rssiWindow[latestWindowIndex(2)]; const float older = rssiWindow[latestWindowIndex(3)]; const float currentDelta = fabsf(current - previous); const float previousDelta = fabsf(previous - older); return currentDelta > previousDelta + 1.0f ? currentDelta : 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 /= static_cast(WINDOW_SIZE); const float stdDev = fmaxf(analyzeWindowVariance(), 0.1f); const float current = rssiWindow[latestWindowIndex(1)]; return fabsf((current - mean) / stdDev); } int calculateConfidence(float variance, float rateOfChange, float peak, float zScore) { float c = variance > FAST_MOVEMENT_THRESHOLD ? 75.0f : variance > SLOW_MOVEMENT_THRESHOLD ? 50.0f : 15.0f; if (rateOfChange > 2.0f) c += 15.0f; if (peak > PEAK_THRESHOLD) c += 10.0f; if (zScore > Z_SCORE_THRESHOLD) c += 10.0f; return static_cast(clampValue(c, 0.0f, 100.0f)); } const char* getMotionIntensity(float variance, float rateOfChange) { if (variance > FAST_MOVEMENT_THRESHOLD && rateOfChange > 2.5f) return "SPRINT"; if (variance > FAST_MOVEMENT_THRESHOLD) return "FAST"; if (variance > SLOW_MOVEMENT_THRESHOLD && rateOfChange > 1.0f) return "WALKING"; if (variance > SLOW_MOVEMENT_THRESHOLD) return "SLOW"; return "CALM"; } void initializeRssiWindows(float initialRssi) { for (int i = 0; i < WINDOW_SIZE; ++i) rssiWindow[i] = initialRssi; for (int i = 0; i < LONG_WINDOW; ++i) rssiLongWindow[i] = initialRssi; windowIndex = 0; longWindowIndex = 0; sampleCount = WINDOW_SIZE; } void setDetectionLed(bool detected) { digitalWrite(LED_PIN, detected ? LED_ON_LEVEL : LED_OFF_LEVEL); } // ═════════════════════════════════════════════════════════════════════════════ // OLED helpers // ═════════════════════════════════════════════════════════════════════════════ /* * Vẽ thanh ngang [x,y] rộng maxW, chiều cao h, giá trị val trong [0,100] */ 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 * (maxW - 2)); if (fill > 0) display.fillRect(x + 1, y + 1, fill, h - 2, WHITE); } /* * Trang 0: Thông tin chính * ┌──────────────────────────────┐ * │ WiFiSense [MOTION] │ ← dòng tiêu đề + trạng thái * │ RSSI: -67 dBm Conf: 85% │ * │ Conf [████████░░░░░░░] │ * │ Qual [██████████░░░░░] │ * │ Base:-65.3 Var:2.1 #12 │ ← dòng tóm tắt * └──────────────────────────────┘ */ void drawPageMain(int rawRssi, int confidence, int quality, float variance, bool detected) { display.clearDisplay(); // ── Dòng 1: tiêu đề + trạng thái ── display.setTextSize(1); display.setTextColor(WHITE); display.setCursor(0, 0); display.print("WiFiSense"); // Hộp trạng thái bên phải const char* status = detected ? " MOTION " : " CLEAR "; int sx = 128 - 6 * strlen(status); if (detected) { display.fillRect(sx - 1, 0, 128 - sx + 1, 8, WHITE); display.setTextColor(BLACK); } display.setCursor(sx, 0); display.print(status); display.setTextColor(WHITE); // ── Dòng 2: RSSI & Conf số ── display.setCursor(0, 11); display.printf("RSSI:%4d dBm", rawRssi); display.setCursor(84, 11); display.printf("C:%d%%", 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); // ── Dòng cuối: baseline, variance, tổng detections ── display.setCursor(0, 43); display.printf("Base:%.1f", baseline); display.setCursor(64, 43); display.printf("Var:%.1f", variance); display.setCursor(0, 54); display.printf("Detections: %lu", (unsigned long)totalDetections); display.display(); } /* * Trang 1: Chi tiết kỹ thuật * ┌──────────────────────────────┐ * │ --- Detail --- │ * │ Kalman: -66.43 │ * │ Smooth: -66.21 │ * │ ZScore: 1.87 │ * │ Rate: 0.6 [WALKING] │ * └──────────────────────────────┘ */ void drawPageDetail(float kalmanFiltered, float smoothFiltered, float zScore, float rateOfChange, float variance) { display.clearDisplay(); display.setTextSize(1); display.setTextColor(WHITE); display.setCursor(0, 0); display.print("--- Detail ---"); display.setCursor(0, 11); display.printf("Kalman: %.2f dBm", kalmanFiltered); display.setCursor(0, 21); display.printf("Smooth: %.2f dBm", smoothFiltered); display.setCursor(0, 31); display.printf("ZScore: %.2f", zScore); display.setCursor(0, 41); display.printf("Rate: %.1f", rateOfChange); // Nhãn cường độ chuyển động ở góc phải dưới const char* intensity = getMotionIntensity(variance, rateOfChange); int lx = 128 - 6 * (int)strlen(intensity); display.setCursor(lx, 41); display.print(intensity); // Trang số nhỏ display.setCursor(110, 56); display.print("2/2"); display.display(); } // ── Màn hình khởi động ──────────────────────────────────────────────────────── void oledSplash() { display.clearDisplay(); display.setTextSize(1); display.setTextColor(WHITE); display.setCursor(16, 10); display.print("WiFiSense v2"); display.setCursor(10, 22); display.print("ESP32-S3 + OLED"); display.setCursor(4, 36); display.print("orangepi.vn"); display.display(); delay(1500); } // ── Màn hình trạng thái kết nối Wi-Fi ──────────────────────────────────────── void oledStatus(const char* msg, bool clear = true) { if (clear) display.clearDisplay(); // Cuộn lên: in ở dòng cuối rồi scroll display.setTextSize(1); display.setTextColor(WHITE); // Tìm dòng trống kế tiếp (đơn giản: in lên dòng 56) display.setCursor(0, 56); display.print(msg); display.display(); } // ── Màn hình log cuộn (dùng trong khi kết nối / calibrate) ─────────────────── #define LOG_LINES 8 char logBuf[LOG_LINES][22]; // 21 ký tự + null uint8_t logHead = 0; 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) { int idx = (logHead + i) % LOG_LINES; display.setCursor(0, i * 8); display.print(logBuf[idx]); } display.display(); } // ═════════════════════════════════════════════════════════════════════════════ // Wi-Fi & Calibration (với OLED log) // ═════════════════════════════════════════════════════════════════════════════ bool connectToWifi() { WiFi.mode(WIFI_STA); WiFi.begin(ssid, password); char tmp[22]; snprintf(tmp, sizeof(tmp), "Connecting..."); logPush(tmp); logRender(); Serial.print("Connecting to Wi-Fi"); const uint32_t startedAt = millis(); uint8_t dotCount = 0; while (WiFi.status() != WL_CONNECTED) { if (millis() - startedAt >= WIFI_CONNECT_TIMEOUT_MS) { logPush("Timeout! Restart"); logRender(); Serial.println("\nTimeout. Restarting..."); return false; } delay(500); Serial.print('.'); dotCount++; if (dotCount % 10 == 0) { snprintf(tmp, sizeof(tmp), "Wait %lus...", (millis() - startedAt) / 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("\nConnected. IP: %s, RSSI: %d dBm\n", WiFi.localIP().toString().c_str(), WiFi.RSSI()); delay(800); return true; } void calibrate() { char tmp[22]; snprintf(tmp, sizeof(tmp), "Calibrating %u smpl", CALIBRATION_SAMPLES); logPush(tmp); logPush("Keep room EMPTY!"); logRender(); Serial.printf("Calibrating %u samples; keep room empty...\n", CALIBRATION_SAMPLES); float calibrationMean = 0.0f; for (uint16_t i = 0; i < CALIBRATION_SAMPLES; ++i) { calibrationMean += static_cast(WiFi.RSSI()); delay(30); // Cập nhật tiến trình mỗi 25 mẫu if (i % 25 == 0) { snprintf(tmp, sizeof(tmp), "Cal %d/%d", i, CALIBRATION_SAMPLES); logPush(tmp); logRender(); } } baseline = calibrationMean / static_cast(CALIBRATION_SAMPLES); kalmanEstimate = baseline; smoothedRssi = baseline; initializeRssiWindows(baseline); snprintf(tmp, sizeof(tmp), "Base: %.2f dBm", baseline); logPush(tmp); logPush("Cal done! Starting."); logRender(); delay(1000); Serial.printf("Calibration done. Baseline: %.2f dBm\n", baseline); } // ═════════════════════════════════════════════════════════════════════════════ // setup() & loop() // ═════════════════════════════════════════════════════════════════════════════ void setup() { Serial.begin(115200); pinMode(LED_PIN, OUTPUT); setDetectionLed(false); // Khởi OLED trước tiên Wire.begin(OLED_SDA_PIN, OLED_SCL_PIN); if (!display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDRESS)) { Serial.println("SSD1306 init failed — check wiring!"); // Không dừng, tiếp tục chạy với Serial } display.cp437(true); display.setTextWrap(false); oledSplash(); Serial.println("\n========== WiFiSense ESP32-S3 OLED =========="); if (!connectToWifi()) { delay(2'000); ESP.restart(); } calibrate(); Serial.println("Running. RSSI metrics active."); display.clearDisplay(); display.display(); lastPageSwitch = millis(); } void loop() { // ── Reconnect nếu mất kết nối ────────────────────────────────────────── if (WiFi.status() != WL_CONNECTED) { setDetectionLed(false); logPush("WiFi lost!"); logPush("Reconnecting..."); logRender(); if (!connectToWifi()) { delay(2'000); } return; } // ── Lấy mẫu ──────────────────────────────────────────────────────────── const int rawRssi = WiFi.RSSI(); const float kalmanFiltered = kalmanFilter(static_cast(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ị OLED — xoay trang ───────────────────────────────────────── // if (millis() - lastPageSwitch >= PAGE_DURATION_MS) { // currentPage = (currentPage == PAGE_MAIN) ? PAGE_DETAIL : PAGE_MAIN; // lastPageSwitch = millis(); // } if (currentPage == PAGE_MAIN) { drawPageMain(rawRssi, confidence, (int)signalQuality, variance, detected); } else { drawPageDetail(kalmanFiltered, smoothFiltered, zScore, rateOfChange, variance); } // ── Serial (vẫn giữ để debug) ─────────────────────────────────────────── Serial.printf( "Raw:%d,Kalman:%.2f,Smooth:%.2f,Baseline:%.2f,Var:%.2f,Z:%.2f,Conf:%d\n", rawRssi, kalmanFiltered, smoothFiltered, baseline, variance, zScore, confidence); Serial.printf( "[%s] Conf:%d%% | Qual:%d%% | Rate:%.1f | Total:%lu\n", getMotionIntensity(variance, rateOfChange), confidence, (int)signalQuality, rateOfChange, (unsigned long)totalDetections); delay(SAMPLE_INTERVAL_MS); }