摘要:主要使用ESP32S3开发板外接人体红外PIR感应传感器模块,当传感器检测到有人经过时,在网页UI界面上显示当前经过的时间以及历史经过的时间记录等,实现人体感应监测效果;
一、硬件介绍
1、主控开发板
使用的是XIAO ESP32S3开发板,集成ESP32S3 32位双核处理器芯片,最高运行频率240MHz,板载8MB PSRAM和8MB FLASH;

功能引脚示意图

2、人体感应传感器模块(SY-DP-36BM)
该模组属于被动红外人体运动检测模块,整套系统由热释电传感探头、菲涅尔透镜、专用信号处理芯片、稳压外围电路四部分协同完成人体感应检测;

通过跳线帽可配置两种触发模式 H:重复触发 / L:不重复触发;
> 不重复触发:感应输出高电平后,延时时间一结束,输出将自动从高电平变为低电平
> 可重复触发: 感应输出高电平后,在延时时间段内,如果有人体在其感应范围内活动,其输出将一直保持高电平,直到人离开后才延时将高电平变为低电平

可以通过上方的距离 / 时间调节旋钮,来改变对应的感应距离范围和感应延时时间;
> 延时时间(默认3秒): 3 ~ 300s
> 在延时时间段内,不再接收任何感应信号
> 感应距离(默认4m):4 ~ 8m
主要特性
工作电压范围:DC 5-20V(5V时,高电平 3V / 低 0V)
触发方式:重复触发 / 不可重复触发
延时时间3-300S(可调)
感应角度100度
感应距离4-8米(可调)
二、功能实现
1、硬件搭建
将传感器模块的OUT引脚连接至开发板的D7引脚上;
实物搭建效果:

2、功能效果
功能:当传感器模块监测到有人经过时,在WIFI网页的UI界面上记录并显示此时经过的时间,以及历史经过的时间记录;
UI界面效果
> 上方显示:当前有人 / 无人状态以及最新触发时间
> 下方显示:历史触发时间记录(10条触发记录)

