Công tắc cảm ứng thông minh âm tường tự làm với ESP32: dây trung tính, chống bật nhầm, an toàn 220 V
Tự làm công tắc cảm ứng thông minh bằng ESP32: kiểm tra dây trung tính trong hộp âm tường, sơ đồ khối an toàn 220 V, thuật toán chạm theo giá trị nền, nhớ trạng thái, điều khiển Wi-Fi.
Công tắc cảm ứng thông minh là công tắc đèn không có nút bấm cơ, chạm tay lên mặt nhựa là đổi trạng thái, và điều khiển được qua điện thoại. Tự làm bằng ESP32 là một dự án thú vị, nhưng có hai điểm phải biết trước khi mua linh kiện: hộp âm tường nhà bạn có dây trung tính không, và phần 220 V phải làm đúng an toàn. Bài này đi qua hai điểm đó, thuật toán chạm chống bật nhầm kèm mô phỏng, và chương trình đầy đủ cho ESP32.
Cảnh báo an toàn: điện 220 V gây chết người và cháy nổ. Luôn cắt aptomat và kiểm tra lại bằng bút thử điện trước khi đụng vào dây. Không cắm USB vào ESP32 khi mạch đang nối với lưới điện. Mạch tự làm không có chứng nhận an toàn như sản phẩm thương mại; nếu chưa quen làm điện, hãy thử phần điện áp thấp với LED trước và nhờ thợ điện cho phần lắp đặt. Nên đọc bài điện một chiều, xoay chiều và an toàn điện 220 V trước.
1. Kiểm tra dây trung tính trước tiên

Công tắc cơ chỉ nối tiếp vào dây pha, nên hộp âm tường trong nhiều nhà chỉ có 2 dây: L đi vào và L đi ra đèn. ESP32 cần nguồn liên tục kể cả khi đèn tắt, mà hai dây này không cho nguồn đó. Có ba hướng:
- Hộp có dây N: làm theo bài này.
- Chỉ có 2 dây: kéo thêm dây N từ hộp nối. Đây là cách đúng.
- Đặt mạch tại đèn: gắn cạnh bóng đèn, nơi có đủ L và N, còn công tắc tường để luôn đóng. Cách này không cần đụng hộp âm tường.
Công tắc thông minh loại "không dây N" bán trên thị trường lấy một dòng rất nhỏ rò qua bóng đèn. Cách đó cần mạch nguồn chuyên dụng và chip tiêu thụ cực thấp, không làm được với mạch ESP32 thông thường vì dòng Wi-Fi quá lớn. Đừng thử nối nối tiếp với bóng đèn.
2. Sơ đồ khối

| Khối | Linh kiện | Lưu ý |
|---|---|---|
| Nguồn | Module AC-DC cách ly 5 V (loại HLK-PM01 hoặc tương đương) | Cầu chì 1 A trên dây L, varistor 470 V giữa L và N |
| Điều khiển | ESP32. Mạch DevKit dài, chỉ để thử trên bàn; lắp thật cần module nhỏ hơn hoặc đặt mạch tại hộp nổi | Hộp âm tường rất chật, tính kích thước trước |
| Đóng cắt | Module relay 1 kênh 5 V | Chỉ dùng cho đèn, quạt nhỏ; để dư dòng |
| Cảm ứng | Tấm đồng hoặc băng đồng khoảng 25 × 25 mm dán sau mặt nhựa 2–3 mm | Dây ngắn dưới 10 cm, xa dây 220 V |
- Không làm mặt công tắc bằng kim loại. Người dùng chỉ chạm vào lớp nhựa cách điện.
- Giữ khoảng cách giữa vùng 220 V và vùng điện áp thấp tối thiểu 6 mm trên mạch.
- Module nguồn và relay tỏa nhiệt trong không gian kín. Không dùng cho bình nóng lạnh, máy lạnh hay tải động cơ lớn.
Cách đấu module nguồn 220 V có trong bài cùng cụm về HLK-PM01. Relay kích mức thấp và chân JD-VCC xem ở bài module relay 1, 2, 4 kênh. Nếu relay làm mạch khởi động lại thì xem đóng relay làm ESP32 bị reset.
3. Thuật toán chạm chống bật nhầm
ESP32 có sẵn các kênh cảm ứng điện dung. Hàm touchRead() trả một con số thay đổi khi ngón tay tới gần. Bài chạm tay bật đèn giới thiệu cách dùng. Với công tắc treo tường chạy nhiều năm, không nên so với ngưỡng cố định kiểu if (touchRead(4) < 60), vì độ ẩm, nhiệt độ và tuổi của keo dán làm giá trị nền trôi dần. Khi nền tụt tới gần ngưỡng, đèn sẽ tự bật tắt lúc nửa đêm.

