Chapter 10

实战:环境监测站

综合运用前九章知识,构建一个具备实际生产价值的完整 IoT 项目

项目概述与硬件清单

本章构建一个室内/室外环境监测站,具备以下功能:实时采集温度、湿度、气压和空气质量;在本地 E-ink 显示屏上显示当前数值;通过 MQTT 上报到云端并展示在 Web Dashboard;支持 OTA 远程升级;深度睡眠省电,电池可用数月。

ESP32-WROOM-32
主控芯片,240MHz 双核,4MB Flash,Wi-Fi + BLE。建议使用 38 引脚版 DevKit。
BME280
博世出品的高精度温湿度气压传感器(I2C 地址 0x76/0x77)。温度 ±1°C、湿度 ±3%RH、气压 ±1hPa。注意:BMP280 无湿度功能,务必选 BME280。
MQ135
空气质量传感器,对 NH3、NOX、苯、CO2 等有响应,模拟输出连接 ADC1。需要约 24 小时预热才能读数稳定。
2.9 寸 E-ink 显示屏
SPI 接口,296×128 分辨率,黑白双色。断电后图像保持不变(约数月),刷新功耗极低(约 26mJ/次)。适合间歇更新的信息展示。
3.7V 锂电池 + TP4056 充电模块
2000mAh 锂电池供电,TP4056 实现充电保护。深度睡眠每 10 分钟唤醒一次时,理论电池寿命约 6 个月。

系统架构设计

┌─────────────────────── ESP32 系统架构 ───────────────────────┐ │ │ │ 传感器层 核心任务层 通信层 │ │ ────────── ────────── ──────── │ │ BME280 (I2C) sensor_task wifi_task │ │ MQ135 (ADC1) ──► (读取、滤波) ──► mqtt_task │ │ RTC 时间戳 display_task http_server │ │ (E-ink 刷新) │ │ │ │ ┌─── 数据队列 Queue ───┐ │ │ │ sensor_data_t │ │ │ └──────────────────────┘ │ │ │ │ 存储层 │ │ ────── │ │ NVS: Wi-Fi凭证、设备ID、上报间隔 │ │ LittleFS: Web Dashboard HTML/CSS/JS │ │ RTC SRAM: 跨睡眠数据(boot count、上次读数) │ │ │ │ 电源管理 │ │ ──────── │ │ 活跃期: 读传感器 → 更新 E-ink → 发 MQTT → 进深度睡眠 │ │ 睡眠期: 仅 RTC 定时器运行,10μA │ └───────────────────────────────────────────────────────────────┘

项目文件结构

env_monitor/
├── CMakeLists.txt
├── partitions.csv        # 自定义分区表,含 OTA + LittleFS
├── sdkconfig.defaults    # 预设 menuconfig 选项
├── data/                 # LittleFS 内容(烧录时打包)
│   └── index.html        # Web Dashboard 页面
└── main/
    ├── CMakeLists.txt
    ├── main.c            # app_main,系统初始化
    ├── sensor.c / .h     # BME280 + MQ135 驱动封装
    ├── display.c / .h    # E-ink 显示逻辑
    ├── mqtt_client.c / .h
    ├── http_server.c / .h # Web Dashboard HTTP 服务器
    ├── ota.c / .h
    ├── storage.c / .h    # NVS 配置读写封装
    └── power.c / .h      # 深度睡眠控制

核心数据结构

/* sensor.h */
#pragma once
#include "esp_err.h"

typedef struct {
    float    temperature;    // °C
    float    humidity;       // %RH
    float    pressure;       // hPa
    uint16_t aqi_raw;        // MQ135 ADC 原始值 0~4095
    uint32_t timestamp;      // Unix 时间戳
    int      battery_mv;     // 电池电压 mV
} env_data_t;

typedef struct {
    char     wifi_ssid[33];
    char     wifi_pass[65];
    char     mqtt_uri[128];
    char     device_id[20];
    uint32_t sleep_seconds;   // 深度睡眠时长,默认 600
    bool     eink_enabled;
} app_config_t;

esp_err_t sensor_init(void);
esp_err_t sensor_read(env_data_t *data);

多任务架构实现