模块主要工作原理
当前模块传感器主要通过热释电红外来检测人体的移动,模块通电后需约10s进行初始化,然后进入检测状态:当有人进入模块感应范围时,OUT引脚输出高电平,人离开感应范围则自动延时关闭高电平,输出低电平;
热释电晶体元件会随温度变化产生电荷:人体会向外辐射 10μm 左右的红外热辐射,只有红外辐射强度发生变化时,晶体才会输出微弱电信号,人静止不动、红外辐射恒定不变时,传感器无输出;
> 当前模块的模式为可重复触发模式
> 延时时间: 3s
> 感应距离:4m
三、代码编写
主要相关Arduino代码
#include <WiFi.h>
#include <WebServer.h>
#define PIR_PIN D7
const char* ssid = "WIFI_SSID";
const char* password = "WIFI_PASS";
WebServer server(80);
const char* ntpServer = "ntp.aliyun.com";
const long gmtOffset_sec = 8 * 3600;
const long daylightOffset_sec = 0;
unsigned long lastTime = 0; // 上一次读取传感器的时间标记
const unsigned long interval = 500; // 传感器读取间隔
bool pirState = false; // PIR传感器状态标记,false无人,true有人
#define MAX_HISTORY 10 // 最大保存历史触发记录条数
String triggerHistory[MAX_HISTORY]; // 存储触发时间的字符串数组
int historyCount = 0; // 当前已经存储的记录数量
// 获取格式化后的北京时间
String getBeijingTime()
{
struct tm timeinfo;
// 读取失败返回--
if (!getLocalTime(&timeinfo)) return "--";
char buf[32];
// 将时间结构体格式化为 年-月-日 时:分:秒 的字符串
strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", &timeinfo);
return String(buf);
}
// 添加一条新的触发记录
void addHistory(String time)
{
for (int i = MAX_HISTORY - 1; i > 0; i--) {
triggerHistory[i] = triggerHistory[i - 1];
}
triggerHistory[0] = time; // 新记录存入数组头部
if (historyCount < MAX_HISTORY) historyCount++;
}
// 返回JSON数据给网页显示
void handleApi()
{
String json = "{";
json += "\"pirState\":" + String(pirState ? "true" : "false") + ","; // 传感器有人 / 无人状态
json += "\"historyCount\":" + String(historyCount) + ","; // 当前记录总数
json += "\"history\":["; // 历史记录数组
// 循环拼接所有历史时间
for(int i=0;i<historyCount;i++){
json += "\"" + triggerHistory[i] + "\"";
if(i != historyCount-1) json += ",";
}
json += "]}";
server.send(200, "application/json", json);
}
// 网页显示界面UI
void handleRoot()
{
String html = "<!DOCTYPE html>";
html += "<html><head><meta charset='utf-8'>";
html += "<meta name='viewport' content='width=device-width, initial-scale=1'>";
html += "<title>人体感应监测界面</title>";
html += "<style>";
html += "*{margin:0;padding:0;box-sizing:border-box;}";
html += "body{font-family:-apple-system,'Segoe UI','Microsoft YaHei',sans-serif;";
html += "background:linear-gradient(135deg,#667eea 0%,#764ba2 100%);min-height:100vh;";
html += "display:flex;justify-content:center;align-items:flex-start;padding:20px;}";
html += ".card{background:#fff;border-radius:20px;box-shadow:0 20px 60px rgba(0,0,0,.2);";
html += "width:100%;max-width:420px;padding:32px 28px;margin-top:40px;}";
html += "h2{text-align:center;color:#333;font-size:22px;margin-bottom:24px;font-weight:600;}";
html += ".box{text-align:center;padding:20px;border-radius:14px;margin-bottom:20px;transition:0.3s;}";
html += ".box-on{background:linear-gradient(135deg,#f093fb 0%,#f5576c 100%);color:#fff;}";
html += ".box-off{background:#f5f6fa;color:#999;}";
html += ".box-label{font-size:14px;opacity:.85;margin-bottom:6px;}";
html += ".box-text{font-size:28px;font-weight:700;}";
html += ".history-title{font-size:15px;color:#555;margin-bottom:12px;font-weight:600;";
html += "border-left:4px solid #764ba2;padding-left:10px;}";
html += ".history-list{list-style:none;}";
html += ".history-list li{padding:12px 14px;margin-bottom:8px;background:#f8f9ff;";
html += "border-radius:10px;font-size:14px;color:#555;display:flex;align-items:center;gap:10px;transition:0.2s;}";
html += ".dot{width:8px;height:8px;border-radius:50%;background:#f5576c;flex-shrink:0;}";
html += ".empty{text-align:center;color:#bbb;padding:20px;font-size:14px;}";
html += "</style></head><body>";
html += "<div>";
html += "<h2>人体感应监测界面</h2>";
// 当前状态
html += "<div id='statusBox' class='box box-off'>";
html += "<div>当前状态</div>";
html += "<div id='statusText'>无人</div></div>";
// 最新触发时间
html += "<div id='latestBox' class='box box-off'>";
html += "<div>最新触发时间</div>";
html += "<div id='latestTime'>暂无记录</div></div>";
// 历史记录
html += "<div>历史触发记录(<span id='historyNum'>0</span> 条)</div>";
html += "<ul id='historyList'></ul>";
html += "</div>";
html += "<script>";
html += "let lastHash = '';"; // 保存上一次数据校验值,用于判断数据是否变化
html += "function getHash(d){return d.pirState + '|' + d.historyCount + '|' + d.history.join(',');}";
html += "async function updateData(){try{";
html += "const res = await fetch('/api',{cache:'no-cache'});";
html += "const d = await res.json();"; // 解析返回的json
html += "const h = getHash(d);";
html += "if(h === lastHash) return;";
html += "lastHash = h;";
// 更新当前状态
html += "const sBox=document.getElementById('statusBox');";
html += "const sTxt=document.getElementById('statusText');";
html += "if(d.pirState){sBox.className='box box-on';sTxt.innerText='有人';}";
html += "else{sBox.className='box box-off';sTxt.innerText='无人';}";
// 更新最新触发时间
html += "const lBox=document.getElementById('latestBox');";
html += "const lTxt=document.getElementById('latestTime');";
html += "if(d.history.length>0){lBox.className='box box-on';lTxt.innerText=d.history[0];}";
html += "else{lBox.className='box box-off';lTxt.innerText='暂无记录';}";
// 更新历史记录
html += "document.getElementById('historyNum').innerText=d.historyCount;";
html += "const list=document.getElementById('historyList');list.innerHTML='';";
html += "if(d.history.length===0){list.innerHTML='<div class=\"empty\">暂无触发记录,等待人体经过…</div>';}";
html += "else{d.history.forEach(item=>{let li=document.createElement('li');li.innerHTML='<span class=\"dot\"></span>'+item;list.appendChild(li);});}";
html += "}catch(e){console.log('获取失败',e);}}";
html += "setInterval(updateData,2000);updateData();";
html += "</script>";
html += "</body></html>";
server.send(200, "text/html", html);
}
void setup() {
Serial.begin(115200);
pinMode(PIR_PIN, INPUT); // PIR引脚
pinMode(LED_BUILTIN, OUTPUT);
digitalWrite(LED_BUILTIN, HIGH);
WiFi.begin(ssid, password); // WiFi连接
Serial.print("正在连接WiFi");
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("");
Serial.println("WiFi连接成功");
Serial.print("网页地址:");
Serial.println(WiFi.localIP()); // 网页显示IP地址
configTime(gmtOffset_sec, daylightOffset_sec, ntpServer); // 配置NTP自动同步时间
server.on("/", handleRoot); // 注册路由
server.on("/api", handleApi);
server.begin(); // 启动网页服务器
}
void loop() {
server.handleClient();
unsigned long now = millis();
// 间隔读取传感器引脚状态
if (now - lastTime >= interval) {
lastTime = now;
bool val = digitalRead(PIR_PIN); // 读取OUT引脚电平
// 状态发生变化
if (val != pirState) {
pirState = val;
if (pirState) {
// 检测到经过
String t = getBeijingTime();
addHistory(t);
Serial.println("【触发】" + t);
digitalWrite(LED_BUILTIN, LOW);
} else {
// 离开
Serial.println("【离开】");
digitalWrite(LED_BUILTIN, HIGH);
}
}
}
}四、效果演示

我要赚赏金