Chương trình dùng bốn biện pháp, và mô phỏng cho thấy chúng nhận đúng 3 lần chạm trong khi ngưỡng cố định báo 80 lần:
- So theo tỉ lệ với nền: lệch quá 25 % mới tính. Tỉ lệ dùng được cho cả hai thang giá trị của
touchRead()và cả hai chiều tăng giảm. - Nền tự theo: chỉ cập nhật rất chậm khi không chạm, nên trôi theo môi trường nhưng không trôi theo ngón tay.
- Hai ngưỡng: vào ở 25 %, ra ở 12 %, tránh rung quanh một ngưỡng.
- Xác nhận 4 mẫu liên tiếp (80 ms) để bỏ qua xung nhiễu do relay hay thiết bị điện khác.
Script mô phỏng dưới đây dùng đúng các hằng số của chương trình. Đổi số trong đó để xem tác dụng trước khi sửa code.
# PYWORLD.VN - Mô phỏng phát hiện chạm: so ngưỡng cố định với cách của chương trình trong bài (lệch theo nền, hai ngưỡng, xác nhận 4 mẫu)
# PYWORLD.VN - Số đo giả định: nền 80 có nhiễu, độ ẩm làm nền tụt dần, một xung nhiễu ngắn, 3 lần chạm thật; lấy mẫu 20 ms
# PYWORLD.VN - Chạy: python mo_phong_cham.py (chỉ dùng thư viện chuẩn)
"""Hằng số TY_LE_CHAM, TY_LE_NHA, SO_MAU_XAC_NHAN, HE_SO_NEN giống hệt file .ino."""
import random
BUOC_S = 0.02
TY_LE_CHAM, TY_LE_NHA, SO_MAU_XAC_NHAN, HE_SO_NEN = 0.25, 0.12, 4, 0.002
NGUONG_CO_DINH = 60 # kiểu hay viết: if (touchRead(4) < 60)
def tin_hieu(tong_s: float = 60.0) -> list[float]:
rng = random.Random(7)
ra = []
for i in range(int(tong_s / BUOC_S)):
t = i * BUOC_S
nen = 80 - 26 * (t / tong_s) # nền tụt từ 80 xuống 54 khi ẩm lên, mô phỏng rút ngắn thời gian
v = nen + rng.gauss(0, 1.2)
if 10 <= t < 10.3 or 30 <= t < 30.3 or 50 <= t < 50.3:
v = nen * 0.5 + rng.gauss(0, 1.2)
if 20 <= t < 20.03: # xung nhiễu một mẫu
v = nen * 0.45
ra.append(v)
return ra
def co_dinh(mau: list[float]) -> list[float]:
"""Mỗi lần xuống dưới ngưỡng tính một lần chạm, trả về các thời điểm."""
su_kien, truoc = [], False
for i, v in enumerate(mau):
nay = v < NGUONG_CO_DINH
if nay and not truoc:
su_kien.append(i * BUOC_S)
truoc = nay
return su_kien
def theo_nen(mau: list[float]) -> tuple[list[float], list[float]]:
"""Cùng logic xuLyCham() trong .ino; trả về (các lần chạm, đường nền theo thời gian)."""
nen = sum(mau[:32]) / 32
dang_cham, dem, su_kien, duong_nen = False, 0, [], []
for i, v in enumerate(mau):
lech = abs(v - nen) / nen
if not dang_cham:
dem = dem + 1 if lech > TY_LE_CHAM else 0
if dem >= SO_MAU_XAC_NHAN:
dang_cham, dem = True, 0
su_kien.append(i * BUOC_S)
elif lech < TY_LE_NHA:
nen += HE_SO_NEN * (v - nen)
else:
dem = dem + 1 if lech < TY_LE_NHA else 0
if dem >= SO_MAU_XAC_NHAN:
dang_cham, dem = False, 0
duong_nen.append(nen)
return su_kien, duong_nen
if __name__ == "__main__":
m = tin_hieu()
a = co_dinh(m)
b, _ = theo_nen(m)
print(f"Nguong co dinh: {len(a)} lan, lan dau {a[0]:.2f} s, lan cuoi {a[-1]:.2f} s")
print("Theo nen:", [round(x, 2) for x in b])
4. Chương trình ESP32
Tấm cảm ứng nối GPIO4, IN của relay nối GPIO32. Sửa tên và mật khẩu Wi-Fi rồi nạp khi chưa nối 220 V, nguồn từ cổng USB. Thử bằng LED hoặc nghe tiếng relay trước.
// PYWORLD.VN - Công tắc cảm ứng tự làm: chạm tấm đồng ở GPIO4 để đảo relay GPIO32 kích mức thấp, nhớ trạng thái khi mất điện
// PYWORLD.VN - So độ lệch với giá trị nền (nền tự theo độ ẩm, nhiệt độ) thay vì ngưỡng cố định; cần vài mẫu liên tiếp mới nhận là chạm
// PYWORLD.VN - Có Wi-Fi thì bật tắt qua trình duyệt; mất Wi-Fi công tắc vẫn chạy. Phần 220 V phải theo đúng cảnh báo trong bài
#include <Preferences.h>
#include <WebServer.h>
#include <WiFi.h>
const char *TEN_MANG = "TEN_WIFI";
const char *MAT_KHAU = "MAT_KHAU_WIFI";
const int CHAN_CHAM = 4; // GPIO4 là kênh cảm ứng T0
const int CHAN_RELAY = 32; // IN của module relay
const int MUC_BAT = LOW; // module kích mức thấp
const int MUC_TAT = HIGH;
const uint32_t CHU_KY_MAU_MS = 20;
const float TY_LE_CHAM = 0.25; // lệch quá 25 % so với nền thì coi là đang chạm
const float TY_LE_NHA = 0.12; // về dưới 12 % mới coi là đã nhả (trễ hai ngưỡng)
const int SO_MAU_XAC_NHAN = 4; // 4 mẫu liên tiếp = 80 ms
const float HE_SO_NEN = 0.002; // nền chạy rất chậm, chỉ cập nhật khi không chạm
Preferences boNho;
WebServer mayChu(80);
float nen = 0;
bool dangCham = false;
bool relayBat = false;
int demMau = 0;
uint32_t lucLayMau = 0;
void datRelay(bool bat, const char *nguon) {
relayBat = bat;
digitalWrite(CHAN_RELAY, bat ? MUC_BAT : MUC_TAT);
boNho.putBool("bat", bat); // chỉ ghi khi đổi trạng thái để giữ tuổi thọ flash
Serial.printf("Relay %s (%s)\n", bat ? "BAT" : "TAT", nguon);
}
float docTrungBinh(int soLan) {
uint32_t tong = 0;
for (int i = 0; i < soLan; i++) {
tong += touchRead(CHAN_CHAM);
delay(5);
}
return (float)tong / soLan;
}
void xuLyCham() {
const float giaTri = touchRead(CHAN_CHAM);
const float lech = fabs(giaTri - nen) / nen;
if (!dangCham) {
demMau = (lech > TY_LE_CHAM) ? demMau + 1 : 0;
if (demMau >= SO_MAU_XAC_NHAN) {
dangCham = true;
demMau = 0;
datRelay(!relayBat, "cham");
} else if (lech < TY_LE_NHA) {
nen += HE_SO_NEN * (giaTri - nen); // nền theo độ ẩm, nhiệt độ
}
} else {
demMau = (lech < TY_LE_NHA) ? demMau + 1 : 0;
if (demMau >= SO_MAU_XAC_NHAN) {
dangCham = false;
demMau = 0;
}
}
}
void trangChu() {
String html = "<!doctype html><meta name=viewport content='width=device-width'><h2>Den: ";
html += relayBat ? "BAT" : "TAT";
html += "</h2><a href=/dao>Dao trang thai</a>";
mayChu.send(200, "text/html", html);
}
void daoQuaWeb() {
datRelay(!relayBat, "web");
mayChu.sendHeader("Location", "/");
mayChu.send(303);
}
void setup() {
Serial.begin(115200);
boNho.begin("cong-tac", false);
relayBat = boNho.getBool("bat", false);
// Gói esp32 3.x bỏ qua digitalWrite trước pinMode: đặt mức ở INPUT rồi mới chuyển OUTPUT.
pinMode(CHAN_RELAY, INPUT);
digitalWrite(CHAN_RELAY, relayBat ? MUC_BAT : MUC_TAT);
pinMode(CHAN_RELAY, OUTPUT);
delay(300);
nen = docTrungBinh(32); // không chạm tay vào tấm cảm ứng lúc cấp điện
Serial.printf("Gia tri nen: %.1f\n", nen);
WiFi.mode(WIFI_STA);
WiFi.setAutoReconnect(true);
WiFi.begin(TEN_MANG, MAT_KHAU); // không chờ: công tắc phải chạy được khi chưa có Wi-Fi
mayChu.on("/", trangChu);
mayChu.on("/dao", daoQuaWeb);
mayChu.begin();
}
void loop() {
mayChu.handleClient();
if (millis() - lucLayMau >= CHU_KY_MAU_MS) {
lucLayMau = millis();
xuLyCham();
}
static bool daBaoIp = false;
if (!daBaoIp && WiFi.status() == WL_CONNECTED) {
daBaoIp = true;
Serial.print("Dia chi: http://");
Serial.println(WiFi.localIP());
}
}
- Đừng chạm vào tấm cảm ứng trong lúc cấp điện, vì chương trình đo nền ở bước này.
Preferencesnhớ trạng thái đèn khi mất điện. Lệnh ghi chỉ chạy khi trạng thái đổi, nên flash không bị ghi liên tục.- Ba dòng
pinModetrongsetup()giữ relay ở đúng mức ngay từ đầu: gói esp32 3.x bỏ quadigitalWrite()gọi trướcpinMode(). WiFi.begin()không chờ kết nối. Mất Wi-Fi hay router hỏng, chạm tay vẫn bật được đèn.- Trang web chỉ dùng trong mạng nội bộ, không có mật khẩu. Không mở cổng ra Internet.

