基于 ESP32-C61 开发板的 SHT40 温湿度传感器设计
一、项目介绍和创意介绍
本项目的主题是“智能家居:基于 ESP32-C61 开发板的 SHT40 温湿度传感器设计”。从应用角度看,温湿度是家庭环境感知中最基础、最实用的一类数据。无论是卧室、书房、婴儿房、储藏间还是阳台花卉养护场景,用户都希望能够随时了解当前空气状态,并进一步联动空调、新风、加湿器、除湿机等设备,实现更加舒适和节能的居住体验。
本项目的核心创意并不是单纯“把传感器数值读出来”,而是把“环境采集 + 无线联网 + 本地网页实时展示”整合成一个轻量级、可扩展的智能家居节点。系统基于 ESP32-C61 开发板作为主控,使用 SHT40 传感器采集温湿度数据,通过 Wi-Fi 接入家庭局域网,并在开发板内部运行一个轻量 HTTP 服务器。用户无需安装额外 App,只需让手机或电脑与开发板处于同一网络,直接在浏览器中访问设备 IP 地址,就可以实时查看温度和湿度变化。
这样的设计具有几个明显优势。第一,部署简单,开发板上电联网后即可工作;第二,交互门槛低,浏览器就是界面;第三,后续扩展空间大,可以继续叠加历史数据存储、异常告警、阈值联动、云端同步以及多房间组网等功能。因此,这个项目既适合作为智能家居入门作品,也具备继续工程化演进的潜力。
二、使用到的硬件介绍
1. ESP32-C61 开发板
ESP32-C61 是乐鑫推出的一款具备 Wi-Fi 6、Bluetooth LE 以及 802.15.4 能力的低功耗 SoC,适合智能家居、物联网边缘节点等应用场景。本项目中,ESP32-C61 主要承担以下任务:
- 作为主控制器完成系统初始化。
- 通过 I2C 总线与 SHT40 传感器通信。
- 通过 Wi-Fi 连接家庭路由器。
- 运行嵌入式 Web 服务器和 WebSocket 服务。
- 定时采集并对外发布温湿度数据。
ESP32-C61 之所以适合该项目,一方面是因为其网络能力强,方便做本地网页展示或后续云接入;另一方面是 ESP-IDF 开发框架成熟,组件生态完善,适合快速完成原型设计和后续迭代。
2. SHT40 温湿度传感器
SHT40 是 Sensirion 推出的数字温湿度传感器,具有精度高、响应快、体积小、接口简单等特点。它采用 I2C 接口与主控通信,适合智能家居环境监测类应用。本项目中,SHT40 的优势主要体现在以下几点:
- 温湿度数据输出数字化,避免模拟量采集中的噪声影响。
- I2C 接口简单,只需 SDA、SCL 两根信号线即可完成通信。
- 器件功耗较低,适合后续低功耗场景扩展。
- 自带 CRC 校验机制,便于提高数据可靠性。
3. 硬件连接方式
SHT40 使用 I2C0 与 ESP32-C61 通信,连接关系如下:
- SDA 连接到 GPIO6。
- SCL 连接到 GPIO7。
- 传感器 I2C 地址使用 0x44。
- I2C 内部上拉在软件中启用。
三、方案框图和项目设计思路介绍
1. 系统方案框图

2. 设计思路
本项目采用“采集层、数据层、通信层、展示层”分层设计思路。
- 采集层:由 SHT40 和 I2C 驱动构成,负责定时获取温湿度原始数据。
- 数据层:通过共享数据结构缓存当前最新测量值,并使用互斥锁保证不同任务之间的数据一致性。
- 通信层:ESP32-C61 以 Wi-Fi Station 模式接入路由器,设备联网成功后启动 HTTP 服务器和 WebSocket 服务。
- 展示层:浏览器访问开发板 IP 后加载内置网页,网页通过 WebSocket 实时接收 JSON 格式数据并刷新显示。
这种设计的好处是模块职责清晰,传感器采集逻辑和网页展示逻辑互不干扰。即便后续要把网页改成 MQTT 上云、蓝牙广播或者接入 Home Assistant,也只需要替换部分通信或展示模块,而不必大幅修改底层采集驱动。
四、软件流程图和关键代码介绍
1. 软件总体流程图

