update
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
Source-Available License
|
||||
|
||||
Copyright (c) 2026 Tran Thanh Tan / TTAI Solutions Software.
|
||||
All rights reserved.
|
||||
|
||||
Permission is granted to view, download, install, and use this
|
||||
software for personal, educational, and internal organizational
|
||||
purposes.
|
||||
|
||||
Modification for personal or internal use is permitted.
|
||||
|
||||
Without prior written permission from the copyright holder,
|
||||
you may not:
|
||||
|
||||
1. Sell, resell, sublicense, or commercially distribute this
|
||||
software or any substantial portion of its source code.
|
||||
|
||||
2. Offer this software, whether modified or unmodified, as a
|
||||
commercial product, hosted service, or paid SaaS offering.
|
||||
|
||||
3. Bundle this software with hardware or other products for
|
||||
resale.
|
||||
|
||||
4. Remove or alter copyright notices or claim authorship of
|
||||
the original software.
|
||||
|
||||
Commercial licensing inquiries should be directed to:
|
||||
|
||||
https://ttaisolutions.com
|
||||
+45
-5
@@ -115,6 +115,30 @@ Once motion is detected, it must persist for **3 consecutive cycles** before the
|
||||
|
||||
---
|
||||
|
||||
## OLED Display
|
||||
|
||||
The firmware supports a **0.96-inch SSD1306 128×64 I2C OLED display** connected to the ESP32-S3 as follows:
|
||||
|
||||
| OLED pin | ESP32-S3 pin |
|
||||
|---|---|
|
||||
| VCC | 3.3V |
|
||||
| GND | GND |
|
||||
| SDA | GPIO 6 |
|
||||
| SCL | GPIO 7 |
|
||||
|
||||
The default I2C address is `0x3C`. The OLED uses the `Wire` interface initialized with `Wire.begin(6, 7)`.
|
||||
|
||||
Install these libraries using Arduino IDE's Library Manager:
|
||||
|
||||
- **Adafruit SSD1306** by Adafruit
|
||||
- **Adafruit GFX Library** by Adafruit
|
||||
|
||||
The main display page shows the Wi-Fi status, RSSI, confidence, signal quality, baseline, variance, and total detections. A technical detail page is implemented in the sketch, but page rotation is currently disabled in `loop()`, so the main page is displayed continuously.
|
||||
|
||||
If the display does not initialize, verify the 3.3V power, common ground, SDA/SCL wiring, and the `OLED_ADDRESS` value in `WiFiSense.ino`.
|
||||
|
||||
---
|
||||
|
||||
## Real-Time Outputs
|
||||
|
||||
### Serial Plotter (Graphs)
|
||||
@@ -225,8 +249,8 @@ To exceed 90% accuracy, you'd need:
|
||||
|
||||
3. **Install Required Libraries**
|
||||
- `Tools > Manage Libraries`
|
||||
- `WiFi.h` is included with ESP32 board support
|
||||
- No external libraries needed! ✓
|
||||
- `WiFi.h` and `Wire.h` are included with ESP32 board support
|
||||
- Install **Adafruit SSD1306** and **Adafruit GFX Library**
|
||||
|
||||
4. **Configure Board Settings**
|
||||
```
|
||||
@@ -417,9 +441,24 @@ Not implemented but possible:
|
||||
|
||||
---
|
||||
|
||||
## License
|
||||
## Source-Available License
|
||||
|
||||
This project is provided as-is for educational and personal use.
|
||||
Copyright (c) 2026 Tran Thanh Tan / [TTAI Solutions Software](https://ttaisolutions.com/).
|
||||
|
||||
All rights reserved.
|
||||
|
||||
Permission is granted to view, download, install, and use this software for personal, educational, and internal organizational purposes.
|
||||
|
||||
Modification for personal or internal use is permitted.
|
||||
|
||||
Without prior written permission from the copyright holder, you may not:
|
||||
|
||||
1. Sell, resell, sublicense, or commercially distribute this software or any substantial portion of its source code.
|
||||
2. Offer this software, whether modified or unmodified, as a commercial product, hosted service, or paid SaaS offering.
|
||||
3. Bundle this software with hardware or other products for resale.
|
||||
4. Remove or alter copyright notices or claim authorship of the original software.
|
||||
|
||||
Commercial licensing inquiries: https://ttaisolutions.com
|
||||
|
||||
---
|
||||
|
||||
@@ -433,7 +472,8 @@ This project is provided as-is for educational and personal use.
|
||||
- [ ] LED responding to movement
|
||||
- [ ] Placed in optimal position (3-5m from router)
|
||||
- [ ] Waited 2-3 minutes for calibration
|
||||
- [ ] Tested by walking past ESP8266
|
||||
- [ ] Tested by walking past the ESP32-S3
|
||||
- [ ] OLED displays the Wi-FiSense status page
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -124,6 +124,30 @@ Sau khi có chuyển động, điều kiện phải duy trì **3 chu kỳ liên
|
||||
|
||||
---
|
||||
|
||||
## Màn hình OLED
|
||||
|
||||
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:
|
||||
|
||||
| Chân OLED | Chân ESP32-S3 |
|
||||
|---|---|
|
||||
| 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`.
|
||||
|
||||
---
|
||||
|
||||
## Dữ liệu đầu ra thời gian thực
|
||||
|
||||
### Serial Plotter (đồ thị)
|
||||
@@ -244,8 +268,8 @@ Ví dụ dữ liệu thời gian thực:
|
||||
- Tìm **esp32** và cài **esp32 by Espressif Systems**.
|
||||
|
||||
3. **Thư viện cần thiết**
|
||||
- `WiFi.h` được cung cấp sẵn cùng ESP32 board support.
|
||||
- Không cần thư viện bên ngoài. ✓
|
||||
- `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**
|
||||
|
||||
@@ -445,9 +469,24 @@ Chưa triển khai, nhưng có thể thực hiện:
|
||||
|
||||
---
|
||||
|
||||
## Giấy phép
|
||||
## Source-Available License
|
||||
|
||||
Dự án được cung cấp nguyên trạng cho mục đích học tập và sử dụng cá nhân.
|
||||
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
|
||||
|
||||
---
|
||||
|
||||
@@ -462,6 +501,7 @@ Dự án được cung cấp nguyên trạng cho mục đích học tập và s
|
||||
- [ ] 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
|
||||
|
||||
---
|
||||
|
||||
|
||||
+406
-156
@@ -1,46 +1,83 @@
|
||||
/*
|
||||
* 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 <WiFi.h>
|
||||
#include <Wire.h>
|
||||
#include <Adafruit_GFX.h>
|
||||
#include <Adafruit_SSD1306.h>
|
||||
|
||||
// Replace these placeholders before uploading.
|
||||
const char* ssid = "YOUR_SSID";
|
||||
const char* password = "YOUR_PASSWORD";
|
||||
// ─── Cấu hình Wi-Fi ──────────────────────────────────────────────────────────
|
||||
const char* ssid = "OrangePiVietnam";
|
||||
const char* password = "orangepi.vn";
|
||||
|
||||
// ESP32-S3 boards do not share a universal built-in LED pin. GPIO 2 is a
|
||||
// conservative default for an external LED; change it for your board.
|
||||
constexpr int LED_PIN = 2;
|
||||
constexpr uint8_t LED_ON_LEVEL = HIGH;
|
||||
// ─── 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;
|
||||
|
||||
constexpr size_t WINDOW_SIZE = 40;
|
||||
constexpr size_t LONG_WINDOW = 100;
|
||||
constexpr uint16_t SAMPLE_INTERVAL_MS = 35;
|
||||
constexpr uint16_t CALIBRATION_SAMPLES = 200;
|
||||
// ─── 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;
|
||||
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;
|
||||
|
||||
float rssiWindow[WINDOW_SIZE];
|
||||
float rssiLongWindow[LONG_WINDOW];
|
||||
size_t windowIndex = 0;
|
||||
// ─── 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;
|
||||
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;
|
||||
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);
|
||||
@@ -50,10 +87,10 @@ 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;
|
||||
@@ -64,207 +101,400 @@ float exponentialSmoothing(float measurement) {
|
||||
return smoothedRssi;
|
||||
}
|
||||
|
||||
// ─── Thống kê ────────────────────────────────────────────────────────────────
|
||||
float standardDeviation(const float* values, size_t count) {
|
||||
if (count == 0) {
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
if (count == 0) return 0.0f;
|
||||
float mean = 0.0f;
|
||||
for (size_t i = 0; i < count; ++i) {
|
||||
mean += values[i];
|
||||
}
|
||||
for (size_t i = 0; i < count; ++i) mean += values[i];
|
||||
mean /= static_cast<float>(count);
|
||||
|
||||
float variance = 0.0f;
|
||||
for (size_t i = 0; i < count; ++i) {
|
||||
const float difference = values[i] - mean;
|
||||
variance += difference * difference;
|
||||
const float d = values[i] - mean;
|
||||
variance += d * d;
|
||||
}
|
||||
return sqrtf(variance / static_cast<float>(count));
|
||||
}
|
||||
|
||||
float analyzeWindowVariance() {
|
||||
return standardDeviation(rssiWindow, WINDOW_SIZE);
|
||||
}
|
||||
|
||||
float analyzeLongTermVariance() {
|
||||
return standardDeviation(rssiLongWindow, LONG_WINDOW);
|
||||
}
|
||||
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;
|
||||
float oldMean = 0.0f;
|
||||
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)];
|
||||
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);
|
||||
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;
|
||||
}
|
||||
|
||||
if (sampleCount < WINDOW_SIZE) return 0.0f;
|
||||
float mean = 0.0f;
|
||||
for (size_t i = 0; i < WINDOW_SIZE; ++i) {
|
||||
mean += rssiWindow[i];
|
||||
}
|
||||
for (size_t i = 0; i < WINDOW_SIZE; ++i) mean += rssiWindow[i];
|
||||
mean /= static_cast<float>(WINDOW_SIZE);
|
||||
|
||||
const float stdDev = fmaxf(analyzeWindowVariance(), 0.1f);
|
||||
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 confidence = variance > FAST_MOVEMENT_THRESHOLD ? 75.0f
|
||||
: variance > SLOW_MOVEMENT_THRESHOLD ? 50.0f
|
||||
: 15.0f;
|
||||
if (rateOfChange > 2.0f) confidence += 15.0f;
|
||||
if (peak > PEAK_THRESHOLD) confidence += 10.0f;
|
||||
if (zScore > Z_SCORE_THRESHOLD) confidence += 10.0f;
|
||||
return static_cast<int>(clampValue(confidence, 0.0f, 100.0f));
|
||||
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<int>(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 > FAST_MOVEMENT_THRESHOLD) return "FAST";
|
||||
if (variance > SLOW_MOVEMENT_THRESHOLD && rateOfChange > 1.0f) return "WALKING";
|
||||
if (variance > SLOW_MOVEMENT_THRESHOLD) return "SLOW";
|
||||
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;
|
||||
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;
|
||||
}
|
||||
|
||||
bool connectToWifi() {
|
||||
WiFi.mode(WIFI_STA);
|
||||
WiFi.begin(ssid, password);
|
||||
|
||||
Serial.print("Connecting to Wi-Fi");
|
||||
const uint32_t startedAt = millis();
|
||||
while (WiFi.status() != WL_CONNECTED) {
|
||||
if (millis() - startedAt >= WIFI_CONNECT_TIMEOUT_MS) {
|
||||
Serial.println("\nWi-Fi connection timed out. Restarting...");
|
||||
return false;
|
||||
}
|
||||
delay(500);
|
||||
Serial.print('.');
|
||||
}
|
||||
|
||||
Serial.printf("\nConnected. IP: %s, RSSI: %d dBm\n", WiFi.localIP().toString().c_str(), WiFi.RSSI());
|
||||
return true;
|
||||
}
|
||||
|
||||
void calibrate() {
|
||||
Serial.printf("Calibrating from %u RSSI samples; keep the room empty...\n", CALIBRATION_SAMPLES);
|
||||
float calibrationMean = 0.0f;
|
||||
for (uint16_t i = 0; i < CALIBRATION_SAMPLES; ++i) {
|
||||
const float rssi = static_cast<float>(WiFi.RSSI());
|
||||
calibrationMean += rssi;
|
||||
delay(30);
|
||||
}
|
||||
|
||||
baseline = calibrationMean / static_cast<float>(CALIBRATION_SAMPLES);
|
||||
kalmanEstimate = baseline;
|
||||
smoothedRssi = baseline;
|
||||
initializeRssiWindows(baseline);
|
||||
Serial.printf("Calibration complete. Baseline: %.2f dBm\n", baseline);
|
||||
sampleCount = WINDOW_SIZE;
|
||||
}
|
||||
|
||||
void setDetectionLed(bool detected) {
|
||||
digitalWrite(LED_PIN, detected ? LED_ON_LEVEL : LED_OFF_LEVEL);
|
||||
}
|
||||
|
||||
void printMeasurements(
|
||||
int rawRssi,
|
||||
float kalmanFiltered,
|
||||
float smoothFiltered,
|
||||
float variance,
|
||||
float zScore,
|
||||
int confidence,
|
||||
float signalQuality,
|
||||
float rateOfChange) {
|
||||
Serial.printf(
|
||||
"Raw:%d,Kalman:%.2f,Smooth:%.2f,Baseline:%.2f,Variance:%.2f,ZScore:%.2f,Confidence:%d\n",
|
||||
rawRssi, kalmanFiltered, smoothFiltered, baseline, variance, zScore, confidence);
|
||||
Serial.printf(
|
||||
"[%s] Conf:%d%% | Quality:%d%% | Rate:%.1f | Total:%lu\n",
|
||||
getMotionIntensity(variance, rateOfChange), confidence, static_cast<int>(signalQuality), rateOfChange,
|
||||
static_cast<unsigned long>(totalDetections));
|
||||
// ═════════════════════════════════════════════════════════════════════════════
|
||||
// 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<float>(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<float>(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);
|
||||
|
||||
Serial.println("\n========== WiFiSense ESP32-S3 ==========");
|
||||
// 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("Open Tools > Serial Plotter to view RSSI metrics.");
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
const int rawRssi = WiFi.RSSI();
|
||||
// ── Lấy mẫu ────────────────────────────────────────────────────────────
|
||||
const int rawRssi = WiFi.RSSI();
|
||||
const float kalmanFiltered = kalmanFilter(static_cast<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;
|
||||
|
||||
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);
|
||||
// ── 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;
|
||||
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);
|
||||
@@ -276,6 +506,26 @@ void loop() {
|
||||
}
|
||||
setDetectionLed(detected);
|
||||
|
||||
printMeasurements(rawRssi, kalmanFiltered, smoothFiltered, variance, zScore, confidence, signalQuality, rateOfChange);
|
||||
// ── 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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user