Tiếp theo có thể nối công tắc vào nhà thông minh: thay trang web bằng MQTT theo bài đưa ESP32 và relay vào Home Assistant.
5. Xử lý lỗi thường gặp
| Hiện tượng | Nguyên nhân | Cách xử lý |
|---|---|---|
| Đèn tự bật tắt | Dây cảm ứng dài, đi cạnh dây 220 V; nguồn nhiễu | Rút ngắn dây, tách xa dây điện; tăng SO_MAU_XAC_NHAN |
| Chạm không ăn | Mặt nhựa quá dày, tấm đồng nhỏ | Dùng mặt 2–3 mm, tấm đồng lớn hơn; giảm TY_LE_CHAM |
| Luôn nhận là đang chạm | Tay đặt trên tấm lúc cấp điện | Nhấn EN, không chạm trong 1 giây đầu |
| Đóng relay thì ESP32 khởi động lại | Nguồn yếu, xung nhiễu khi relay đóng | Tụ 470 µF ở 5 V, mạch RC dập tia lửa cho tải cảm |
| Module nguồn nóng, có tiếng rít | Quá tải, không thoáng | Kiểm tra tổng dòng; module 3 W chỉ cho khoảng 600 mA ở 5 V |
| Trang web không vào được | Khác mạng, sai IP | Xem IP trên Serial Monitor, điện thoại phải cùng Wi-Fi |
6. Xem thêm
Muốn điều khiển relay không cần mở hộp điện, xem ESP32 làm web server điều khiển relay và điều khiển relay bằng Blynk.
Sản phẩm dùng trong bài: ESP32 DevKit V1 30 chân, module relay 1, 2, 4 kênh.
Bài cùng cụm chủ đề: HLK-PM01; an toàn khi làm mạch 220 V.
Xem thêm