2. 关键代码说明
(1)Wi-Fi 连接流程
在主程序中,设备工作在 Wi-Fi Station 模式。系统通过事件回调处理三类关键状态:启动连接、断线重连、获取 IP。断线后会按照设定的最大重试次数自动重新连接,这种设计能够提高系统在家庭网络环境下的稳定性。联网成功后,程序会打印获取到的 IP 地址,作为浏览器访问入口。
这里的设计亮点在于:Wi-Fi 连接并不是简单阻塞式等待,而是通过事件组同步系统状态。这样既符合 ESP-IDF 的事件驱动风格,也方便后续继续扩展更多网络状态处理逻辑,比如联网失败后的配网模式切换等。
(2)SHT40 采集任务
项目使用独立任务完成传感器采样。任务启动时先初始化 I2C 主总线,再完成 SHT40 驱动初始化,并读取一次传感器序列号用于设备识别。之后每隔 2 秒进行一次高精度测量,成功时把温度和湿度写入共享数据区,失败时打印错误日志。
该模块的实现体现了两个工程细节:
- 驱动中加入了 CRC 校验,能够发现总线传输异常或数据损坏问题。
- 测量延时根据 SHT40 不同精度模式分别设置,符合数据手册要求,保证了采样可靠性。
(3)共享数据设计
为了让采样任务和 Web 服务器任务安全访问同一份数据,项目定义了一个全局的 sensor_data_t 结构体,内部包含温度、湿度、有效标志位以及互斥锁。写入数据时先上锁,读取数据时同样先上锁,从而避免任务切换造成的数据竞争。
这部分虽然代码量不大,却是系统稳定运行的关键。如果没有互斥锁保护,就可能出现网页读取到一半更新中的数据,导致显示异常甚至隐蔽 bug。
(4)Web 服务器与网页实时显示
联网成功后,系统启动内置 HTTP 服务器。根路径 / 返回一份嵌入在 C 字符串中的 HTML 页面,页面包含温度卡片、湿度卡片以及连接状态显示。网页加载后会自动建立 WebSocket 连接,请求 /ws 接口;开发板端则由广播任务每 2 秒向所有在线客户端发送一帧 JSON 数据,例如:
{"t":25.36,"h":58.42}
前端收到数据后,使用 JavaScript 动态更新页面中的温度和湿度数值。相比传统轮询方式,WebSocket 能显著降低通信开销,并让数据刷新更加及时,适合实时监测类页面。
3. 关键代码价值总结
从软件架构上看,本项目最核心的代码价值主要体现在以下三个方面:
- 基于 ESP-IDF 的事件驱动式 Wi-Fi 联网机制。
- 带 CRC 校验的 SHT40 驱动封装,提高了采集可靠性。
- 通过 HTTP + WebSocket 实现嵌入式设备到浏览器的实时数据可视化。
传感器SHT40驱动代码
/*
* SHT4x Temperature & Humidity Sensor Driver Implementation
* Based on Sensirion SHT4x Datasheet Version 6.4
*/
#include "sht4x.h"
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include <string.h>
static const char *TAG = "sht4x";
/* CRC-8 parameters from datasheet Table 7:
* Polynomial: 0x31, Init: 0xFF, Final XOR: 0x00 */
static uint8_t sht4x_crc8(const uint8_t *data, size_t len)
{
uint8_t crc = 0xFF;
for (size_t i = 0; i < len; i++) {
crc ^= data[i];
for (int bit = 0; bit < 8; bit++) {
if (crc & 0x80) {
crc = (crc << 1) ^ 0x31;
} else {
crc = crc << 1;
}
}
}
return crc;
}
static esp_err_t sht4x_send_cmd(sht4x_t *dev, uint8_t cmd)
{
return i2c_master_transmit(dev->i2c_dev, &cmd, 1, 100);
}
static esp_err_t sht4x_read_response(sht4x_t *dev, uint8_t *data, size_t len)
{
return i2c_master_receive(dev->i2c_dev, data, len, 100);
}
/* Parse 6 bytes: [T_MSB, T_LSB, T_CRC, RH_MSB, RH_LSB, RH_CRC] */
static esp_err_t sht4x_parse_measurement(const uint8_t *raw, float *temperature, float *humidity)
{
/* Verify temperature CRC */
uint8_t crc_t = sht4x_crc8(&raw[0], 2);
if (crc_t != raw[2]) {
ESP_LOGE(TAG, "Temperature CRC mismatch: calc=0x%02X, recv=0x%02X", crc_t, raw[2]);
return ESP_ERR_INVALID_CRC;
}
/* Verify humidity CRC */
uint8_t crc_rh = sht4x_crc8(&raw[3], 2);
if (crc_rh != raw[5]) {
ESP_LOGE(TAG, "Humidity CRC mismatch: calc=0x%02X, recv=0x%02X", crc_rh, raw[5]);
return ESP_ERR_INVALID_CRC;
}
/* Convert raw values using datasheet formulas (section 4.6) */
uint16_t t_ticks = ((uint16_t)raw[0] << 8) | raw[1];
uint16_t rh_ticks = ((uint16_t)raw[3] << 8) | raw[4];
/* T = -45 + 175 * t_ticks / 65535 */
*temperature = -45.0f + 175.0f * (float)t_ticks / 65535.0f;
/* RH = -6 + 125 * rh_ticks / 65535, clamped to [0, 100] */
float rh = -6.0f + 125.0f * (float)rh_ticks / 65535.0f;
if (rh > 100.0f) rh = 100.0f;
if (rh < 0.0f) rh = 0.0f;
*humidity = rh;
return ESP_OK;
}
esp_err_t sht4x_init(sht4x_t *dev, const sht4x_config_t *config)
{
if (dev == NULL || config == NULL) {
return ESP_ERR_INVALID_ARG;
}
i2c_device_config_t dev_cfg = {
.dev_addr_length = I2C_ADDR_BIT_LEN_7,
.device_address = config->i2c_addr,
.scl_speed_hz = 400000, /* Fast mode 400 kHz */
};
esp_err_t ret = i2c_master_bus_add_device(config->i2c_bus, &dev_cfg, &dev->i2c_dev);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to add I2C device: %s", esp_err_to_name(ret));
return ret;
}
/* Soft reset to ensure known state */
ret = sht4x_soft_reset(dev);
if (ret != ESP_OK) {
ESP_LOGW(TAG, "Soft reset failed (sensor may not be connected): %s", esp_err_to_name(ret));
}
ESP_LOGI(TAG, "SHT4x initialized at address 0x%02X", config->i2c_addr);
return ESP_OK;
}
esp_err_t sht4x_soft_reset(sht4x_t *dev)
{
esp_err_t ret = sht4x_send_cmd(dev, 0x94);
if (ret != ESP_OK) return ret;
/* Wait max 1 ms for reset (datasheet Table 5).
* +1 to guarantee minimum delay when tick period > 1ms */
vTaskDelay(pdMS_TO_TICKS(2) + 1);
return ESP_OK;
}
esp_err_t sht4x_read_serial(sht4x_t *dev, uint32_t *serial)
{
if (dev == NULL || serial == NULL) {
return ESP_ERR_INVALID_ARG;
}
esp_err_t ret = sht4x_send_cmd(dev, 0x89);
if (ret != ESP_OK) return ret;
vTaskDelay(pdMS_TO_TICKS(2) + 1);
uint8_t raw[6];
ret = sht4x_read_response(dev, raw, 6);
if (ret != ESP_OK) return ret;
/* Verify CRCs for both 16-bit words */
if (sht4x_crc8(&raw[0], 2) != raw[2]) {
ESP_LOGE(TAG, "Serial number word 1 CRC mismatch");
return ESP_ERR_INVALID_CRC;
}
if (sht4x_crc8(&raw[3], 2) != raw[5]) {
ESP_LOGE(TAG, "Serial number word 2 CRC mismatch");
return ESP_ERR_INVALID_CRC;
}
*serial = ((uint32_t)raw[0] << 24) | ((uint32_t)raw[1] << 16) |
((uint32_t)raw[3] << 8) | raw[4];
return ESP_OK;
}
esp_err_t sht4x_measure(sht4x_t *dev, sht4x_repeatability_t repeatability,
float *temperature, float *humidity)
{
if (dev == NULL || temperature == NULL || humidity == NULL) {
return ESP_ERR_INVALID_ARG;
}
esp_err_t ret = sht4x_send_cmd(dev, (uint8_t)repeatability);
if (ret != ESP_OK) return ret;
/* Wait for measurement to complete (datasheet Table 5).
* +1 to guarantee minimum delay when tick period > 1ms */
switch (repeatability) {
case SHT4X_REPEATABILITY_HIGH:
vTaskDelay(pdMS_TO_TICKS(10) + 1); /* max 8.3 ms */
break;
case SHT4X_REPEATABILITY_MEDIUM:
vTaskDelay(pdMS_TO_TICKS(5) + 1); /* max 4.5 ms */
break;
case SHT4X_REPEATABILITY_LOW:
vTaskDelay(pdMS_TO_TICKS(2) + 1); /* max 1.6 ms */
break;
}
uint8_t raw[6];
ret = sht4x_read_response(dev, raw, 6);
if (ret != ESP_OK) return ret;
return sht4x_parse_measurement(raw, temperature, humidity);
}
esp_err_t sht4x_heater_measure(sht4x_t *dev, sht4x_heater_cmd_t heater_cmd,
float *temperature, float *humidity)
{
if (dev == NULL || temperature == NULL || humidity == NULL) {
return ESP_ERR_INVALID_ARG;
}
esp_err_t ret = sht4x_send_cmd(dev, (uint8_t)heater_cmd);
if (ret != ESP_OK) return ret;
/* Determine wait time based on heater command */
uint32_t wait_ms;
switch (heater_cmd) {
case SHT4X_HEATER_200MW_1S:
case SHT4X_HEATER_110MW_1S:
case SHT4X_HEATER_20MW_1S:
wait_ms = 1100; /* 1s heater + measurement */
break;
case SHT4X_HEATER_200MW_100MS:
case SHT4X_HEATER_110MW_100MS:
case SHT4X_HEATER_20MW_100MS:
wait_ms = 120; /* 0.1s heater + measurement */
break;
default:
return ESP_ERR_INVALID_ARG;
}
vTaskDelay(pdMS_TO_TICKS(wait_ms));
uint8_t raw[6];
ret = sht4x_read_response(dev, raw, 6);
if (ret != ESP_OK) return ret;
return sht4x_parse_measurement(raw, temperature, humidity);
}
webserver,启动ws_broadcast_task任务监听websocket连接和数据收发。
#include <string.h>
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_http_server.h"
#include "esp_log.h"
#include "sensor_data.h"
#include "webserver.h"
static const char *TAG = "webserver";
/* Store server handle */
static httpd_handle_t s_server = NULL;
/* ---- Embedded HTML page ---- */
static const char index_html[] =
"<!DOCTYPE html>"
"<html><head><meta charset=\"UTF-8\">"
"<meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">"
"<title>ESP32 Sensor Monitor</title>"
"<style>"
"*{margin:0;padding:0;box-sizing:border-box}"
"body{font-family:system-ui,-apple-system,sans-serif;background:#0f172a;color:#e2e8f0;"
"display:flex;justify-content:center;align-items:center;min-height:100vh}"
".container{text-align:center;padding:2rem}"
"h1{font-size:1.5rem;margin-bottom:2rem;color:#94a3b8}"
".cards{display:flex;gap:1.5rem;flex-wrap:wrap;justify-content:center}"
".card{background:#1e293b;border-radius:1rem;padding:2rem 3rem;min-width:200px;"
"box-shadow:0 4px 24px rgba(0,0,0,.3)}"
".card.temp{border-top:4px solid #f97316}"
".card.humi{border-top:4px solid #06b6d4}"
".label{font-size:.85rem;color:#94a3b8;margin-bottom:.5rem}"
".value{font-size:2.8rem;font-weight:700}"
".unit{font-size:1rem;color:#64748b;margin-left:.2rem}"
".temp .value{color:#fb923c}"
".humi .value{color:#22d3ee}"
".status{margin-top:1.5rem;font-size:.8rem;color:#475569}"
".status.connected{color:#22c55e}"
".status.disconnected{color:#ef4444}"
"</style></head><body>"
"<div class=\"container\">"
"<h1>🌡 ESP32 Sensor Monitor</h1>"
"<div class=\"cards\">"
"<div class=\"card temp\">"
"<div class=\"label\">Temperature</div>"
"<div class=\"value\" id=\"temp\">--<span class=\"unit\">°C</span></div>"
"</div>"
"<div class=\"card humi\">"
"<div class=\"label\">Humidity</div>"
"<div class=\"value\" id=\"humi\">--<span class=\"unit\">%%RH</span></div>"
"</div></div>"
"<div class=\"status disconnected\" id=\"status\">Connecting...</div>"
"</div>"
"<script>"
"var ws,st=document.getElementById('status');"
"function connect(){"
"ws=new WebSocket('ws://'+location.host+'/ws');"
"ws.onopen=function(){st.textContent='Connected';st.className='status connected'};"
"ws.onclose=function(){st.textContent='Disconnected';st.className='status disconnected';"
"setTimeout(connect,2000)};"
"ws.onerror=function(){ws.close()};"
"ws.onmessage=function(e){"
"var d=JSON.parse(e.data);"
"document.getElementById('temp').innerHTML=d.t.toFixed(1)+'<span class=\"unit\">°C</span>';"
"document.getElementById('humi').innerHTML=d.h.toFixed(1)+'<span class=\"unit\">%%RH</span>';"
"}}"
"connect();"
"</script></body></html>";
/* ---- HTTP GET / handler ---- */
static esp_err_t index_get_handler(httpd_req_t *req)
{
httpd_resp_set_type(req, "text/html");
httpd_resp_send(req, index_html, sizeof(index_html) - 1);
return ESP_OK;
}
/* ---- WebSocket handler ---- */
/*
* Async send structure: we use httpd_queue_work to push data from the
* sensor broadcast task into the httpd thread context.
*/
typedef struct {
httpd_handle_t hd;
int fd;
} ws_client_t;
#define MAX_WS_CLIENTS 4
static ws_client_t s_ws_clients[MAX_WS_CLIENTS];
static int s_ws_client_count = 0;
static esp_err_t ws_handler(httpd_req_t *req)
{
/* First invocation for a new client: handshake */
if (req->method == HTTP_GET) {
ESP_LOGI(TAG, "WS handshake fd=%d", httpd_req_to_sockfd(req));
/* Register client */
if (s_ws_client_count < MAX_WS_CLIENTS) {
s_ws_clients[s_ws_client_count].hd = req->handle;
s_ws_clients[s_ws_client_count].fd = httpd_req_to_sockfd(req);
s_ws_client_count++;
}
return ESP_OK;
}
/* Receive frame (we only expect pong / close) */
httpd_ws_frame_t ws_pkt;
memset(&ws_pkt, 0, sizeof(ws_pkt));
ws_pkt.type = HTTPD_WS_TYPE_TEXT;
esp_err_t ret = httpd_ws_recv_frame(req, &ws_pkt, 0);
if (ret != ESP_OK) {
return ret;
}
/* We don't process incoming data, just acknowledge */
return ESP_OK;
}
/* Remove disconnected clients */
static void ws_remove_client(int idx)
{
if (idx < s_ws_client_count - 1) {
s_ws_clients[idx] = s_ws_clients[s_ws_client_count - 1];
}
s_ws_client_count--;
}
/* Broadcast task: periodically sends sensor data to all WS clients */
static void ws_broadcast_task(void *arg)
{
char buf[64];
float temperature, humidity;
while (1) {
vTaskDelay(pdMS_TO_TICKS(2000));
if (s_ws_client_count == 0 || s_server == NULL) {
continue;
}
if (!sensor_data_get(&temperature, &humidity)) {
continue;
}
int len = snprintf(buf, sizeof(buf), "{\"t\":%.2f,\"h\":%.2f}", temperature, humidity);
httpd_ws_frame_t ws_pkt = {
.final = true,
.fragmented = false,
.type = HTTPD_WS_TYPE_TEXT,
.payload = (uint8_t *)buf,
.len = len,
};
for (int i = s_ws_client_count - 1; i >= 0; i--) {
esp_err_t ret = httpd_ws_send_frame_async(s_ws_clients[i].hd, s_ws_clients[i].fd, &ws_pkt);
if (ret != ESP_OK) {
ESP_LOGW(TAG, "WS send failed fd=%d, removing client", s_ws_clients[i].fd);
ws_remove_client(i);
}
}
}
}
/* ---- URI definitions ---- */
static const httpd_uri_t uri_index = {
.uri = "/",
.method = HTTP_GET,
.handler = index_get_handler,
};
static const httpd_uri_t uri_ws = {
.uri = "/ws",
.method = HTTP_GET,
.handler = ws_handler,
.is_websocket = true,
};
/* ---- Public API ---- */
esp_err_t webserver_start(void)
{
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
config.max_open_sockets = MAX_WS_CLIENTS + 1;
ESP_LOGI(TAG, "Starting HTTP server on port %d", config.server_port);
esp_err_t ret = httpd_start(&s_server, &config);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to start HTTP server: %s", esp_err_to_name(ret));
return ret;
}
httpd_register_uri_handler(s_server, &uri_index);
httpd_register_uri_handler(s_server, &uri_ws);
/* Start broadcast task */
xTaskCreate(ws_broadcast_task, "ws_bcast", 4096, NULL, 5, NULL);
ESP_LOGI(TAG, "HTTP server started");
return ESP_OK;
}
void webserver_stop(void)
{
if (s_server) {
httpd_stop(s_server);
s_server = NULL;
s_ws_client_count = 0;
}
}
主函数启动传感器读取任务,webserver任务,初始化WiFi
void app_main(void)
{
//Initialize NVS
esp_err_t ret = nvs_flash_init();
if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
ESP_ERROR_CHECK(nvs_flash_erase());
ret = nvs_flash_init();
}
ESP_ERROR_CHECK(ret);
/* Print chip information */
esp_chip_info_t chip_info;
uint32_t flash_size;
esp_chip_info(&chip_info);
printf("This is %s chip with %d CPU core(s), %s%s%s%s, ",
CONFIG_IDF_TARGET,
chip_info.cores,
(chip_info.features & CHIP_FEATURE_WIFI_BGN) ? "WiFi/" : "",
(chip_info.features & CHIP_FEATURE_BT) ? "BT" : "",
(chip_info.features & CHIP_FEATURE_BLE) ? "BLE" : "",
(chip_info.features & CHIP_FEATURE_IEEE802154) ? ", 802.15.4 (Zigbee/Thread)" : "");
unsigned major_rev = chip_info.revision / 100;
unsigned minor_rev = chip_info.revision % 100;
printf("silicon revision v%d.%d, ", major_rev, minor_rev);
if(esp_flash_get_size(NULL, &flash_size) != ESP_OK) {
printf("Get flash size failed");
return;
}
ESP_LOGI(TAG, "%" PRIu32 "MB %s flash\n", flash_size / (uint32_t)(1024 * 1024),
(chip_info.features & CHIP_FEATURE_EMB_FLASH) ? "embedded" : "external");\
ESP_LOGI(TAG, "Minimum free heap size: %" PRIu32 " bytes", esp_get_minimum_free_heap_size());
if (CONFIG_LOG_MAXIMUM_LEVEL > CONFIG_LOG_DEFAULT_LEVEL) {
/* If you only want to open more logs in the wifi module, you need to make the max level greater than the default level,
* and call esp_log_level_set() before esp_wifi_init() to improve the log level of the wifi module. */
esp_log_level_set("wifi", CONFIG_LOG_MAXIMUM_LEVEL);
}
/* Initialize shared sensor data */
sensor_data_init();
/* Start SHT40 sensor task (before WiFi, which blocks until connected) */
xTaskCreate(sht40_task, "sht40_task", 4096, NULL, 5, NULL);
ESP_LOGI(TAG, "ESP_WIFI_MODE_STA");
wifi_init_sta();
/* Start HTTP server with WebSocket after WiFi is connected */
webserver_start();
for (;;) {
vTaskDelay(2000 / portTICK_PERIOD_MS);
// ESP_LOGI(TAG, "free heap size: %" PRIu32 " bytes", esp_get_free_internal_heap_size());
}
}
五、功能展示图及说明
- 硬件实物连接:使用杜邦线连接传感器SHT40

