一、项目概述

随着物联网的普及,如何让设备在本地自动响应环境变化、并能远程维护升级,成为常见需求。本文示例展示了一套基于 ESP8266 的解决方案,集成了:

  1. DHT11 温度传感:实时采集室内温度
  2. PIR 人体红外检测:监测区域内是否有人活动
  3. 继电器控制:根据环境条件(温度与是否有人)自动切换负载
  4. Web 界面:通过浏览器查看状态、手动开关继电器
  5. OTA 无线升级:后续固件更新无需连线,直接通过 Wi‑Fi 推送

二、核心功能拆解

1. 硬件与引脚配置

  • DHT11:数据线接 D5,负责温度采样
  • PIR(SR505/HC‑SR501):输出接 D6,用于人体活动检测
  • 继电器:控制脚接 D7,根据逻辑驱动外部电路
  • 电源及 Wi‑Fi:模块以 5 V (VIN)供电,运行在 STA 模式,连入家庭网络

2. 自动控制逻辑

unsigned long lastMotionTime = 0;
const unsigned long detectionWindow = 0.5 * 60 * 1000UL; // 30 秒

// 每次检测到 PIR 输出高电平时,更新时间戳
if (digitalRead(PIRPIN)) {
  lastMotionTime = millis();
}

// 读取温度
float newTemp = dht.readTemperature();
if (!isnan(newTemp)) temperature = newTemp;

// 根据“最后一次有人时间距今”与温度阈值决定继电器开关
if ((millis() - lastMotionTime) <= detectionWindow && temperature > 25.0) {
  digitalWrite(RELAYPIN, HIGH);  // 打开
} else {
  digitalWrite(RELAYPIN, LOW);   // 关闭
}
  • 时间窗口:设为 0.5 分钟,保证只要有人经过的 30 秒内,且温度高于 25 ℃,继电器便处于“打开”状态。
  • 温度保护:当温度低于阈值或无人离开超过窗口时自动关闭,避免长期开启。

3. 网页状态与手动控制

// 根路径:返回包含当前温度、继电器状态、上次检测到人时间的 HTML
server.on("/", handleRoot);

// /on 和 /off:通过 POST 请求手动打开/关闭继电器
server.on("/on",  HTTP_POST, handleOn);
server.on("/off", HTTP_POST, handleOff);
  • 页面上有“手动打开”“手动关闭”按钮,用户点击后触发相应处理函数,写入数字引脚状态。
  • 同时提示“自动控制逻辑每 2 秒会再次评估”,保证本地传感逻辑的优先级。

4. OTA 无线升级

ESP8266远程烧录实现

#include <ArduinoOTA.h>

ArduinoOTA.setHostname("esp8266-ota");
ArduinoOTA.onStart([]{ Serial.println("开始 OTA"); });
ArduinoOTA.onEnd(  []{ Serial.println("OTA 完成"); });
ArduinoOTA.onError([](ota_error_t err){ /* 打印错误 */ });
ArduinoOTA.begin();

// 在 loop() 中
ArduinoOTA.handle();
  • 首次烧录:需通过 USB 刷入上述代码,使设备具备 OTA 能力。
  • 后续升级:Arduino IDE 会自动检测到局域网内的 esp8266-ota 设备,选择网络端口即可直接上传,无需物理连线。

三、性能与应用场景

  • 资源消耗:OTA 与 WebServer 仅占用少量 RAM (~10 KB) 与极短的 CPU 调度时间,对原有传感与控制逻辑影响可忽略。
  • 典型场景
  • 智能风扇或排气扇:当室内有人且温度高于阈值时自动启动
  • 室内通风控制:结合湿度传感或 CO₂ 传感模块扩展
  • 远程运维:固件更新、阈值调整可在线完成

四、后续可扩展方向

  1. HTTPS OTA:使用加密传输,保护固件完整性
  2. MQTT 接入:与云平台对接,实现多设备管理与告警
  3. 阈值动态配置:把温度阈值、时间窗口等参数暴露在网页上动态修改
  4. 多传感融合:加入光照、空气质量等传感器,提高环境智能化水平

五、代码

#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>
#include <ArduinoOTA.h>
#include <DHT.h>

// ===== WiFi 配置 =====
const char* ssid = "yang1234";
const char* password = "y123456789";

