Phát hiện chuyển động bằng sóng Wi-Fi (CSI) trên ESP32: không cần cảm biến
Giải thích Wi-Fi CSI và hướng dẫn dùng ESP32 phát hiện người di chuyển trong phòng chỉ bằng sóng Wi-Fi từ router: lấy dữ liệu CSI, tính điểm chuyển động, đặt ngưỡng và xem trên Serial Plotter.
ESP32 có thể phát hiện người di chuyển trong phòng mà không cần gắn thêm cảm biến nào, chỉ bằng cách theo dõi tín hiệu Wi-Fi nhận từ router. Kỹ thuật này dựa trên CSI (Channel State Information, thông tin trạng thái kênh): khi có người đi lại, đường truyền của sóng Wi-Fi thay đổi và ESP32 đo được sự thay đổi đó. Bài viết giải thích nguyên lý, cách lấy dữ liệu CSI trên Arduino IDE và một thuật toán đơn giản để báo có chuyển động.
Đây là bài thử nghiệm nâng cao. Kết quả phụ thuộc nhiều vào căn phòng, router và vị trí đặt mạch; kỹ thuật này dùng để học và thử nghiệm, chưa thay được cảm biến chuyên dụng trong các ứng dụng cần độ tin cậy.
1. Sóng Wi-Fi phát hiện chuyển động thế nào?
- Trong nhà, sóng Wi-Fi từ router tới ESP32 theo nhiều đường cùng lúc: đường thẳng và các đường phản xạ từ tường, trần, đồ đạc, cơ thể người.
- Wi-Fi chia kênh truyền thành nhiều sóng mang con. Với mỗi khung nhận được, ESP32 ước lượng biên độ và pha của từng sóng mang con. Tập số liệu đó là CSI.
- Khi phòng yên tĩnh, CSI gần như không đổi từ khung này sang khung khác. Khi có người di chuyển, các đường phản xạ đổi theo và biên độ CSI dao động.
- So với RSSI chỉ là một con số cho cả khung, CSI có hàng chục giá trị nên nhạy với thay đổi nhỏ hơn nhiều.
2. Dữ liệu CSI trên ESP32
- Bài dùng phần CSI lấy từ trường LLTF của khung, có ở mọi khung Wi-Fi kiểu OFDM. Phần này gồm 64 sóng mang con, mỗi sóng mang hai byte có dấu: phần ảo rồi phần thực.
- Thứ tự trong bộ đệm: sóng mang 0 đến 31, sau đó là -32 đến -1. Sóng mang ở giữa kênh và ở hai rìa kênh không mang dữ liệu nên có biên độ bằng 0.
- Trên ESP32, bốn byte đầu của bộ đệm không hợp lệ, nên chương trình bỏ qua hai sóng mang đầu.
- ESP32 chỉ có CSI khi nhận được khung. Để có dữ liệu đều đặn, chương trình ping router 20 lần mỗi giây; mỗi lần router trả lời là một khung có CSI.
- Chương trình chỉ dùng khung do chính router đang kết nối phát ra (so địa chỉ MAC), để dữ liệu của các thiết bị khác không lẫn vào.
3. Chuẩn bị và bố trí
- Một mạch ESP32 DevKit V1 30 chân (hoặc bản dùng chip CH340) và cáp USB có truyền dữ liệu.
- Một router Wi-Fi 2,4 GHz. Không cần nối thêm linh kiện nào.
- Arduino IDE 2.x với gói esp32 by Espressif Systems (bài dùng phiên bản 3.3.10). Không cần cài thêm thư viện.
Bố trí: đặt ESP32 cách router khoảng 2 đến 5 m, sao cho khu vực cần theo dõi nằm giữa hoặc gần đường nối hai thiết bị. Đặt cả hai cố định, cao hơn mặt bàn, không để sát vật kim loại lớn. Mỗi lần dời mạch hoặc router cần hiệu chuẩn lại.
4. Chương trình 1: xem dữ liệu CSI
Sửa tên Wi-Fi và mật khẩu rồi nạp chương trình. Mỗi giây chương trình in số khung CSI nhận được và biên độ của sáu sóng mang con:
/*
PYWORLD.VN - Wi-Fi CSI trên ESP32: xem dữ liệu trạng thái kênh
ESP32 kết nối router, ping router 20 lần mỗi giây và in biên độ CSI của vài sóng mang con
Chỉ cần mạch ESP32 và một router Wi-Fi 2,4 GHz
*/
#include <WiFi.h>
#include <esp_wifi.h>
#include <ping/ping_sock.h>
#include <lwip/ip_addr.h>
const char *WIFI_SSID = "TEN_WIFI";
const char *WIFI_PASS = "MAT_KHAU_WIFI";
#define SO_SONG_MANG 64 // Phần LLTF của một khung có 64 sóng mang con
const uint32_t CHU_KY_PING_MS = 50; // 20 lần mỗi giây
uint8_t macRouter[6];
float bienDo[SO_SONG_MANG];
volatile int rssiMoi = 0;
volatile uint32_t soKhung = 0;
portMUX_TYPE khoaDuLieu = portMUX_INITIALIZER_UNLOCKED;
// Được gọi mỗi khi ESP32 nhận một khung Wi-Fi có kèm CSI. Chạy trong tác vụ Wi-Fi nên phải ngắn gọn
void khiCoCsi(void *thamSo, wifi_csi_info_t *thongTin) {
if (thongTin == NULL || thongTin->buf == NULL || thongTin->len < 2 * SO_SONG_MANG) {
return;
}
if (memcmp(thongTin->mac, macRouter, 6) != 0) {
return; // Chỉ dùng khung do router đang kết nối phát ra
}
// Mỗi sóng mang con là hai byte có dấu: phần ảo rồi phần thực
const int8_t *duLieu = thongTin->buf;
portENTER_CRITICAL(&khoaDuLieu);
for (int k = 0; k < SO_SONG_MANG; k++) {
float phanAo = duLieu[2 * k];
float phanThuc = duLieu[2 * k + 1];
bienDo[k] = sqrtf(phanAo * phanAo + phanThuc * phanThuc);
}
portEXIT_CRITICAL(&khoaDuLieu);
rssiMoi = thongTin->rx_ctrl.rssi;
soKhung = soKhung + 1;
}
// Ping router đều đặn để router trả lời, nhờ đó có khung Wi-Fi đến với nhịp ổn định
void batDauPing() {
IPAddress cong = WiFi.gatewayIP();
esp_ping_config_t cauHinh = ESP_PING_DEFAULT_CONFIG();
IP_ADDR4(&cauHinh.target_addr, cong[0], cong[1], cong[2], cong[3]);
cauHinh.count = ESP_PING_COUNT_INFINITE;
cauHinh.interval_ms = CHU_KY_PING_MS;
esp_ping_callbacks_t hamGoiLai = {};
esp_ping_handle_t phienPing = NULL;
if (esp_ping_new_session(&cauHinh, &hamGoiLai, &phienPing) == ESP_OK) {
esp_ping_start(phienPing);
}
}
void batCsi() {
wifi_csi_config_t cauHinh = {};
cauHinh.lltf_en = true; // Lấy CSI từ phần LLTF, có ở mọi khung OFDM
cauHinh.htltf_en = true;
cauHinh.stbc_htltf2_en = true;
cauHinh.ltf_merge_en = true;
cauHinh.channel_filter_en = true; // Làm mượt giữa các sóng mang con kề nhau
cauHinh.manu_scale = false;
esp_wifi_set_csi_config(&cauHinh);
esp_wifi_set_csi_rx_cb(khiCoCsi, NULL);
esp_wifi_set_csi(true);
}
void setup() {
Serial.begin(115200);
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASS);
Serial.printf("Dang ket noi WiFi %s", WIFI_SSID);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.printf("\nDa ket noi, kenh %d, IP: %s\n", WiFi.channel(), WiFi.localIP().toString().c_str());
memcpy(macRouter, WiFi.BSSID(), 6);
batCsi();
batDauPing();
}
void loop() {
static uint32_t soKhungTruoc = 0;
delay(1000);
float banSao[SO_SONG_MANG];
portENTER_CRITICAL(&khoaDuLieu);
memcpy(banSao, bienDo, sizeof(banSao));
portEXIT_CRITICAL(&khoaDuLieu);
uint32_t soKhungHienTai = soKhung;
Serial.printf("Khung/giay: %lu | RSSI %d dBm | bien do song mang 6, 12, 20, 44, 52, 58: %.1f %.1f %.1f %.1f %.1f %.1f\n",
(unsigned long)(soKhungHienTai - soKhungTruoc), rssiMoi,
banSao[6], banSao[12], banSao[20], banSao[44], banSao[52], banSao[58]);
soKhungTruoc = soKhungHienTai;
}
Khi phòng yên tĩnh, sáu con số biên độ thay đổi rất ít giữa các dòng. Thử đi lại giữa router và mạch: các con số nhảy rõ rệt. Dòng Khung/giay nên ở quanh 20; nếu bằng 0, xem mục xử lý lỗi.
5. Cách tính điểm chuyển động
- Biên độ: với mỗi sóng mang con, biên độ bằng căn bậc hai của tổng bình phương phần thực và phần ảo.
- Chuẩn hóa: ESP32 tự chỉnh độ lợi máy thu theo từng khung nên biên độ tuyệt đối lúc lớn lúc nhỏ. Chia mỗi biên độ cho trung bình của cả khung để chỉ còn lại hình dạng của đường bao.
- Phương sai trượt: với từng sóng mang con, chương trình giữ một trung bình trượt và một phương sai trượt. Phòng yên tĩnh thì phương sai nhỏ; có chuyển động thì phương sai tăng.
- Điểm chuyển động: lấy trung bình độ lệch chuẩn của mọi sóng mang con, nhân 100 để ra đơn vị phần trăm.
- Ngưỡng: trong 10 giây đầu, chương trình đo mức nền lúc phòng yên tĩnh rồi đặt ngưỡng bằng hai lần mức nền.
6. Chương trình 2: phát hiện chuyển động
/*
PYWORLD.VN - Phát hiện chuyển động bằng Wi-Fi CSI trên ESP32, không cần cảm biến
Tính "điểm chuyển động" từ độ dao động biên độ CSI; LED GPIO2 sáng khi điểm vượt ngưỡng
In dạng "diem:..,nguong:..,chuyen_dong:.." để xem bằng Serial Monitor hoặc Serial Plotter
*/
#include <WiFi.h>
#include <esp_wifi.h>
#include <ping/ping_sock.h>
#include <lwip/ip_addr.h>
const char *WIFI_SSID = "TEN_WIFI";
const char *WIFI_PASS = "MAT_KHAU_WIFI";
#define LED_PIN 2
#define SO_SONG_MANG 64
#define SONG_MANG_DAU 2 // Bỏ hai sóng mang đầu: bốn byte đầu của khung CSI trên ESP32 không hợp lệ
const uint32_t CHU_KY_PING_MS = 50; // 20 khung mỗi giây
const float HE_SO_LAM_MUOT = 0.05; // Càng nhỏ càng mượt nhưng phản ứng càng chậm
const float HE_SO_NGUONG = 2.0; // Ngưỡng = mức nền lúc phòng yên tĩnh x hệ số này
const float NGUONG_TOI_THIEU = 0.5;
const unsigned long THOI_GIAN_HIEU_CHUAN_MS = 10000;
const unsigned long THOI_GIAN_GIU_MS = 2000; // Giữ trạng thái "có chuyển động" thêm 2 giây
uint8_t macRouter[6];
float giaTriTb[SO_SONG_MANG]; // Trung bình trượt của biên độ đã chuẩn hóa
float phuongSai[SO_SONG_MANG]; // Phương sai trượt
bool daKhoiTao = false;
volatile float diemChuyenDong = 0;
volatile uint32_t soKhung = 0;
unsigned long lucBatDauHieuChuan = 0;
void khiCoCsi(void *thamSo, wifi_csi_info_t *thongTin) {
if (thongTin == NULL || thongTin->buf == NULL || thongTin->len < 2 * SO_SONG_MANG) {
return;
}
if (memcmp(thongTin->mac, macRouter, 6) != 0) {
return;
}
// Bước 1: biên độ từng sóng mang con và trung bình của cả khung
const int8_t *duLieu = thongTin->buf;
float bienDo[SO_SONG_MANG];
float tong = 0;
int soHopLe = 0;
for (int k = SONG_MANG_DAU; k < SO_SONG_MANG; k++) {
float phanAo = duLieu[2 * k];
float phanThuc = duLieu[2 * k + 1];
bienDo[k] = sqrtf(phanAo * phanAo + phanThuc * phanThuc);
if (bienDo[k] > 0) {
tong += bienDo[k];
soHopLe++;
}
}
if (soHopLe < 20) {
return;
}
float trungBinhKhung = tong / soHopLe;
// Bước 2: chuẩn hóa theo trung bình khung để loại ảnh hưởng của bộ tự chỉnh độ lợi,
// rồi cập nhật trung bình trượt và phương sai trượt của từng sóng mang con
float tongDoLech = 0;
for (int k = SONG_MANG_DAU; k < SO_SONG_MANG; k++) {
if (bienDo[k] <= 0) {
continue; // Sóng mang rỗng ở giữa và hai rìa kênh
}
float x = bienDo[k] / trungBinhKhung;
if (!daKhoiTao) {
giaTriTb[k] = x;
phuongSai[k] = 0;
}
float lech = x - giaTriTb[k];
giaTriTb[k] += HE_SO_LAM_MUOT * lech;
phuongSai[k] = (1.0f - HE_SO_LAM_MUOT) * (phuongSai[k] + HE_SO_LAM_MUOT * lech * lech);
tongDoLech += sqrtf(phuongSai[k]);
}
daKhoiTao = true;
// Bước 3: điểm chuyển động = độ lệch chuẩn trung bình của các sóng mang con, tính theo phần trăm
diemChuyenDong = 100.0f * tongDoLech / soHopLe;
soKhung = soKhung + 1;
}
void batDauPing() {
IPAddress cong = WiFi.gatewayIP();
esp_ping_config_t cauHinh = ESP_PING_DEFAULT_CONFIG();
IP_ADDR4(&cauHinh.target_addr, cong[0], cong[1], cong[2], cong[3]);
cauHinh.count = ESP_PING_COUNT_INFINITE;
cauHinh.interval_ms = CHU_KY_PING_MS;
esp_ping_callbacks_t hamGoiLai = {};
esp_ping_handle_t phienPing = NULL;
if (esp_ping_new_session(&cauHinh, &hamGoiLai, &phienPing) == ESP_OK) {
esp_ping_start(phienPing);
}
}
void batCsi() {
wifi_csi_config_t cauHinh = {};
cauHinh.lltf_en = true;
cauHinh.htltf_en = true;
cauHinh.stbc_htltf2_en = true;
cauHinh.ltf_merge_en = true;
cauHinh.channel_filter_en = true;
cauHinh.manu_scale = false;
esp_wifi_set_csi_config(&cauHinh);
esp_wifi_set_csi_rx_cb(khiCoCsi, NULL);
esp_wifi_set_csi(true);
}
void setup() {
Serial.begin(115200);
pinMode(LED_PIN, OUTPUT);
WiFi.mode(WIFI_STA);
WiFi.setSleep(false); // Tắt chế độ tiết kiệm điện của Wi-Fi để khung đến đều
WiFi.begin(WIFI_SSID, WIFI_PASS);
Serial.printf("Dang ket noi WiFi %s", WIFI_SSID);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.printf("\nDa ket noi, kenh %d\n", WiFi.channel());
memcpy(macRouter, WiFi.BSSID(), 6);
batCsi();
batDauPing();
lucBatDauHieuChuan = millis();
Serial.println("Hieu chuan 10 giay: giu phong yen tinh, khong di lai");
}
void loop() {
static float tongNen = 0;
static int soMauNen = 0;
static float nguong = 0;
static bool dangChuyenDong = false;
static unsigned long lanVuotNguong = 0;
delay(200);
float diem = diemChuyenDong;
// Giai đoạn hiệu chuẩn: đo mức nền khi phòng yên tĩnh (bỏ 2 giây đầu để bộ lọc ổn định)
if (nguong == 0) {
unsigned long daQua = millis() - lucBatDauHieuChuan;
if (daQua > 2000 && soKhung > 40) {
tongNen += diem;
soMauNen++;
}
if (daQua > THOI_GIAN_HIEU_CHUAN_MS + 2000 && soMauNen > 0) {
float mucNen = tongNen / soMauNen;
nguong = max(mucNen * HE_SO_NGUONG, NGUONG_TOI_THIEU);
Serial.printf("Muc nen %.2f, nguong %.2f\n", mucNen, nguong);
}
return;
}
if (diem > nguong) {
lanVuotNguong = millis();
dangChuyenDong = true;
} else if (dangChuyenDong && millis() - lanVuotNguong > THOI_GIAN_GIU_MS) {
dangChuyenDong = false;
}
digitalWrite(LED_PIN, dangChuyenDong ? HIGH : LOW);
Serial.printf("diem:%.2f,nguong:%.2f,chuyen_dong:%.2f\n", diem, nguong, dangChuyenDong ? nguong : 0.0f);
}
7. Kết quả mong đợi
- Nạp chương trình, mở Tools > Serial Monitor ở 115200 baud và rời khỏi vùng giữa router với mạch trong 12 giây đầu.
- Khi thấy dòng
Muc nen ..., nguong ..., đóng Serial Monitor và mở Tools > Serial Plotter để xem ba đường:diem,nguongvàchuyen_dong. - Đi lại trong phòng: đường
diemvọt lên trênnguong, LED trên mạch (GPIO2) sáng. Đứng yên khoảng 2 giây: LED tắt.
8. Tinh chỉnh
| Hằng số | Mặc định | Tác dụng khi thay đổi |
|---|---|---|
HE_SO_NGUONG | 2.0 | Tăng lên 3 hoặc 4 nếu hay báo giả; giảm xuống 1.5 nếu bỏ sót chuyển động nhỏ |
HE_SO_LAM_MUOT | 0.05 | Nhỏ hơn thì đường điểm mượt hơn nhưng phản ứng chậm hơn |
THOI_GIAN_GIU_MS | 2000 | Thời gian giữ trạng thái có chuyển động sau lần vượt ngưỡng cuối |
CHU_KY_PING_MS | 50 | Giảm xuống 20 để có 50 khung mỗi giây, phản ứng nhanh hơn nhưng chiếm mạng hơn |
THOI_GIAN_HIEU_CHUAN_MS | 10000 | Hiệu chuẩn lâu hơn cho mức nền ổn định hơn |
9. Giới hạn cần biết
- Phụ thuộc môi trường: quạt đang quay, rèm bay, thú nuôi, cửa mở ra đóng vào đều làm CSI thay đổi và có thể bị báo là chuyển động.
- Xuyên tường: sóng Wi-Fi đi qua tường nên người đi lại ở phòng bên hoặc hành lang cũng có thể làm điểm tăng.
- Người đứng yên: giống PIR, cách tính trong bài chỉ nhận ra chuyển động, không nhận ra người ngồi yên.
- Phụ thuộc router: router đổi kênh, đổi công suất hoặc có thiết bị khác truyền dữ liệu nhiều làm mức nền thay đổi. Khi đó cần khởi động lại mạch để hiệu chuẩn lại.
- Không dùng cho an ninh: kỹ thuật này có cả báo giả lẫn bỏ sót. Với đèn tự bật hoặc báo động, relay nên được điều khiển bằng cảm biến chuyên dụng, hoặc kết hợp cả hai nguồn tín hiệu.
10. Xử lý lỗi thường gặp
| Hiện tượng | Nguyên nhân thường gặp | Cách xử lý |
|---|---|---|
Dừng ở dòng Dang ket noi WiFi | Sai tên, mật khẩu hoặc router chỉ phát 5 GHz | Kiểm tra lại; ESP32 chỉ dùng Wi-Fi 2,4 GHz. Xem bài kết nối Wi-Fi trên ESP32 |
Khung/giay bằng 0 hoặc rất thấp | Router không trả lời ping, hoặc đang trả lời bằng kiểu khung không có CSI | Sửa hàm batDauPing() để ping một máy khác đang bật trong cùng mạng; thử router khác |
Không bao giờ in dòng Muc nen | Không nhận được khung CSI nào nên chưa hiệu chuẩn được | Chạy chương trình 1 trước để chắc có khung CSI |
| LED sáng liên tục dù phòng yên tĩnh | Có người đi lại lúc hiệu chuẩn nên mức nền sai, hoặc trong phòng có quạt đang quay | Nhấn nút EN để hiệu chuẩn lại khi phòng yên; tăng HE_SO_NGUONG |
| Đi lại mà điểm tăng rất ít | Người di chuyển ở xa đường truyền giữa router và mạch | Dời mạch để khu vực cần theo dõi nằm giữa router và mạch; giảm HE_SO_NGUONG |
Lỗi biên dịch ở các hàm esp_wifi_set_csi | Gói esp32 quá cũ hoặc chọn nhầm loại mạch | Cập nhật gói esp32; chọn đúng DOIT ESP32 DEVKIT V1 trong Tools > Board |
11. Xem thêm
- Kết nối Wi-Fi trên ESP32: quét mạng, tự kết nối lại, IP tĩnh.
- Dùng MQTT với ESP32: gửi trạng thái có chuyển động lên điện thoại.
- ESP32 điều khiển module relay 4 kênh an toàn.
- Các mạch ESP32 tại PYWORLD: danh mục ESP32.
Xem thêm