
“妹妹出沒”這個名字來自一個很具體的家庭需求想知道孩子或住在同一屋檐下的妹妹幾點到家、什么時候出門又不想在門口安裝攝像頭。把它做成技術項目本質就是一套“人體存在檢測 出入事件記錄 手機消息推送”的家庭自動化小系統。本文會帶你把這條鏈路從零跑通用一塊 ESP32 開發板配合人體感應模塊檢測門口是否有人把狀態變化通過 MQTT 發布到本地 Broker再讓 Python 服務訂閱消息、寫入 SQLite 并推送到手機最后用一個簡單的 Flask 頁面查看歷史出沒記錄。適合有一定 Arduino 或 Python 基礎、想嘗試硬件與服務端聯調的讀者。讀完這條實踐鏈路你能掌握 PIR 人體感應模塊怎么接 ESP32、MQTT 的發布與訂閱模型怎么落地、消息如何落庫和推送以及斷電重連、誤報排查和生產部署時要注意什么。整條鏈路用到的組件都很常見成本在幾十元到一百多元之間適合作為家庭自動化入門項目反復調優。1. 先確認需求邊界出沒檢測到底檢測什么1.1 把“誰、幾點、進還是出”拆成可檢測的事件一個容易犯的錯誤是拿到“妹妹出沒”這個標題后直接想做一個“能識別是誰回家”的系統。這類需求一旦拆開會發現它包含三個難度完全不同的子問題時間段幾點發生。狀態門口區域有沒有人。身份這個人是誰。第一和第二個問題用一只人體感應傳感器加一塊單片機就能解決。第三個問題則完全不同它需要攝像頭加人臉識別、手機定位地理圍欄或者多個傳感器配合做方向判斷。用單只 PIR 模塊無法知道“是誰”只能知道“有人”或“沒人”。把預期放在正確的層級上后面的技術選型才不會跑偏。因此本文把“妹妹出沒”收斂為檢測門口或某個公共區域是否存在人體記錄狀態發生變化的時刻并在變化時推送通知。這是一個明確、可測試、可驗證的目標。至于“識別身份”放到擴展方向里討論。1.2 三條技術路線的取舍路線識別能力硬件成本隱私風險適合場景ESP32 PIR/毫米波 MQTT區域有人/無人不識別身份低幾十到一百多元低不采集圖像家庭出入提醒、房間占用檢測樹莓派 攝像頭 OpenCV可統計人體活動擴展可識別人中高需要攝像頭和算力高涉及圖像采集進階學習、行為分析手機定位地理圍欄可直接識別“誰”到了位置中低主要靠手機中后臺定位和權限敏感兒童/老人到家提醒需要安裝 App本文選擇第一條路線。理由很直接它不依賴手機權限不采集圖像數據可以全部留在本地而且 MQTT 這條消息鏈路對后續擴展同樣成立。就算以后換成毫米波雷達或攝像頭方案Broker、數據庫、推送、看板這些服務端部分仍然可以復用。2. 硬件連接和環境準備ESP32、人體感應模塊和 MQTT Broker2.1 硬件清單搭建這套最小系統需要以下材料ESP32 開發板一塊推薦 DevKitC 或 NodeMCU-32S帶 USB 接口。HC-SR501 PIR 人體感應模塊一個或 LD2410 毫米波人體存在模塊。杜邦線若干母對公即可。5V 供電的 USB 電源和數據線。一臺可以運行 Linux 的機器可以是樹莓派、舊筆記本、云服務器或本地虛擬機用來跑 MQTT Broker 和 Python 服務。路由器或交換機組成的局域網保證 ESP32 能訪問到 Broker 所在機器。HC-SR501 是最常見的入門款價格低輸出邏輯簡單適合驗證整條鏈路。LD2410 是毫米波方案能檢測靜止存在誤報更少但通信協議更復雜。下面先用 HC-SR501 把系統跑通它的數字輸出直接接 GPIO 即可。2.2 接線方式與注意事項ESP32 引腳HC-SR501 模塊說明5VVCC模塊供電部分型號需 5VGNDGND必須與 ESP32 共地GPIO4OUT數字信號輸出檢測到人時為高電平接線時有幾個細節值得注意。第一HC-SR501 通常需要 5V 供電而它的 OUT 輸出高電平接近 3.3V可以直接接入 ESP32 的 GPIO不需要電平轉換。第二不同批次的模塊輸出電平可能有差異如果發現 GPIO 讀數異常先用萬用表確認 OUT 在觸發時是否為 3.3V 左右。第三接線完成后不要急著上電先檢查有沒有接反或短路。2.3 安裝 Arduino 環境并準備開發板支持在 Arduino IDE 2.x 中先打開“文件 - 首選項”在“附加開發板管理器網址”里填入 ESP32 官方支持包地址https://espressif.github.io/arduino-esp32/package_esp32_index.json保存后在“工具 - 開發板 - 開發板管理器”里搜索 esp32安裝由 Espressif 提供的開發板支持包。接著在庫管理器里安裝 PubSubClient 庫這個庫負責 MQTT 的發布與訂閱。安裝完成后選擇開發板為 ESP32 Dev Module并選擇 ESP32 對應的串口。這里有一個常見問題Windows 下如果串口不出現多半是缺少 USB 驅動需要下載對應芯片的驅動Mac 和 Linux 下一般插上就能識別。2.4 安裝 MQTT Broker 并驗證收發Broker 選擇 Mosquitto輕量、穩定、命令行工具齊全。在 Linux 上安裝sudo apt update sudo apt install -y mosquitto mosquitto-clients sudo systemctl enable --now mosquitto默認配置下 Mosquitto 只允許本機訪問。為了讓 ESP32 能從局域網連接需要新增一個監聽配置。在/etc/mosquitto/conf.d/local.conf中寫入listener 1883 0.0.0.0 allow_anonymous false password_file /etc/mosquitto/passwdallow_anonymous false表示所有客戶端必須使用用戶名密碼。在家庭局域網里這可以防止同一網絡中的其他設備隨意向主題發布消息。創建賬號sudo mosquitto_passwd -c /etc/mosquitto/passwd esp32上面的命令會創建密碼文件并添加 esp32 用戶。如果需要再加用戶去掉-c參數。重啟服務sudo systemctl restart mosquitto驗證 Broker 是否正常收發。開兩個終端一個訂閱mosquitto_sub -h 127.0.0.1 -t test/hello -v -u esp32 -P 你設置的密碼另一個發布mosquitto_pub -h 127.0.0.1 -t test/hello -m hello -u esp32 -P 你設置的密碼訂閱端能打印出test/hello hello說明 Broker 工作正常。這一步很重要后面的固件和服務端調試都需要以 Broker 可用為前提。3. ESP32 固件讀取傳感器狀態并發布 MQTT 消息3.1 固件要做的事ESP32 固件承擔三件事連接 WiFi、讀取 PIR 引腳電平、把狀態變化通過 MQTT 發布到指定主題。這里不要在每次讀取到高電平時都發消息而只在狀態從“無人”變為“有人”、或從“有人”變為“無人”時發布這樣服務端收到的是一條條有意義的事件而不是一堆重復數據。為了讓服務端在重啟后也能知道當前門口狀態發布時可以使用 MQTT 的 retained 消息。retained 消息會被 Broker 保留新訂閱者一上線就能收到最近一次狀態。3.2 完整固件代碼#include WiFi.h #include PubSubClient.h const char* WIFI_SSID 你的WiFi名稱; const char* WIFI_PASSWORD 你的WiFi密碼; const char* MQTT_HOST 192.168.1.100; // 改成 Broker 所在機器 IP const uint16_t MQTT_PORT 1883; const char* MQTT_USER esp32; const char* MQTT_PASSWORD 你的MQTT密碼; const char* MQTT_TOPIC home/presence/door; const int PIR_PIN 4; const unsigned long WARMUP_MS 30000; // 傳感器預熱時間 const unsigned long STATE_HOLD_MS 5000; // 狀態保持時間避免抖動 WiFiClient wifiClient; PubSubClient mqttClient(wifiClient); int lastState -1; unsigned long lastStateChangeAt 0; void setup() { Serial.begin(115200); pinMode(PIR_PIN, INPUT); WiFi.mode(WIFI_STA); WiFi.begin(WIFI_SSID, WIFI_PASSWORD); while (WiFi.status() ! WL_CONNECTED) { delay(500); Serial.print(.); } Serial.println(\nwifi connected); Serial.print(ip: ); Serial.println(WiFi.localIP()); mqttClient.setServer(MQTT_HOST, MQTT_PORT); // 傳感器上電后有預熱期等待 30 秒后再讀取初始狀態 Serial.println(warming up sensor...); delay(WARMUP_MS); lastState digitalRead(PIR_PIN); Serial.printf(initial state: %s\n, lastState HIGH ? present : away); } void connectMQTT() { while (!mqttClient.connected()) { Serial.print(connect mqtt...); if (mqttClient.connect(esp32-door-01, MQTT_USER, MQTT_PASSWORD)) { Serial.println(ok); } else { Serial.printf(fail rc%d, retry 5s\n, mqttClient.state()); delay(5000); } } } void publishState(int state) { const char* payload (state HIGH) ? present : away; boolean retained true; if (mqttClient.publish(MQTT_TOPIC, payload, retained)) { Serial.printf(publish ok: %s\n, payload); } else { Serial.printf(publish fail: %s\n, payload); } } void loop() { if (!mqttClient.connected()) { connectMQTT(); } mqttClient.loop(); int state digitalRead(PIR_PIN); unsigned long now millis(); if (state ! lastState (now - lastStateChangeAt) STATE_HOLD_MS) { lastStateChangeAt now; publishState(state); lastState state; } delay(200); }代碼里有兩個客戶端 ID 要特別注意。mqttClient.connect(esp32-door-01, ...)中的第一個參數是 MQTT client ID在同一 Broker 下必須唯一否則后連接的客戶端會擠掉先連接的。WiFi 賬號密碼和 MQTT 密碼不要直接寫死在共享代碼里本地練習可以先用常量準備上傳到公開倉庫前必須改成配置文件或環境變量。3.3 關鍵邏輯說明預熱期的處理是很多人忽略的一步。HC-SR501 上電后約 20 到 60 秒內可能持續輸出高電平如果不對預熱期做處理系統啟動時會立刻上報一次“有人”產生一條假事件。代碼里在 setup 中等待 30 秒再讀取初始狀態是為了讓傳感器穩定下來。狀態保持時間的處理同樣重要。PIR 在高電平持續時間可調如果傳感器在邊界反復觸發GPIO 電平會在短時間內抖動。代碼里只有在狀態變化且距離上一次變化超過STATE_HOLD_MS時才發布能過濾掉大部分短時抖動。發布時使用 retained 消息服務端訂閱后能立即拿到當前狀態而不是等到下次變化。要注意retained 消息的語義是“Broker 保留最近一條”如果設備離線時間很長新的訂閱者可能會收到過期的狀態。更嚴謹的做法是發布時加一個時間戳字段服務端根據時間判斷是否過期。對于家庭提醒場景retained 已經夠用。3.4 編譯燒錄與串口驗證在 Arduino IDE 中選擇 ESP32 Dev Module選擇正確的串口編譯上傳。打開串口監視器波特率選 115200。能看到 WiFi 連接成功、MQTT 連接成功并且在傳感器前擺手時打印publish ok: present人離開一段時間后打印publish ok: away。同時可以在電腦上訂閱主題來確認消息確實到了 Brokermosquitto_sub -h 127.0.0.1 -t home/presence/door -v -u esp32 -P 你的MQTT密碼如果能看到home/presence/door present和home/presence/door away說明硬件到 Broker 這一段已經通了可以進入服務端開發。4. Python 服務端訂閱消息、寫入 SQLite 并推送手機通知4.1 服務端職責與依賴服務端要做三件事訂閱 MQTT 主題、把事件寫入 SQLite、調用推送服務通知手機。用 Python 寫最直接依賴只有 paho-mqtt、requests 和 flask。安裝pip install paho-mqtt requests flask如果是 paho-mqtt 2.x 版本不同版本在Client構造和回調簽名上略有差異。安裝后可以先打印版本如果遇到 API 兼容提示按官方示例調整Client的構造參數即可回調邏輯不變。4.2 SQLite 表結構出沒記錄表只需要記錄設備、事件類型和發生時間。SQLite 單文件、零配置非常適合這個規模的項目。CREATE TABLE IF NOT EXISTS presence_log ( id INTEGER PRIMARY KEY AUTOINCREMENT, device TEXT NOT NULL, event TEXT NOT NULL, created_at TEXT NOT NULL );event字段只存in和out兩種值分別對應有人進入和人員離開。created_at使用本地時間字符串便于直接閱讀也方便 SQL 按天統計。如果以后接入多個傳感器device字段可以區分門口、客廳、走廊等位置。4.3 訂閱與推送代碼import sqlite3 import time import requests import paho.mqtt.client as mqtt MQTT_HOST 127.0.0.1 MQTT_PORT 1883 MQTT_TOPIC home/presence/door SENDKEY 你的Server醬SendKey DB_PATH /opt/presence/presence.db def init_db(): conn sqlite3.connect(DB_PATH) conn.execute( CREATE TABLE IF NOT EXISTS presence_log ( id INTEGER PRIMARY KEY AUTOINCREMENT, device TEXT NOT NULL, event TEXT NOT NULL, created_at TEXT NOT NULL ) ) conn.commit() conn.close() def push_notify(event, created_at): if event in: title 妹妹出沒檢測到有人進入 desp f{created_at} 門口區域檢測到人員進入 else: title 妹妹出沒檢測到人員離開 desp f{created_at} 門口區域人員已離開 url fhttps://sctapi.ftqq.com/{SENDKEY}.send try: resp requests.post(url, data{title: title, desp: desp}, timeout10) print(push response:, resp.text[:200]) except Exception as exc: print(push error:, exc) def on_connect(client, userdata, flags, rc): print(mqtt connected, rc , rc) client.subscribe(MQTT_TOPIC) def on_message(client, userdata, msg): payload msg.payload.decode(errorsignore).strip() if payload present: event in elif payload away: event out else: print(unknown payload:, payload) return created_at time.strftime(%Y-%m-%d %H:%M:%S) conn sqlite3.connect(DB_PATH) conn.execute( INSERT INTO presence_log(device, event, created_at) VALUES (?, ?, ?), (door, event, created_at) )