diff --git a/README.md b/README.md index d7dc7f1..8d702c8 100644 --- a/README.md +++ b/README.md @@ -1,510 +1,330 @@ -# WiFiSense +# WiFiSense — ESP32-S3 + OLED Edition -Firmware ESP32-S3 phát hiện sự hiện diện/chuyển động của người trong phòng bằng **phân tích tín hiệu Wi‑Fi và xử lý tín hiệu thống kê**. Không cần cảm biến bổ sung — chỉ cần Wi‑Fi. +> Phát hiện chuyển động thụ động qua biến động tín hiệu RSSI WiFi, hiển thị trực tiếp trên màn hình OLED 0.96", tự động cấu hình WiFi qua Captive Portal. -> **English version:** [README-en.md](README-en.md) - -## Tổng quan - -Dự án sử dụng ESP32-S3 liên tục theo dõi cường độ tín hiệu Wi‑Fi (RSSI) và phát hiện những nhiễu động đo được do chuyển động của con người gây ra. Khi phát hiện chuyển động, ngõ ra LED đã cấu hình sẽ được kích hoạt theo thời gian thực. - -**Phù hợp cho:** tự động hóa nhà ở, phát hiện có người trong phòng, kích hoạt đèn thông minh và tiết kiệm năng lượng. +![Platform](https://img.shields.io/badge/Platform-ESP32--S3-blue) +![IDE](https://img.shields.io/badge/IDE-Arduino-teal) +![License](https://img.shields.io/badge/License-MIT-green) --- -## Cơ sở vật lý +## Mục lục -### Nguyên lý nền tảng - -Cơ thể người chứa khoảng 60% nước. Nước dẫn điện và hấp thụ mạnh bức xạ điện từ tại 2,4 GHz (tần số Wi‑Fi): - -- **Hệ số hấp thụ của nước ở 2,4 GHz:** ~0,15 dB/cm -- **Bề rộng thân người điển hình:** ~20 cm -- **Suy hao tín hiệu do cơ thể người:** 5–15 dB, tùy vị trí - -Khi bạn di chuyển giữa router và ESP32-S3: - -1. **Tín hiệu mạnh hơn** khi có ít vật cản hơn. -2. **Tín hiệu yếu đi** khi cơ thể chặn đường truyền thẳng. -3. **Tín hiệu dao động** do bị tán xạ và phản xạ. - -Chính biến động RSSI động này là tín hiệu mà firmware phát hiện. - -### Vì sao phương sai quan trọng - -Nhiễu tĩnh (tường, nội thất) ảnh hưởng RSSI một cách ổn định. Ngược lại, **chuyển động của người tạo ra thay đổi nhanh và khó dự đoán**: - -- **Môi trường yên tĩnh:** độ lệch chuẩn RSSI = 0,2–0,5 dBm (nhiễu ngẫu nhiên) -- **Người đứng yên:** độ lệch chuẩn = 0,8–1,5 dBm (hơi thở, cử động nhỏ) -- **Người đi lại:** độ lệch chuẩn = 2,5–5,0 dBm (biến động mạnh) - -Firmware đo **độ lệch chuẩn của 40 lần đọc RSSI gần nhất** để phát hiện bất thường. +- [Nguyên lý hoạt động](#nguyên-lý-hoạt-động) +- [Sơ đồ luồng hệ thống](#sơ-đồ-luồng-hệ-thống) +- [Phần cứng](#phần-cứng) +- [Cài đặt môi trường](#cài-đặt-môi-trường) +- [Nạp code](#nạp-code) +- [Cách sử dụng](#cách-sử-dụng) +- [Màn hình OLED](#màn-hình-oled) +- [Troubleshooting](#troubleshooting) --- -## Các thuật toán phát hiện +## Nguyên lý hoạt động -Firmware kết hợp **6 hệ thống phát hiện độc lập**: +WiFiSense **không dùng camera, microphone hay cảm biến vật lý nào**. Thay vào đó, nó khai thác một hiện tượng vật lý: khi có người/vật di chuyển trong phòng, sóng WiFi bị phản xạ và nhiễu loạn, khiến **chỉ số RSSI** (cường độ tín hiệu) dao động theo những mẫu đặc trưng. -### 1. Bộ lọc Kalman - -Giảm nhiễu đo lường nhưng vẫn giữ các cạnh tín hiệu (các thay đổi quan trọng). +### Pipeline xử lý tín hiệu ``` -filtered_value = estimate + gain × (measurement - estimate) +WiFi RSSI thô + │ + ▼ +┌─────────────────┐ +│ Kalman Filter │ ← Loại bỏ nhiễu ngẫu nhiên ngắn hạn +└────────┬────────┘ + │ + ▼ +┌──────────────────────┐ +│ Exponential Smoothing │ ← Làm mượt tín hiệu (α = 0.15) +└──────────┬───────────┘ + │ + ▼ +┌──────────────────────────────────────────────────┐ +│ Cửa sổ trượt (40 mẫu ngắn + 100 mẫu dài) │ +│ • Độ lệch chuẩn (Variance) → biên độ dao động │ +│ • Rate of Change → tốc độ thay đổi │ +│ • Peak Detection → đỉnh đột biến │ +│ • Z-Score → bất thường thống │ +└──────────────────────┬───────────────────────────┘ + │ + ▼ +┌────────────────────────────────┐ +│ Tính Confidence Score (0–100) │ +└──────────────┬─────────────────┘ + │ + ▼ +┌──────────────────────────────────────────────────┐ +│ Persistence Counter (≥ 3 mẫu liên tiếp) │ +│ → Kết luận: MOTION / CLEAR │ +│ → Bật LED + hiển thị OLED │ +└──────────────────────────────────────────────────┘ ``` -- **Kalman gain:** 0,15 (cân bằng độ nhạy và độ ổn định) -- **Tác dụng:** loại bỏ 60–70% nhiễu ngẫu nhiên mà ít gây trễ +### Adaptive Baseline -### 2. Làm mượt hàm mũ - -Lớp lọc thứ hai để tính đường nền rất mượt. - -``` -smooth = 0.15 × new_reading + 0.85 × previous_smooth -``` - -### 3. Phân tích phương sai cửa sổ trượt - -Phát hiện bất thường bằng cách phân tích hành vi tín hiệu theo thời gian. - -``` -variance = √(Σ(reading - mean)² / N) -``` - -- **Kích thước cửa sổ:** 40 mẫu, tương đương khoảng 1,4 giây dữ liệu thời gian thực -- **Ngưỡng chuyển động chậm:** độ lệch chuẩn 1,8 dBm -- **Ngưỡng chuyển động nhanh:** độ lệch chuẩn 3,5 dBm - -### 4. Phát hiện Z-score (bất thường thống kê) - -Xác định các lần đọc lệch khỏi chuẩn thống kê. - -``` -z_score = |current_reading - mean| / standard_deviation -``` - -- **Ngưỡng bất thường:** Z > 2,5 (độ tin cậy thống kê 99,4%) -- **Tác dụng:** hạn chế dương tính giả do các đột biến ngẫu nhiên - -### 5. Phát hiện đỉnh - -Bắt các chuyển tiếp tín hiệu đột ngột khi bạn bắt đầu di chuyển vào phòng. - -``` -peak = max(Δ_current - Δ_previous) -``` - -Thuật toán kiểm tra liệu bản thân tốc độ thay đổi có biến động đột ngột hay không. - -### 6. Tốc độ thay đổi - -Định lượng mức độ thay đổi nhanh của tín hiệu. - -``` -rate = mean(recent_5_samples) - mean(old_5_samples) -``` - -Chuyển động nhanh thường tạo tốc độ thay đổi lớn hơn. +Baseline (đường cơ sở khi không có chuyển động) **tự thích nghi** theo thời gian thực với tốc độ rất chậm (α = 0.008), giúp hệ thống tự điều chỉnh khi môi trường thay đổi dần (người rời phòng, thay đổi vị trí router…). --- -## Logic phát hiện (đa tiêu chí) - -Firmware kích hoạt phát hiện khi: +## Sơ đồ luồng hệ thống ``` -motion = (variance > 1.8) OR (Z-score > 2.5 AND peak > 2.0) + ┌──────────────┐ + │ Khởi động │ + └──────┬───────┘ + │ + ┌──────▼───────┐ + │ Đọc NVS │ credentials đã lưu? + └──────┬───────┘ + Có ▼ ▼ Không + ┌──────────────┐ ┌──────────────────────┐ + │ Kết nối WiFi │ │ Mở AP WiFiSense-Setup│ + └──────┬───────┘ │ DNS + Captive Portal │ + OK ▼ ▼ Fail │ Người dùng chọn mạng │ + ┌────────┐ ┌──────────┐ │ → Lưu NVS → Restart │ + │Calibrate│ │Thử lại x3│ └──────────────────────┘ + └────┬───┘ └────┬─────┘ + │ Fail│ + │ ┌────▼──────────────┐ + │ │ Xóa NVS + Restart │ + │ └───────────────────┘ + ┌────▼──────────────────┐ + │ Loop chính │ + │ • Lấy RSSI mỗi 500ms │ + │ • Kalman + Smooth │ + │ • Tính Variance/Z │ + │ • Cập nhật OLED │ + │ • Bật/tắt LED │ + └───────────────────────┘ ``` -Diễn giải: chuyển động được phát hiện nếu độ lệch chuẩn tín hiệu cao, **hoặc** có bất thường thống kê đồng thời có chuyển tiếp tín hiệu sắc nét. - -Sau khi có chuyển động, điều kiện phải duy trì **3 chu kỳ liên tiếp** thì LED mới bật. Đây là cổng bền vững để loại bỏ dương tính giả. - --- -## Màn hình OLED +## Phần cứng -Firmware hỗ trợ **màn hình OLED SSD1306 0,96 inch, độ phân giải 128×64, giao tiếp I2C** nối với ESP32-S3 như sau: +### Linh kiện cần thiết -| Chân OLED | Chân ESP32-S3 | +| Linh kiện | Thông số | Ghi chú | +|---|---|---| +| Vi điều khiển | ESP32-S3 (bất kỳ board) | Đã test trên ESP32-S3-DevKitC-1 | +| Màn hình OLED | 0.96" SSD1306 128×64 I2C | Địa chỉ I2C: `0x3C` | +| LED | 3mm hoặc 5mm, bất kỳ màu | Kèm điện trở 220Ω–330Ω | +| Dây jumper | Male-to-Male hoặc Male-to-Female | — | + +### Sơ đồ đấu dây + +``` +ESP32-S3 SSD1306 OLED +───────── ──────────── +GPIO 6 ───────── SDA +GPIO 7 ───────── SCL +3.3V ───────── VCC +GND ───────── GND + +ESP32-S3 LED +───────── ──────────── +GPIO 2 ──[330Ω]── Anode (+) +GND ───────── Cathode (-) +``` + +> **Lưu ý:** Một số board ESP32-S3 (ví dụ Waveshare) có thể có sẵn LED trên GPIO 2 — kiểm tra datasheet của board bạn đang dùng. + +--- + +## Cài đặt môi trường + +### 1. Arduino IDE + +Tải về tại [arduino.cc/en/software](https://www.arduino.cc/en/software) (phiên bản 2.x khuyến nghị). + +### 2. Thêm ESP32 vào Board Manager + +Vào **File → Preferences → Additional boards manager URLs**, thêm: + +``` +https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json +``` + +Sau đó vào **Tools → Board → Boards Manager**, tìm **esp32** của Espressif và cài đặt. + +### 3. Cài thư viện + +Vào **Tools → Manage Libraries** và cài đặt 2 thư viện sau: + +| Thư viện | Tác giả | Ghi chú | +|---|---|---| +| **Adafruit SSD1306** | Adafruit | Tìm đúng tên này | +| **Adafruit GFX Library** | Adafruit | Dependency của SSD1306 | + +> Các thư viện còn lại (`WiFi`, `WebServer`, `DNSServer`, `Preferences`) đã có sẵn trong ESP32 Arduino core, **không cần cài thêm**. + +### 4. Chọn board + +**Tools → Board → ESP32 Arduino → ESP32S3 Dev Module** + +Các thông số quan trọng: + +| Tùy chọn | Giá trị | |---|---| -| VCC | 3.3V | -| GND | GND | -| SDA | GPIO 6 | -| SCL | GPIO 7 | - -Địa chỉ I2C mặc định là `0x3C`. Giao tiếp I2C được khởi tạo bằng `Wire.begin(6, 7)`. - -Cài các thư viện sau bằng Library Manager của Arduino IDE: - -- **Adafruit SSD1306** của Adafruit -- **Adafruit GFX Library** của Adafruit - -Trang chính hiển thị trạng thái Wi‑Fi, RSSI, độ tin cậy, chất lượng tín hiệu, đường nền, phương sai và tổng số lần phát hiện. Sketch đã có trang chi tiết kỹ thuật, nhưng phần tự chuyển trang hiện đang được tắt trong `loop()`, vì vậy trang chính được hiển thị liên tục. - -Nếu màn hình không khởi tạo, hãy kiểm tra nguồn 3,3V, dây GND chung, dây SDA/SCL và giá trị `OLED_ADDRESS` trong `WiFiSense.ino`. +| USB Mode | Hardware CDC and JTAG | +| Flash Size | 4MB (hoặc theo board của bạn) | +| Partition Scheme | Default 4MB with spiffs | +| PSRAM | Disabled (nếu board không có) | --- -## Dữ liệu đầu ra thời gian thực +## Nạp code -### Serial Plotter (đồ thị) +1. Kết nối ESP32-S3 với máy tính qua USB. +2. Mở file `WiFiSense.ino` trong Arduino IDE. +3. Chọn đúng **Port** tại **Tools → Port**. +4. Nhấn nút **Upload** (→) hoặc `Ctrl+U`. +5. Chờ quá trình biên dịch và nạp hoàn tất (~30–60 giây). -Mở `Tools > Serial Plotter` trong Arduino IDE để xem: - -- **Raw:** cường độ Wi‑Fi trực tiếp -- **Kalman:** tín hiệu đã giảm nhiễu -- **Smooth:** đường nền đã làm mượt -- **Baseline:** mức tín hiệu kỳ vọng hiện tại -- **Variance:** độ lệch chuẩn, chỉ báo chuyển động -- **ZScore:** độ lệch thống kê - -### Serial Monitor (văn bản) - -Ví dụ dữ liệu thời gian thực: - -``` -[WALKING] Conf:75% | Quality:92% | Rate:2.5 | Total:14 -[CALM] Conf:20% | Quality:88% | Rate:0.3 | Total:14 -``` - -- **Intensity:** `CALM → SLOW → WALKING → FAST → SPRINT` -- **Confidence:** độ chắc chắn phát hiện, 0–100% -- **Quality:** điểm ổn định tín hiệu, 0–100% -- **Rate:** tốc độ tín hiệu thay đổi -- **Total:** tổng số lần phát hiện trong phiên - ---- - -## Lắp đặt vật lý (rất quan trọng với độ chính xác) - -### Cấu hình tối ưu - -``` - Router (WiFi AP) - | - [3-5m] - | - [Người đi qua tại đây] - | - [ESP32-S3] -``` - -**Quy tắc đặt thiết bị:** - -1. **Khoảng cách:** 3–5 m giữa router và ESP32-S3. -2. **Tầm nhìn:** người nên đi qua gần vị trí giữa hai thiết bị. -3. **Vị trí router:** đặt anten thẳng đứng để có vùng phủ gần đẳng hướng. -4. **Tránh:** lò vi sóng, điện thoại không dây và các thiết bị 2,4 GHz khác. - -### Đánh giá thực tế - -- **Lắp đặt tốt:** độ chính xác phát hiện 85–92%. -- **Lắp đặt kém:** 60–75%. -- **Kém nhất:** đặt sát router hoặc bị chắn đường truyền thẳng có thể làm hệ thống không hiệu quả. - ---- - -## Độ chính xác và giới hạn - -### ✅ Hoạt động tốt với - -- **Nhận biết trạng thái có người trong phòng**. -- **Phát hiện chuyển động:** đi bộ, chạy, cử chỉ lớn. -- **Phản hồi thời gian thực:** độ trễ khoảng 35 ms. -- **Chống báo động giả:** cổng bền vững 3 chu kỳ và xác thực đa tiêu chí. - -### ⚠️ Giới hạn do đặc tính vật lý - -| Tình huống | Vấn đề | -|---|---| -| Chuyển động rất chậm (đang ngủ) | Có thể không phát hiện dao động mức hô hấp | -| Nhiều người | Tín hiệu bị trung bình; chỉ nhận biết “có người”, không đếm người | -| Vật thể kim loại lớn | Phản xạ có thể gây dương tính giả | -| Wi‑Fi ở xa | Biến động tín hiệu yếu khó phân biệt với nhiễu | -| Tường kim loại | Tán xạ tín hiệu làm giảm độ tin cậy | - -### ❌ Không thể thực hiện chỉ với Wi‑Fi - -- Phân biệt chính xác **đứng hay ngồi**. -- Xác định **đó là ai**. -- Định vị chính xác hơn mức “có/không có người trong phòng”. -- Đạt **độ chính xác trên 95%** (giới hạn vật lý thực tế khoảng 90%). - -Để vượt 90% độ chính xác, có thể cân nhắc: - -- Nhiều ESP32-S3 để tam giác hóa. -- Phân loại bằng học máy. -- Cảm biến kết hợp: Wi‑Fi + PIR. -- Wi‑Fi 5 GHz (nhạy hơn nhưng tầm ngắn hơn). - ---- - -## Hướng dẫn cài đặt - -### Phần cứng cần có - -- **Bo phát triển ESP32-S3** -- **Cáp USB** để nạp firmware -- **Mạng Wi‑Fi 2,4 GHz** -- LED nối vào `LED_PIN` đã cấu hình trong `WiFiSense.ino` (GPIO2 chỉ là mặc định; hãy kiểm tra chân LED của board) - -### Cài đặt phần mềm - -1. **Cài Arduino IDE** nếu chưa có - - Tải tại: https://www.arduino.cc/en/software - -2. **Thêm hỗ trợ board ESP32** - - Mở `Arduino IDE > Preferences`. - - Thêm URL sau vào **Additional Board Manager URLs**: - - ``` - https://espressif.github.io/arduino-esp32/package_esp32_index.json - ``` - - - Vào `Tools > Board > Board Manager`. - - Tìm **esp32** và cài **esp32 by Espressif Systems**. - -3. **Thư viện cần thiết** - - `WiFi.h` và `Wire.h` được cung cấp sẵn cùng ESP32 board support. - - Cài **Adafruit SSD1306** và **Adafruit GFX Library**. - -4. **Cấu hình board** - - ``` - Tools > Board: ESP32S3 Dev Module - Tools > USB CDC On Boot: Enabled - Tools > Flash Size: default - Tools > Baud Rate: 115200 - ``` - -5. **Cập nhật thông tin Wi‑Fi** - - Mở `WiFiSense.ino`. - - Tìm các dòng: - - ```cpp - const char* ssid = "YOUR_SSID"; - const char* password = "YOUR_PASSWORD"; - ``` - - - Thay bằng tên Wi‑Fi và mật khẩu của bạn. - -6. **Nạp firmware cho ESP32-S3** - - Kết nối ESP32-S3 bằng USB. - - Chọn đúng cổng tại `Tools > Port`. - - Nhấn **Upload**. - - Chờ thông báo build/nạp thành công. - -7. **Xem dữ liệu thời gian thực** - - Mở `Tools > Serial Monitor`, đặt tốc độ **115200**. - - Hoặc mở `Tools > Serial Plotter` để xem đồ thị. - - Bạn sẽ thấy ví dụ như: - - ``` - [CALM] Conf:15% | Quality:89% | Rate:0.2 | Total:0 - ``` - ---- - -## Hiệu chỉnh và tinh chỉnh - -### Nếu bỏ sót chuyển động - -Hạ các ngưỡng để tăng độ nhạy. Trong firmware hiện tại, chỉnh các hằng số sau: - -```cpp -constexpr float SLOW_MOVEMENT_THRESHOLD = 1.5f; // mặc định 1.8 -constexpr float FAST_MOVEMENT_THRESHOLD = 3.0f; // mặc định 3.5 -constexpr float Z_SCORE_THRESHOLD = 2.2f; // mặc định 2.5 -``` - -### Nếu có nhiều báo động giả - -Tăng yêu cầu bền vững: - -```cpp -constexpr uint8_t PERSISTENCE_REQUIRED = 5; // mặc định 3 -``` - -Hoặc tăng ngưỡng phương sai: - -```cpp -constexpr float SLOW_MOVEMENT_THRESHOLD = 2.0f; // mặc định 1.8 -``` - -### Điều chỉnh tốc độ phát hiện - -Giảm khoảng cách giữa các mẫu: - -```cpp -constexpr uint16_t SAMPLE_INTERVAL_MS = 25; // mặc định 35 ms -``` - -### Học môi trường - -Firmware tự điều chỉnh đường nền theo phòng: - -```cpp -constexpr float ADAPTIVE_ALPHA = 0.008f; -``` - -Hãy cho thiết bị hoạt động trong phòng yên tĩnh vài phút để đường nền thích nghi ổn định hơn. +> Nếu board không tự vào chế độ nạp: giữ nút **BOOT**, nhấn **RESET**, thả **RESET**, rồi thả **BOOT**. --- ## Cách sử dụng -### Hiệu chuẩn ban đầu ⚠️ **QUAN TRỌNG** +### Lần đầu tiên (chưa có WiFi lưu) -Thiết bị cần một giai đoạn hiệu chuẩn trong phòng trống trước khi phát hiện chuyển động chính xác. Firmware hiện tại thu 200 mẫu, mất khoảng 6 giây; nếu môi trường nhiễu, hãy để phòng trống lâu hơn: +Khi khởi động và chưa có credentials trong NVS, ESP32 sẽ tự động mở **Captive Portal**: -1. Cấp nguồn ESP32-S3 trong phòng cần giám sát. -2. Để phòng trống, không có người di chuyển. -3. Đợi firmware kết nối Wi‑Fi và hoàn tất hiệu chuẩn RSSI. -4. Serial Monitor sẽ hiển thị đường nền; phương sai trong môi trường yên tĩnh thường thấp (<1,0 dBm). -5. Sau đó, kiểm tra bằng cách đi qua vùng giữa router và ESP32-S3. +``` +OLED hiển thị: +┌──────────────────────────────┐ +│ ** SETUP MODE ** │ +│──────────────────────────────│ +│ 1. Ket noi WiFi: │ +│ WiFiSense-Setup │ +│ 2. Mo trinh duyet: │ +│ 192.168.4.1 │ +└──────────────────────────────┘ +``` -**Vì sao cần hiệu chuẩn?** +**Các bước cấu hình:** -- Mỗi phòng có đặc tính Wi‑Fi khác nhau (tường, nội thất, khoảng cách tới router). -- Thuật toán đường nền thích nghi cần điều kiện bình thường làm tham chiếu. -- Nếu không có tham chiếu, hệ thống khó phân biệt nhiễu phòng với chuyển động người. +1. Dùng điện thoại hoặc máy tính, kết nối vào WiFi **`WiFiSense-Setup`** (không mật khẩu). +2. Trình duyệt sẽ tự động mở trang cấu hình (Captive Portal). Nếu không tự mở, truy cập thủ công vào `http://192.168.4.1`. +3. Chọn mạng WiFi nhà bạn từ danh sách quét được. +4. Nhập mật khẩu WiFi → nhấn **Kết nối & Lưu**. +5. ESP32 lưu thông tin vào bộ nhớ NVS và **tự khởi động lại**. +6. Lần này ESP32 kết nối thẳng vào WiFi đã chọn và bắt đầu calibrate. -### Vận hành cơ bản +### Các lần sau (đã có WiFi lưu) -1. Cấp nguồn ESP32-S3. -2. Chờ kết nối Wi‑Fi. -3. Giữ phòng yên tĩnh trong giai đoạn hiệu chuẩn. -4. Mở Serial Monitor hoặc Serial Plotter. -5. LED sẽ bật khi phát hiện chuyển động. -6. Serial hiển thị điểm tin cậy và cường độ chuyển động. +ESP32 tự kết nối WiFi khi khởi động, không cần cấu hình lại. -### Theo dõi hiệu năng +### Đổi WiFi -Quan sát Serial Plotter: +Nếu muốn kết nối sang mạng khác: xóa credentials bằng cách mất điện trong khi đang ở chế độ portal, hoặc thêm một nút bấm vật lý để gọi `clearCredentials()` + `ESP.restart()`. -- **Variance:** nên vượt 1,8 khi có chuyển động. -- **ZScore:** có thể vượt 2,5 trong lúc di chuyển. -- **Confidence:** thường trên 50% khi có chuyển động rõ rệt. +### Calibration -### Khắc phục sự cố +Sau khi kết nối WiFi thành công, ESP32 sẽ **calibrate trong ~6 giây** (200 mẫu × 30ms). Trong thời gian này: -| Vấn đề | Cách xử lý | +- **Giữ phòng trống** (không có người di chuyển). +- OLED hiển thị tiến trình calibration. +- Baseline được thiết lập dựa trên RSSI trung bình. + +--- + +## Màn hình OLED + +Sau calibration, OLED hiển thị **1 trang duy nhất** cập nhật mỗi 500ms: + +``` +┌──────────────────────────────┐ ← 128px +│ WiFiSense [ MOTION ] │ dòng 1: tiêu đề + badge trạng thái +│──────────────────────────────│ đường kẻ +│ RSSI: -67dBm C: 82% │ dòng 2: RSSI thô + Confidence số +│ Conf [████████████░░░░░░] │ dòng 3: thanh Confidence +│ Qual [█████████░░░░░░░░░] │ dòng 4: thanh Signal Quality +│──────────────────────────────│ đường kẻ +│ Base:-66.1 Var:2.3 │ dòng 5: Baseline + Variance +│ #15 WALKING │ dòng 6: tổng detections + cường độ +└──────────────────────────────┘ +``` + +### Ý nghĩa các chỉ số + +| Chỉ số | Mô tả | |---|---| -| LED không sáng | Kiểm tra Wi‑Fi, `LED_PIN`, và mức kích hoạt `LED_ON_LEVEL` trong `WiFiSense.ino` | -| LED luôn sáng | Tăng `PERSISTENCE_REQUIRED` hoặc ngưỡng phương sai | -| Phát hiện chập chờn | Đặt ESP32-S3 tại vị trí Wi‑Fi tốt hơn, cách router 3–5 m | -| Không có Serial output | Kiểm tra baud rate 115200 và cáp USB có hỗ trợ dữ liệu | -| Nhiều báo động giả | Giảm `ADAPTIVE_ALPHA` xuống 0,005 để đường nền học chậm hơn | +| **RSSI** | Cường độ tín hiệu WiFi thô (dBm). Giá trị điển hình: -40 (gần) đến -90 (xa). | +| **C (Confidence)** | Độ tin cậy phát hiện chuyển động, 0–100%. Trên ~50% là có người. | +| **Conf bar** | Thanh trực quan hóa Confidence. | +| **Qual bar** | Chất lượng tín hiệu tổng thể (dựa trên độ ổn định dài hạn). | +| **Base** | Baseline RSSI hiện tại (tự thích nghi). | +| **Var** | Variance của cửa sổ 40 mẫu gần nhất — số càng cao = dao động càng mạnh. | +| **#N** | Tổng số lần phát hiện chuyển động kể từ khi khởi động. | +| **Badge** | `MOTION` (đảo màu trắng) khi phát hiện, `CLEAR` khi yên tĩnh. | +| **Cường độ** | `CALM` / `SLOW` / `WALKING` / `FAST` / `SPRINT` — ước lượng mức độ di chuyển. | --- -## Thông số hiệu năng +## Troubleshooting -| Chỉ số | Giá trị | +### OLED không hiển thị gì + +| Nguyên nhân | Giải pháp | |---|---| -| Độ trễ phát hiện | 35–70 ms | -| Tỷ lệ dương tính giả | <5% khi bố trí tốt | -| Tỷ lệ phát hiện đúng | 85–92% | -| Công suất tiêu thụ | ~100 mA khi hoạt động | -| Thời gian hiệu chuẩn | 200 mẫu, khoảng 6 giây; nên để trống lâu hơn nếu nhiễu | -| Tầm bố trí tối ưu | 3–5 m | -| Ngưỡng phương sai tối thiểu | 1,8 dBm | +| Sai địa chỉ I2C | Chạy sketch `I2C Scanner` để tìm địa chỉ thực. Nếu khác `0x3C`, sửa `#define OLED_ADDRESS` thành `0x3D`. | +| Sai chân SDA/SCL | Kiểm tra lại: SDA → GPIO **6**, SCL → GPIO **7**. | +| Nguồn không đủ | Đảm bảo OLED lấy nguồn 3.3V (không phải 5V đối với các module 3.3V). | +| Thư viện chưa cài | Kiểm tra lại Library Manager: `Adafruit SSD1306` và `Adafruit GFX Library`. | + +### Không thấy WiFi "WiFiSense-Setup" + +| Nguyên nhân | Giải pháp | +|---|---| +| ESP32 đang kết nối WiFi cũ | Chờ 30 giây timeout, ESP32 sẽ tự chuyển sang AP mode. | +| Credentials cũ còn trong NVS | Upload lại firmware có `clearCredentials()` trong `setup()` (tạm thời), sau đó upload lại bản gốc. | + +### Trang cấu hình không tự mở trên điện thoại + +Mt số thiết bị Android/iOS không tự kích hoạt captive portal với DNS redirect. Mở trình duyệt và truy cập thủ công: **`http://192.168.4.1`** + +### Phát hiện chuyển động không chính xác + +| Vấn đề | Giải pháp | +|---|---| +| Phát hiện khi không có người | Giảm `FAST_MOVEMENT_THRESHOLD` và `SLOW_MOVEMENT_THRESHOLD`, hoặc tăng `PERSISTENCE_REQUIRED`. | +| Không phát hiện khi có người | Đặt ESP32 gần router hơn; tăng `CALIBRATION_SAMPLES`; kiểm tra không có thiết bị khác gây nhiễu. | +| Baseline trôi quá nhanh | Giảm `ADAPTIVE_ALPHA` (ví dụ từ `0.008` xuống `0.003`). | + +### Mất kết nối WiFi liên tục + +- Kiểm tra khoảng cách tới router. RSSI nên ở mức > -75 dBm để ổn định. +- Sau 3 lần mất kết nối liên tiếp (`MAX_RECONNECT_ATTEMPTS = 3`), ESP32 tự xóa NVS và mở lại Captive Portal — đây là hành vi thiết kế để người dùng có thể đổi mạng. +- Tăng `MAX_RECONNECT_ATTEMPTS` nếu mạng nhà bạn hay bị ngắt tạm thời. + +### Upload thất bại (Port not found / No device) + +1. Kiểm tra cáp USB (phải là cáp data, không phải cáp sạc). +2. Giữ nút **BOOT** trên board, nhấn **RESET**, thả **RESET**, thả **BOOT**, rồi Upload ngay. +3. Thử port COM khác trong Arduino IDE. --- -## Chi tiết kỹ thuật +## Cấu trúc project -### Sử dụng bộ nhớ - -ESP32-S3 có bộ nhớ dư dả cho các buffer và biến xử lý của firmware này. Mức sử dụng Flash/SRAM thực tế phụ thuộc phiên bản Arduino-ESP32, cấu hình board và tùy chọn build. - -### Hiệu năng thuật toán - -- **Bộ lọc Kalman:** O(1), thời gian hằng. -- **Tính phương sai:** O(40), tuyến tính theo cửa sổ ngắn. -- **Tính Z-score:** O(40), tuyến tính theo cửa sổ ngắn. -- **Chu kỳ đo:** mặc định 35 ms. - -### Đặc điểm nhiễu - -Phép đo RSSI Wi‑Fi thường có: - -- **Độ lệch chuẩn:** ±2–3 dBm do nhiễu ngẫu nhiên. -- **Trôi tín hiệu:** ±5 dBm trong nhiều giờ do thay đổi môi trường. -- **Ngoại lai:** khoảng 15% lần đọc có thể là đột biến. - -Bộ lọc Kalman và đường nền thích nghi giúp giảm ảnh hưởng của các hiện tượng này. +``` +WiFiSense/ +└── WiFiSense.ino ← Toàn bộ code trong 1 file +``` --- -## Tài liệu tham khảo +## Tác giả -1. **Đặc tính điện môi của mô người** - - Stogryn, A. (1986). *Equations for calculating the dielectric constant of saline water*. - - Độ hấp thụ của nước ở 2,4 GHz: ~0,15 dB/cm. - -2. **Truyền sóng Wi‑Fi trong môi trường trong nhà** - - Rappaport, T. S. (2002). *Wireless Communications: Principles and Practice*. - - Mô hình suy hao không gian tự do với các yếu tố môi trường. - -3. **Phát hiện bất thường thống kê** - - Chandola, V., và cộng sự (2009). *Anomaly Detection: A Survey*. - - Phương pháp Z-score cho ngoại lai đơn biến. +**Tony Trần** — [github.com/thanhtantran](https://github.com/thanhtantran) +**Orange Pi Việt Nam** — [orangepi.vn](https://orangepi.vn) --- -## Hướng phát triển +## License -Chưa triển khai, nhưng có thể thực hiện: - -- [ ] Phân loại bằng học máy (người, thú cưng, luồng gió) -- [ ] Nhiều ESP32-S3 để tam giác hóa -- [ ] Tích hợp home automation qua MQTT -- [ ] Ghi log đám mây về trạng thái có người -- [ ] Học máy huấn luyện theo từng phòng -- [ ] Biến thể Wi‑Fi 5 GHz (nhạy hơn nhưng khoảng cách ngắn hơn) - ---- - -## Source-Available License - -Copyright (c) 2026 Trần Thanh Tân / [TTAI Solutions Software](https://ttaisolutions.com/). - -All rights reserved. - -Được phép xem, tải xuống, cài đặt và sử dụng phần mềm này cho mục đích cá nhân, giáo dục và sử dụng nội bộ trong tổ chức. - -Được phép sửa đổi cho mục đích cá nhân hoặc sử dụng nội bộ. - -Nếu chưa có sự cho phép trước bằng văn bản của chủ sở hữu bản quyền, bạn không được: - -1. Bán, bán lại, cấp phép lại hoặc phân phối thương mại phần mềm này hay bất kỳ phần đáng kể nào trong mã nguồn của nó. -2. Cung cấp phần mềm này, dù đã sửa đổi hay chưa, dưới dạng sản phẩm thương mại, dịch vụ lưu trữ hoặc dịch vụ SaaS trả phí. -3. Đóng gói phần mềm này cùng phần cứng hoặc sản phẩm khác để bán lại. -4. Xóa hoặc thay đổi thông báo bản quyền, hoặc nhận mình là tác giả của phần mềm gốc. - -Mọi yêu cầu cấp phép thương mại vui lòng gửi tới: https://ttaisolutions.com - ---- - -## Danh sách kiểm tra nhanh - -- [ ] ESP32-S3 đã kết nối và cài driver USB -- [ ] Arduino IDE đã cài ESP32 board support -- [ ] Đã cập nhật thông tin Wi‑Fi trong code -- [ ] Firmware đã nạp thành công -- [ ] Serial Monitor có dữ liệu -- [ ] LED phản hồi khi có chuyển động -- [ ] Thiết bị được đặt tối ưu, cách router 3–5 m -- [ ] Đã để thiết bị hiệu chuẩn trong môi trường yên tĩnh -- [ ] Đã thử đi qua giữa router và ESP32-S3 -- [ ] OLED hiển thị trang trạng thái WiFiSense - ---- - -**Cần hỗ trợ?** Xem phần khắc phục sự cố trước, sau đó kiểm tra lại vị trí đặt thiết bị — đây là yếu tố quyết định lớn đến độ chính xác. - -**Chúc bạn phát hiện hiệu quả!** +MIT License — xem file `LICENSE` để biết thêm chi tiết. diff --git a/WiFiSense.ino b/WiFiSense.ino index 1d58f34..595f695 100644 --- a/WiFiSense.ino +++ b/WiFiSense.ino @@ -1,490 +1,661 @@ /* - * WiFiSense ESP32-S3 — OLED Edition - * OLED: 0.96" SSD1306 128×64 I2C - * SDA → GPIO 6 - * SCL → GPIO 7 + * 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 (Library Manager): - * • Adafruit SSD1306 (by Adafruit) - * • Adafruit GFX Library (by Adafruit) + * 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 -// ─── Cấu hình Wi-Fi ────────────────────────────────────────────────────────── -const char* ssid = "OrangePiVietnam"; -const char* password = "orangepi.vn"; +// ═══════════════════════════════════════════════════════════════════════════════ +// Hằng số cấu hình +// ═══════════════════════════════════════════════════════════════════════════════ // ─── 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_WIDTH 128 +#define OLED_HEIGHT 64 +#define OLED_RESET -1 +#define OLED_ADDRESS 0x3C +#define OLED_SDA_PIN 6 +#define OLED_SCL_PIN 7 -#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; -// ─── 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 ─────────────────────────────────────────────────────────── +// ─── 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 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 +// ─── Biến WiFiManager ──────────────────────────────────────────────────────── +bool apMode = false; +uint8_t reconnectAttempts = 0; +String savedSsid; +String savedPass; -// ═════════════════════════════════════════════════════════════════════════════ -// Hàm tiện ích -// ═════════════════════════════════════════════════════════════════════════════ +// ─── Log OLED cuộn ─────────────────────────────────────────────────────────── +#define LOG_LINES 8 +char logBuf[LOG_LINES][22]; +uint8_t logHead = 0; -float clampValue(float value, float lower, float upper) { - return value < lower ? lower : (value > upper ? upper : value); +// ═══════════════════════════════════════════════════════════════════════════════ +// 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; } - -// ─── 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; +void setDetectionLed(bool on) { + digitalWrite(LED_PIN, on ? HIGH : LOW); } -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; +// ═══════════════════════════════════════════════════════════════════════════════ +// ── 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) { - int idx = (logHead + i) % LOG_LINES; display.setCursor(0, i * 8); - display.print(logBuf[idx]); + display.print(logBuf[(logHead + i) % LOG_LINES]); } display.display(); } -// ═════════════════════════════════════════════════════════════════════════════ -// Wi-Fi & Calibration (với OLED log) -// ═════════════════════════════════════════════════════════════════════════════ +// ── 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; -bool connectToWifi() { WiFi.mode(WIFI_STA); - WiFi.begin(ssid, password); + WiFi.begin(ssid.c_str(), pass.isEmpty() ? nullptr : pass.c_str()); 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; + 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() - startedAt >= WIFI_CONNECT_TIMEOUT_MS) { - logPush("Timeout! Restart"); + if (millis() - t0 >= WIFI_CONNECT_TIMEOUT_MS) { + logPush("Timeout!"); logRender(); - Serial.println("\nTimeout. Restarting..."); + Serial.println("[WiFi] Timeout."); return false; } delay(500); - Serial.print('.'); - - dotCount++; - if (dotCount % 10 == 0) { - snprintf(tmp, sizeof(tmp), "Wait %lus...", (millis() - startedAt) / 1000UL); - logPush(tmp); - logRender(); + 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("\nConnected. IP: %s, RSSI: %d dBm\n", - WiFi.localIP().toString().c_str(), WiFi.RSSI()); - delay(800); + 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), "Calibrating %u smpl", CALIBRATION_SAMPLES); + snprintf(tmp, sizeof(tmp), "Cal %u samples", CALIBRATION_SAMPLES); logPush(tmp); logPush("Keep room EMPTY!"); logRender(); - Serial.printf("Calibrating %u samples; keep room empty...\n", CALIBRATION_SAMPLES); - - float calibrationMean = 0.0f; + float mean = 0.0f; for (uint16_t i = 0; i < CALIBRATION_SAMPLES; ++i) { - calibrationMean += static_cast(WiFi.RSSI()); + mean += (float)WiFi.RSSI(); delay(30); - - // Cập nhật tiến trình mỗi 25 mẫu - if (i % 25 == 0) { + if (i % 40 == 0) { snprintf(tmp, sizeof(tmp), "Cal %d/%d", i, CALIBRATION_SAMPLES); - logPush(tmp); - logRender(); + logPush(tmp); logRender(); } } - - baseline = calibrationMean / static_cast(CALIBRATION_SAMPLES); + baseline = mean / (float)CALIBRATION_SAMPLES; kalmanEstimate = baseline; smoothedRssi = baseline; initializeRssiWindows(baseline); - snprintf(tmp, sizeof(tmp), "Base: %.2f dBm", baseline); + snprintf(tmp, sizeof(tmp), "Base:%.2f dBm", baseline); logPush(tmp); - logPush("Cal done! Starting."); + logPush("Done! Starting..."); logRender(); delay(1000); - - Serial.printf("Calibration done. Baseline: %.2f dBm\n", baseline); + Serial.printf("[Cal] Baseline=%.2f dBm\n", baseline); } -// ═════════════════════════════════════════════════════════════════════════════ -// setup() & loop() -// ═════════════════════════════════════════════════════════════════════════════ - +// ═══════════════════════════════════════════════════════════════════════════════ +// setup() +// ═══════════════════════════════════════════════════════════════════════════════ void setup() { Serial.begin(115200); pinMode(LED_PIN, OUTPUT); setDetectionLed(false); - // Khởi OLED trước tiên + // OLED 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 + Serial.println("[OLED] Init failed — check wiring!"); } display.cp437(true); display.setTextWrap(false); - oledSplash(); - Serial.println("\n========== WiFiSense ESP32-S3 OLED =========="); + Serial.println("\n===== WiFiSense ESP32-S3 v3 ====="); - if (!connectToWifi()) { - delay(2'000); - ESP.restart(); + // Đọ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 } - calibrate(); - Serial.println("Running. RSSI metrics active."); - display.clearDisplay(); - display.display(); - lastPageSwitch = millis(); + 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!"); - logPush("Reconnecting..."); - logRender(); - if (!connectToWifi()) { - delay(2'000); + 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; } - // ── Lấy mẫu ──────────────────────────────────────────────────────────── + reconnectAttempts = 0; + + // ── Lấy mẫu ───────────────────────────────────────────────────────────── const int rawRssi = WiFi.RSSI(); - const float kalmanFiltered = kalmanFilter(static_cast(rawRssi)); + const float kalmanFiltered = kalmanFilter((float)rawRssi); const float smoothFiltered = exponentialSmoothing(kalmanFiltered); - rssiWindow[windowIndex] = smoothFiltered; + rssiWindow[windowIndex] = smoothFiltered; rssiLongWindow[longWindowIndex] = smoothFiltered; - windowIndex = (windowIndex + 1) % WINDOW_SIZE; + windowIndex = (windowIndex + 1) % WINDOW_SIZE; longWindowIndex = (longWindowIndex + 1) % LONG_WINDOW; ++sampleCount; - // ── Tính toán ─────────────────────────────────────────────────────────── + // ── Tính toán ──────────────────────────────────────────────────────────── const float variance = analyzeWindowVariance(); const float longVariance = analyzeLongTermVariance(); const float rateOfChange = detectRateOfChange(); @@ -493,39 +664,23 @@ void loop() { 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; + 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 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; - } + 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(); - // } + // ── Hiển thị ───────────────────────────────────────────────────────────── + drawPageMain(rawRssi, confidence, (int)signalQuality, variance, rateOfChange, detected); - 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); + // ── 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); }