Báo trộm cảm biến chuyển động PIR, bật/tắt canh gác qua điện thoại
Phát hiện người đi vào nhà, kho, cửa hàng trong tầm 3–7m rồi hú còi và nháy đèn. Bật/tắt chế độ canh gác trên web điện thoại; mạch ghi nhật ký giờ phát hiện (giờ Việt Nam). Còi tự tắt sau thời gian cài đặt để không làm phiền hàng xóm.
✅ Đã kiểm thử
Xem chi tiết 11 phép đo
Đang báo động (PIR phát hiện, còi + đèn bật)
- Q1: dẫn 29.2mA, Vce=0.04V (bão hòa tốt).
- D1: dòng 5.9mA (cho phép 20.0mA).
- Chân GPIO25 cấp 5.9mA (an toàn ≤ 20.0mA).
- Chân GPIO26 cấp 2.4mA (an toàn ≤ 20.0mA).
- Tổng dòng đỉnh từ USB 287.7mA (đủ với cục sạc 5V 1A).
- Áp tín hiệu PIR vào ESP32 (phải ≤ 3,3V): 3.30V ✓
- Dòng qua còi: 29.2mA ✓
- Dòng chân GPIO26 mở transistor: 2.4mA ✓
- Dòng LED đỏ: 5.9mA ✓
Canh gác yên lặng
- Tổng dòng đỉnh từ USB 250.1mA (đủ với cục sạc 5V 1A).
- Còi tắt: 0.0mA ✓
Nguyên lý hoạt động
Cảm biến PIR HC-SR501 nhận bức xạ hồng ngoại từ thân nhiệt người. Khi có người di chuyển trong vùng quét, chân OUT lên 3,3V (có ổn áp riêng bên trong) nên nối thẳng vào GPIO27 của ESP32 an toàn. Module được nuôi bằng 5V từ chân VIN. ESP32 phát WiFi và chạy trang web để bạn bật/tắt canh gác. Khi đang canh gác mà PIR báo, ESP32 bật còi (qua transistor S8050, còi ăn khoảng 30mA, chân GPIO chỉ cấp khoảng 2,5mA) trong thời gian cài đặt (mặc định 30 giây), và ghi lại giờ phát hiện lấy từ Internet (giờ Việt Nam UTC+7).
Sơ đồ đấu dây
Mỗi màu là một dây. Chấm tròn là điểm hàn. Chạm vào tên dây trong bảng để làm nổi bật.
| # | Dây | Nối các điểm |
|---|---|---|
| 1 | đỏ 5V | U1 VIN ⟷ U2 VCC ⟷ BZ1 + |
| 2 | đen GND | U1 GND ⟷ U2 GND ⟷ Q1 E (phát) ⟷ D1 K · chân ngắn (−) |
| 3 | vàng PIR_OUT | U2 OUT ⟷ U1 GPIO27 (D27) |
| 4 | xanh lá BZ_DRV | U1 GPIO26 (D26) ⟷ R1 1 |
| 5 | xanh dương BZ_BASE | R1 2 ⟷ Q1 B (gốc) |
| 6 | tím BZ_COL | BZ1 - ⟷ Q1 C (góp) |
| 7 | xanh ngọc LED_DRV | U1 GPIO25 (D25) ⟷ R2 1 |
| 8 | nâu LED_A | R2 2 ⟷ D1 A · chân dài (+) |
Danh sách linh kiện
| Ký hiệu | Linh kiện | Giá tham khảo | |
|---|---|---|---|
| U1 | ESP32 DevKit V1 30 chân (ESP32-WROOM-32) | 110.000đ | Tìm mua |
| U2 | Cảm biến chuyển động PIR HC-SR501 (có 2 biến trở chỉnh tầm xa và thời gian giữ) | 22.000đ | Tìm mua |
| Q1 | Transistor S8050 (TO-92) | 500đ | Tìm mua |
| R1 | Điện trở 1kΩ 1/4W (nâu-đen-đỏ) | 200đ | Tìm mua |
| BZ1 | Còi chip chủ động 5V (hoặc còi báo động 5V loại to) | 4.000đ | Tìm mua |
| D1 | LED đỏ 5mm (red) | 300đ | Tìm mua |
| R2 | Điện trở 220Ω 1/4W (đỏ-đỏ-nâu) | 200đ | Tìm mua |
| Tổng (chưa gồm board hàn, dây, thiếc) | 137.200đ |
Các bước làm
Code nạp cho mạch
Chọn board DOIT ESP32 DEVKIT V1.
/*
Mạch Gọn — Báo trộm PIR, bật/tắt canh gác qua điện thoại
Board : ESP32 DevKit V1 ("DOIT ESP32 DEVKIT V1")
Chân : GPIO27 <- OUT của PIR HC-SR501 (3,3V)
GPIO26 -> R 1k -> B S8050 (còi ở cực C)
GPIO25 -> 220Ω -> LED đỏ
Lần đầu: WiFi "MachGon-BaoTrom" (mật khẩu machgon123) -> 192.168.4.1
*/
#include <Arduino.h>
#include <WiFi.h>
#include <WebServer.h>
#include <ESPmDNS.h>
#include <Preferences.h>
#include <time.h>
const uint8_t PIN_PIR = 27;
const uint8_t PIN_BUZ = 26;
const uint8_t PIN_LED = 25;
const char* AP_SSID = "MachGon-BaoTrom";
const char* HOSTNAME = "baotrom";
const uint32_t PIR_WARMUP_MS = 60000; // PIR cần ~60s ổn định sau khi cấp điện
const uint8_t LOG_MAX = 40;
WebServer server(80);
Preferences cfg;
bool armed = false;
uint32_t armedAt = 0; // thời điểm bấm canh gác (để tính giờ chờ ra khỏi nhà)
uint16_t sirenSec = 30; // còi hú tối đa bao nhiêu giây mỗi lần
uint16_t exitDelaySec = 20; // thời gian chờ sau khi bật canh gác
bool sirenOn = false;
uint32_t sirenStart = 0;
bool lastPir = false;
uint32_t eventCount = 0;
struct LogItem { time_t t; uint32_t ms; bool alarm; };
LogItem logs[LOG_MAX];
uint8_t logHead = 0, logCount = 0;
bool timeOk() { return time(nullptr) > 1700000000; }
void addLog(bool alarm) {
logs[logHead] = { timeOk() ? time(nullptr) : 0, millis(), alarm };
logHead = (logHead + 1) % LOG_MAX;
if (logCount < LOG_MAX) logCount++;
}
String fmtLog(const LogItem& l) {
char b[40];
if (l.t) { struct tm tmv; localtime_r(&l.t, &tmv); strftime(b, sizeof b, "%d/%m %H:%M:%S", &tmv); }
else snprintf(b, sizeof b, "+%lus sau khi bật", (unsigned long)(l.ms / 1000));
return String(b);
}
bool active() { return armed && millis() - armedAt >= exitDelaySec * 1000UL && millis() > PIR_WARMUP_MS; }
void setSiren(bool on) {
sirenOn = on;
if (on) sirenStart = millis();
else digitalWrite(PIN_BUZ, LOW);
}
const char PAGE[] PROGMEM = R"HTML(<!doctype html><html lang="vi"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1"><title>Báo trộm</title>
<style>
body{margin:0;font-family:system-ui,sans-serif;background:#0f172a;color:#e2e8f0}
header{padding:14px;background:#1e293b;font-weight:700}
.card{margin:14px;padding:14px;background:#1e293b;border-radius:14px}
.big{width:100%;padding:22px;font-size:20px;border:0;border-radius:14px;font-weight:700}
.on{background:#dc2626;color:#fff}.off{background:#22c55e;color:#052e16}
.st{font-size:18px;margin-bottom:10px}.muted{color:#94a3b8;font-size:13px}
ul{list-style:none;padding:0;margin:0}li{padding:8px 0;border-bottom:1px solid #334155}
li.a{color:#fca5a5}input{width:100%;padding:9px;margin:4px 0 8px;border-radius:8px;border:1px solid #475569;background:#0b1220;color:#e2e8f0;box-sizing:border-box}
.btn{background:#334155;color:#e2e8f0;border:0;border-radius:10px;padding:10px 14px;margin-top:6px}
</style></head><body>
<header>🛡️ Báo trộm</header>
<div class="card"><div class="st" id="st">…</div><button class="big" id="arm"></button>
<button class="btn" id="mute" style="width:100%;margin-top:10px">🔕 Tắt còi ngay</button>
<p class="muted" id="info"></p></div>
<div class="card"><b>Nhật ký phát hiện</b><ul id="log"></ul></div>
<div class="card"><b>Cài đặt</b>
<label class="muted">Còi hú tối đa (giây)</label><input id="siren" type="number" min="3" max="600">
<label class="muted">Chờ ra khỏi nhà sau khi bật canh gác (giây)</label><input id="exit" type="number" min="0" max="300">
<button class="btn" onclick="saveOpt()">Lưu</button><hr style="border-color:#334155">
<label class="muted">WiFi nhà (để có giờ chính xác)</label><input id="ssid" placeholder="Tên WiFi"><input id="pass" type="password" placeholder="Mật khẩu">
<button class="btn" onclick="wifi()">Lưu WiFi & khởi động lại</button></div>
<script>
const $=s=>document.querySelector(s);let S={};
async function load(){const r=await fetch('/api/state');S=await r.json();
$('#st').textContent=S.siren?'🚨 ĐANG BÁO ĐỘNG':S.armed?(S.active?'🔒 Đang canh gác':'⏳ Sắp canh gác…'):'🔓 Đã tắt canh gác';
$('#arm').textContent=S.armed?'TẮT CANH GÁC':'BẬT CANH GÁC';$('#arm').className='big '+(S.armed?'off':'on');
$('#info').textContent=(S.sta?'Đã nối WiFi nhà, IP '+S.ip+' — http://baotrom.local':'Chưa nối WiFi nhà')+(S.warm?'':' · PIR đang khởi động')+' · PIR hiện tại: '+(S.pir?'CÓ chuyển động':'yên');
$('#log').innerHTML=S.log.map(l=>`<li class="${l.a?'a':''}">${l.a?'🚨':'👣'} ${l.t} — ${l.a?'Báo động':'Có chuyển động'}</li>`).join('')||'<li class="muted">Chưa có</li>';
if(document.activeElement.tagName!='INPUT'){$('#siren').value=S.sirenSec;$('#exit').value=S.exitSec}}
$('#arm').onclick=async()=>{await fetch('/api/arm?on='+(S.armed?0:1),{method:'POST'});load()};
$('#mute').onclick=async()=>{await fetch('/api/mute',{method:'POST'});load()};
async function saveOpt(){await fetch('/api/opt?siren='+$('#siren').value+'&exit='+$('#exit').value,{method:'POST'});load()}
async function wifi(){await fetch('/api/wifi',{method:'POST',body:new URLSearchParams({ssid:$('#ssid').value,pass:$('#pass').value})});alert('Đã lưu, mạch khởi động lại')}
load();setInterval(load,2000);
</script></body></html>)HTML";
void handleState() {
bool sta = WiFi.status() == WL_CONNECTED;
String j = "{\"armed\":" + String(armed ? "true" : "false") + ",\"active\":" + (active() ? "true" : "false") +
",\"siren\":" + (sirenOn ? "true" : "false") + ",\"pir\":" + (lastPir ? "true" : "false") +
",\"warm\":" + (millis() > PIR_WARMUP_MS ? "true" : "false") + ",\"sta\":" + (sta ? "true" : "false") +
",\"ip\":\"" + (sta ? WiFi.localIP().toString() : WiFi.softAPIP().toString()) + "\",\"sirenSec\":" + sirenSec +
",\"exitSec\":" + exitDelaySec + ",\"log\":[";
for (uint8_t i = 0; i < logCount; i++) {
const LogItem& l = logs[(logHead + LOG_MAX - 1 - i) % LOG_MAX];
if (i) j += ',';
j += "{\"t\":\"" + fmtLog(l) + "\",\"a\":" + (l.alarm ? "true" : "false") + "}";
}
j += "]}";
server.send(200, "application/json", j);
}
void setupWeb() {
server.on("/", []() { server.send_P(200, "text/html; charset=utf-8", PAGE); });
server.on("/api/state", HTTP_GET, handleState);
server.on("/api/arm", HTTP_POST, []() {
armed = server.arg("on") == "1"; armedAt = millis(); cfg.putBool("armed", armed);
if (!armed) setSiren(false);
digitalWrite(PIN_BUZ, HIGH); delay(armed ? 60 : 200); digitalWrite(PIN_BUZ, LOW); // bíp xác nhận
server.send(200, "text/plain", "ok");
});
server.on("/api/mute", HTTP_POST, []() { setSiren(false); server.send(200, "text/plain", "ok"); });
server.on("/api/opt", HTTP_POST, []() {
sirenSec = constrain(server.arg("siren").toInt(), 3, 600);
exitDelaySec = constrain(server.arg("exit").toInt(), 0, 300);
cfg.putUShort("siren", sirenSec); cfg.putUShort("exit", exitDelaySec);
server.send(200, "text/plain", "ok");
});
server.on("/api/wifi", HTTP_POST, []() {
cfg.putString("ssid", server.arg("ssid")); cfg.putString("pass", server.arg("pass"));
server.send(200, "text/plain", "ok"); delay(500); ESP.restart();
});
server.onNotFound([]() { server.sendHeader("Location", "/"); server.send(302); });
server.begin();
}
void setup() {
Serial.begin(115200);
pinMode(PIN_PIR, INPUT);
pinMode(PIN_BUZ, OUTPUT);
pinMode(PIN_LED, OUTPUT);
digitalWrite(PIN_BUZ, LOW);
cfg.begin("cfg", false);
armed = cfg.getBool("armed", false); // mất điện có lại vẫn giữ trạng thái canh gác
sirenSec = cfg.getUShort("siren", 30);
exitDelaySec = cfg.getUShort("exit", 20);
armedAt = millis();
WiFi.mode(WIFI_AP_STA);
WiFi.softAP(AP_SSID, cfg.getString("appass", "machgon123").c_str());
String ssid = cfg.getString("ssid", "");
if (ssid.length()) {
WiFi.begin(ssid.c_str(), cfg.getString("pass", "").c_str());
uint32_t t = millis();
while (WiFi.status() != WL_CONNECTED && millis() - t < 15000) delay(250);
}
configTime(7 * 3600, 0, "pool.ntp.org", "time.google.com"); // giờ Việt Nam UTC+7
if (MDNS.begin(HOSTNAME)) MDNS.addService("http", "tcp", 80);
setupWeb();
Serial.printf("San sang. AP %s IP %s\n", AP_SSID, WiFi.softAPIP().toString().c_str());
}
void loop() {
server.handleClient();
uint32_t now = millis();
bool pir = digitalRead(PIN_PIR) == HIGH;
if (pir && !lastPir && now > PIR_WARMUP_MS) {
eventCount++;
bool alarm = active();
addLog(alarm);
Serial.printf("PIR=1 (%s)\n", alarm ? "BAO DONG" : "chi ghi nhat ky");
if (alarm) setSiren(true);
}
lastPir = pir;
// còi hú ngắt quãng, tự tắt sau sirenSec
if (sirenOn) {
digitalWrite(PIN_BUZ, (now / 250) % 2);
if (now - sirenStart >= sirenSec * 1000UL) setSiren(false);
}
// đèn: nháy nhanh khi báo động, nháy chậm khi chờ canh gác, sáng mờ chớp 1 lần/3s khi canh gác
if (sirenOn) digitalWrite(PIN_LED, (now / 150) % 2);
else if (armed && !active()) digitalWrite(PIN_LED, (now / 500) % 2);
else if (armed) digitalWrite(PIN_LED, (now % 3000) < 80);
else digitalWrite(PIN_LED, LOW);
static uint32_t lastCheck = 0;
if (now - lastCheck > 30000) {
lastCheck = now;
if (cfg.getString("ssid", "").length() && WiFi.status() != WL_CONNECTED) WiFi.reconnect();
}
delay(5);
}
Đo kiểm bằng đồng hồ vạn năng
| Giữa VCC và GND của PIR | 4,6 – 5,1V |
| Giữa OUT và GND khi vẫy tay trước PIR | khoảng 3,3V, đứng yên vài giây về 0V |
| Trang web khi vẫy tay | Dòng "Có chuyển động" mới trong nhật ký |
| Giữa C và E của S8050 khi còi tắt | khoảng 3V trở lên |
Sửa lỗi thường gặp
Báo động liên tục dù không có ai
Đợi 60 giây sau khi cấp điện; giảm tầm xa (Sx); đổi hướng tránh luồng gió máy lạnh, rèm bay, nắng chiếu. Dây PIR dài trên 3m nên dùng dây có chống nhiễu.
Không phát hiện được
Kiểm tra PIR được cấp 5V (không phải 3V3), jumper ở H; xem dòng "PIR=1" trên Serial Monitor 115200 khi vẫy tay.
Giờ trong nhật ký sai hoặc là "--"
Mạch chưa vào được Internet: nhập WiFi nhà ở trang Cài đặt. Khi chưa có giờ, nhật ký ghi số giây kể từ lúc bật.
Còi không kêu
Kiểm tra là còi chủ động (cấp 5V trực tiếp phải kêu), chiều + của còi và chân E-B-C.
⚠️ An toàn
- Mạch dùng 5V USB, an toàn khi chạm.
- Nên cắm qua cục sạc dự phòng nếu muốn còn chạy khi mất điện.
- Đây là mạch tham khảo, không thay thế hệ thống an ninh chuyên nghiệp.