/* main.c — 系统初始化与任务编排 */
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
#include "freertos/event_groups.h"
#include "nvs_flash.h"
#include "sensor.h"
#include "storage.h"

#define EVT_WIFI_UP   BIT0
#define EVT_TIME_SYNC BIT1

QueueHandle_t       g_data_queue;
EventGroupHandle_t  g_system_events;
app_config_t        g_config;

/* 传感器任务:每 5 秒读一次,发队列 */
static void sensor_task(void *arg)
{
    sensor_init();
    env_data_t data;
    while (1) {
        if (sensor_read(&data) == ESP_OK) {
            xQueueOverwrite(g_data_queue, &data);   // 覆盖旧数据(取最新)
            ESP_LOGI("SNS", "T=%.1f H=%.1f P=%.1f AQI=%d",
                data.temperature, data.humidity, data.pressure, data.aqi_raw);
        }
        vTaskDelay(pdMS_TO_TICKS(5000));
    }
}

/* 显示任务:等待新数据后刷新 E-ink */
static void display_task(void *arg)
{
    eink_init();
    env_data_t data;
    while (1) {
        if (xQueuePeek(g_data_queue, &data, pdMS_TO_TICKS(10000))) {
            eink_display_env(&data);    // 刷新 E-ink 显示,约 1~2 秒
        }
    }
}

/* MQTT 上报任务:连接成功后定期发布 */
static void mqtt_task(void *arg)
{
    xEventGroupWaitBits(g_system_events,
        EVT_WIFI_UP | EVT_TIME_SYNC, pdFALSE, pdTRUE, portMAX_DELAY);

    mqtt_app_start(g_config.mqtt_uri, g_config.device_id);

    env_data_t data;
    while (1) {
        if (xQueuePeek(g_data_queue, &data, pdMS_TO_TICKS(1000))) {
            mqtt_publish_env_data(&data);
        }
        vTaskDelay(pdMS_TO_TICKS(30000));   // 每 30 秒上报一次
    }
}

void app_main(void)
{
    /* 基础初始化 */
    nvs_flash_init();
    storage_load_config(&g_config);

    /* 共享资源 */
    g_data_queue    = xQueueCreate(1, sizeof(env_data_t));
    g_system_events = xEventGroupCreate();

    /* Wi-Fi + NTP */
    wifi_init_sta(g_config.wifi_ssid, g_config.wifi_pass, g_system_events);
    sntp_init(g_system_events);

    /* 启动任务 */
    xTaskCreatePinnedToCore(sensor_task,  "sensor",  4096, NULL, 6, NULL, 1);
    xTaskCreatePinnedToCore(display_task, "display", 4096, NULL, 4, NULL, 1);
    xTaskCreatePinnedToCore(mqtt_task,    "mqtt",    8192, NULL, 5, NULL, 0);

    /* HTTP 服务器(Web Dashboard + OTA)*/
    http_server_start();
}

Web Dashboard(ESP32 做 HTTP 服务器)

ESP32 可以用 esp_http_server 组件在局域网内托管一个简单的 Web Dashboard,手机浏览器直接访问 ESP32 的 IP 即可查看实时数据,无需云端。

#include "esp_http_server.h"

/* GET /api/data → 返回 JSON */
static esp_err_t api_data_handler(httpd_req_t *req)
{
    env_data_t data;
    xQueuePeek(g_data_queue, &data, 0);

    char resp[256];
    snprintf(resp, sizeof(resp),
        "{\"temperature\":%.1f,\"humidity\":%.1f,"
        "\"pressure\":%.1f,\"aqi\":%d,\"battery\":%d}",
        data.temperature, data.humidity, data.pressure,
        data.aqi_raw, data.battery_mv);

    httpd_resp_set_type(req, "application/json");
    httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*");
    return httpd_resp_sendstr(req, resp);
}

/* GET / → 返回 LittleFS 中的 index.html */
static esp_err_t index_handler(httpd_req_t *req)
{
    FILE *f = fopen("/littlefs/index.html", "r");
    if (!f) {
        httpd_resp_send_404(req);
        return ESP_OK;
    }
    httpd_resp_set_type(req, "text/html");
    char buf[512];
    size_t n;
    while ((n = fread(buf, 1, sizeof(buf), f)) > 0) {
        httpd_resp_send_chunk(req, buf, n);
    }
    fclose(f);
    return httpd_resp_send_chunk(req, NULL, 0);
}