- 串口日志图:展示系统启动、联网成功、周期输出温湿度日志。

- 上电启动,成功连接WiFi网络



- 资源占用情况:开发板自带8MB flash,目前使用了765KB左右;内存使用了不到120KB。还有不少余量,为后面功能扩展也留有充足的空间

- 浏览器页面:实时温度、湿度卡片和WebSocket连接状态。

- 多终端访问:当前最大支持4个客户端同时访问
设备上电后自动连接到预设 Wi-Fi 网络,浏览器访问开发板获取到的 IP 地址,即可看到实时刷新温湿度数据的网页界面。页面采用卡片式布局显示温度和湿度,底部显示 WebSocket 连接状态。当传感器数据发生更新时,通过websocket网页端同步刷新,无需手动重载页面。
六、设计中遇到的难题和解决方法
1. Wi-Fi 联网稳定性问题
在物联网项目中,Wi-Fi 环境并不总是稳定的,路由器重启、信号波动、密码配置错误都可能导致设备无法正常工作。为解决这个问题,项目中使用事件回调配合重连计数方式,在掉线后自动尝试重新连接,并通过事件组区分“成功获取 IP”和“达到最大重试次数失败”两种状态。这种方式相比简单地死循环调用连接函数更规范,也更利于后续扩展。
2. 传感器通信可靠性问题
I2C 总线在连线较长、接触不良或者干扰较大时,容易出现通信异常。如果直接把读到的数据当作有效值使用,可能造成测量结果失真。针对这个问题,项目在 SHT40 驱动中加入了 CRC 校验逻辑,对温度和湿度两个数据字分别进行校验。如果 CRC 不匹配,就直接返回错误并记录日志,从源头上保证数据可靠性。
3. 多任务共享数据问题
传感器采样任务和 Web 服务广播任务会并发访问同一份温湿度数据。如果缺少同步保护,就有可能产生数据竞争。项目采用互斥锁机制封装共享数据读写接口,既保证了结构清晰,又避免了不同任务之间互相影响。
4. 前端与嵌入式结合问题
嵌入式项目往往更重视底层驱动,而忽略用户可视化体验。本项目在有限资源下把 HTML、CSS、JavaScript 页面直接嵌入固件中,结合 WebSocket 实现实时显示。这一过程中需要平衡页面效果、代码体积和通信效率。最终实现的网页虽然轻量,但已经具备较好的可读性和实时性,为后续升级为更完整的控制面板打下了基础。
七、对本次竞赛的心得体会、意见与建议
通过这次论坛活动,我对“从一个传感器出发,逐步搭建完整物联网应用”的过程有了更深的理解。以前做单片机项目时,往往更关注某一个功能点是否能跑通,例如是否能读到寄存器、是否能点亮 LED;而这次项目让我更加关注系统完整性,包括硬件接口、驱动可靠性、联网流程、前端展示和用户体验。这种“从功能验证走向系统设计”的思考方式,对以后做智能家居、边缘网关甚至更复杂的嵌入式产品都很有帮助。
本次项目也让我感受到 ESP32-C61 和 ESP-IDF 生态的成熟。一方面,官方框架对 Wi-Fi、事件循环、HTTP 服务器等基础能力支持完善,开发者可以把更多精力放在应用逻辑本身;另一方面,像 SHT40 这样的外设驱动也很适合通过组件化方式封装,便于复用和维护。对个人开发者来说,这大大降低了从想法到作品落地的门槛。
总体来说,这次竞赛不仅是一次作品提交,更像是一次完整的工程实践训练。通过本项目,我完成了从硬件连接、驱动编写、联网调试到网页可视化展示的全流程开发,也更加明确了未来可以继续深挖的方向,例如加入阈值告警、历史曲线、手机适配界面、Home Assistant 接入以及多传感器节点扩展等。可以说,这个项目虽然体量不大,但非常贴近真实智能家居产品的基本形态,具有较强的学习价值和继续迭代的意义。
八、总结
本项目以 ESP32-C61 开发板和 SHT40 温湿度传感器为基础,实现了一个面向智能家居场景的环境监测节点。系统能够完成温湿度采集、Wi-Fi 联网、本地网页展示和 WebSocket 实时推送,整体方案简洁、实用、可扩展。通过该项目,不仅验证了 ESP32-C61 在智能家居场景中的应用能力,也展示了嵌入式系统结合 Web 技术后所带来的直观交互优势。
如果后续继续完善,本项目完全可以从“课程/竞赛作品”进一步升级为“可落地的家庭环境监测终端”,并与更多智能家居设备形成联动,具备良好的应用前景。