// ===== 引脚配置 =====
#define DHTPIN    D5
#define DHTTYPE   DHT11
#define PIRPIN    D6
#define RELAYPIN  D7

DHT dht(DHTPIN, DHTTYPE);
ESP8266WebServer server(80);

// ===== 控制变量 =====
const float temperatureThreshold     = 25.0;           // 温度阈值 (°C)
const unsigned long detectionWindow  = 30 * 1000UL;    // 30 秒窗口
const unsigned long checkInterval    = 2000;           // 检测间隔 (ms)

// ===== 状态变量 =====
unsigned long lastMotionTime   = 0;  // 上次有效触发时刻
unsigned long lastCheckTime    = 0;  // 上次逻辑检查时刻
float        temperature       = 0.0;
bool         relayState        = false;
bool         lastRelayState    = false;

// ===== 去抖参数 =====
static bool         lastPirState     = LOW;
static unsigned long lastPirTrigger   = 0;
const unsigned long pirMinInterval    = 1000; // 最少 1 秒内不重复触发

// ===== 网页处理 =====
void handleRoot() {
  unsigned long secsSince = (millis() - lastMotionTime) / 1000;
  String html = "<!DOCTYPE html><html><head><meta charset='utf-8'><title>ESP8266 控制界面</title>"
                "<style>body{font-family:sans-serif;}</style></head><body>";
  html += "<h2>ESP8266 状态查看</h2>";
  html += "<p><strong>当前温度:</strong>" + String(temperature,1) + " °C (> "
          + String(temperatureThreshold) + "°C 时自动开)</p>";
  html += "<p><strong>距离上次检测到活动:</strong>" + String(secsSince)
          + " 秒 (需<=" + String(detectionWindow/1000) + " 秒才有效)</p>";
  html += "<p><strong>继电器:</strong>" + String(relayState?"打开 ✅":"关闭 ⛔") + "</p>";
  html += "<p><em>⚙️ 每 " + String(checkInterval/1000)
          + " 秒检测一次,满足两条件才开继电器</em></p>";
  html += "</body></html>";
  server.send(200, "text/html", html);
}

void setup() {
  Serial.begin(115200);
  dht.begin();
  pinMode(PIRPIN, INPUT);            // 外部 10k 下拉
  pinMode(RELAYPIN, OUTPUT);
  digitalWrite(RELAYPIN, LOW);       // 默认关闭

  // 防止刚上电就触发
  lastMotionTime = millis() - detectionWindow - 1;

  // 连接 WiFi
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
  Serial.print("连接 WiFi");
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("\nWiFi 已连接,IP:" + WiFi.localIP().toString());

  // OTA 配置
  ArduinoOTA.setHostname("esp8266-ota");
  ArduinoOTA.onStart([](){ Serial.println("开始 OTA"); });
  ArduinoOTA.onEnd(  [](){ Serial.println("\nOTA 完成,重启中"); });
  ArduinoOTA.onError([](ota_error_t e){ Serial.printf("OTA 错误[%u]\n", e); });
  ArduinoOTA.begin();
  Serial.println("OTA 准备就绪");

  // Web 服务器
  server.on("/", handleRoot);
  server.begin();
  Serial.println("Web 服务器启动");
}

void loop() {
  unsigned long now = millis();
  ArduinoOTA.handle();
  server.handleClient();

  // 非阻塞定期检查
  if (now - lastCheckTime < checkInterval) return;
  lastCheckTime = now;

  // —— PIR 上升沿 + 最小间隔去抖 —— 
  bool pir = digitalRead(PIRPIN);
  if (pir && !lastPirState && (now - lastPirTrigger > pirMinInterval)) {
    lastMotionTime = now;
    lastPirTrigger = now;
    Serial.println("👤 有效人体触发");
  }
  lastPirState = pir;

  // —— 读取温度 —— 
  float t = dht.readTemperature();
  if (!isnan(t)) {
    temperature = t;
  } else {
    Serial.println("⚠️ 读取温度失败");
  }

  // —— 自动控制逻辑 —— 
  bool shouldOn = (now - lastMotionTime <= detectionWindow) && (temperature > temperatureThreshold);
  if (shouldOn != relayState) {
    relayState = shouldOn;
    digitalWrite(RELAYPIN, relayState ? HIGH : LOW);
    Serial.printf("%s 继电器\n", relayState ? "✅ 打开" : "⛔ 关闭");
  }
}