void http_server_start(void)
{
    httpd_config_t config = HTTPD_DEFAULT_CONFIG();
    config.stack_size = 8192;
    httpd_handle_t server = NULL;
    httpd_start(&server, &config);

    httpd_uri_t uri_index = { .uri = "/",         .method = HTTP_GET, .handler = index_handler };
    httpd_uri_t uri_data  = { .uri = "/api/data", .method = HTTP_GET, .handler = api_data_handler };
    httpd_register_uri_handler(server, &uri_index);
    httpd_register_uri_handler(server, &uri_data);
}

OTA 自动升级集成

在 HTTP 服务器上增加 OTA 端点,或定时检查 OTA 服务器,实现远程自动升级:

#include "esp_https_ota.h"

/* POST /ota → 触发 OTA 升级 */
static esp_err_t ota_trigger_handler(httpd_req_t *req)
{
    char url[256];
    int ret = httpd_req_recv(req, url, sizeof(url) - 1);
    if (ret <= 0) return ESP_FAIL;
    url[ret] = '\0';

    esp_http_client_config_t ota_http_cfg = {
        .url     = url,
        .timeout_ms = 30000,
    };
    esp_https_ota_config_t ota_cfg = { .http_config = &ota_http_cfg };

    httpd_resp_sendstr(req, "OTA 开始,设备将重启...");

    esp_err_t err = esp_https_ota(&ota_cfg);
    if (err == ESP_OK) {
        esp_restart();
    }
    return ESP_OK;
}

电路接线参考

硬件接线图(ESP32-WROOM-32D): ESP32 BME280(I2C) GPIO21(SDA) ── SDA GPIO22(SCL) ── SCL 3.3V ── VCC GND ── GND SDO → GND(地址 0x76) ESP32 MQ135(空气质量) GPIO32(ADC1_CH4) ── AO(模拟输出) 5V ── VCC(注意:MQ135 需 5V) GND ── GND ESP32 E-ink(SPI) GPIO23(MOSI) ── DIN GPIO18(CLK) ── CLK GPIO5(CS) ── CS GPIO17(DC) ── DC GPIO16(RST) ── RST GPIO4(BUSY) ── BUSY 3.3V ── 3.3V GND ── GND 电源 3.7V 锂电池(+) ── TP4056(BAT+) TP4056(OUT+) ── ESP32 VIN(含稳压) ADC1通道3(GPIO39) ── 电压分压器(测量电池电压)
电池电压测量

用两个电阻构成分压器(如 100kΩ + 100kΩ),将 3.3~4.2V 的锂电池电压降至 1.65~2.1V,连接到 ADC1 测量(注意用 ADC_ATTEN_DB_6 衰减,量程 0~2.2V)。然后根据电压对照表转换为剩余电量百分比。

项目扩展思路

扩展 1

多节点 Mesh 网络

使用 ESP-MDF(Mesh Development Framework)将多个 ESP32 节点组成自愈合的 Wi-Fi Mesh,扩大监测范围,数据汇聚到根节点上传云端。

扩展 2

Grafana 可视化

MQTT Broker → Telegraf → InfluxDB → Grafana 数据流,实现专业级时序数据可视化与告警,完全开源,可自托管。

扩展 3

Home Assistant 集成

通过 MQTT Discovery 协议自动发现设备,无需编写插件即可接入 Home Assistant 智能家居平台,支持自动化联动。

扩展 4

机器学习异常检测

将长期历史数据用 TensorFlow Lite 或 Edge Impulse 训练异常检测模型,部署到 ESP32-S3 的 AI 加速器,实现本地推理告警。

恭喜完成 ESP32 物联网开发教程!

你已经掌握了 ESP32 从硬件架构到完整 IoT 产品的全链路开发技能:GPIO/ADC/PWM 控制、Wi-Fi 联网与 HTTP/MQTT 通信、BLE 无线传感器、FreeRTOS 多任务、传感器驱动、Flash 存储、低功耗深度睡眠,以及将所有技能整合到完整项目的工程实践。IoT 的世界无比广阔,祝你在物联网领域创造出有价值的产品!