ESP32智能插座Web上位机:DIY免App控制与功率监测
2026/9/5 8:04:10 网站建设 项目流程

1. 项目概述与需求拆解

1.1 为什么我会做这个项目?

说实在的,市面上现成的智能插座一抓一大把,几十块钱就能买到,但真用起来总会碰到几个让人抓狂的问题:要么必须依赖厂商云平台,服务器一挂插座就变“孤儿”;要么App更新频繁、广告满天飞;要么数据全部走别人服务器,隐私基本裸奔。我自己家里有一台老式热水器和一台电暖器,想做个定时通断控制,还要能实时看到功率统计,翻遍市售产品都没找到完全满意的。于是“自己动手做一个ESP32智能插座”就被我列上了日程。

选择ESP32作为主控的核心原因有三个:第一,ESP32自带Wi-Fi和蓝牙双模通信,BLE用于本地近场配置,Wi-Fi用于连接路由器,省掉外挂通信模块的成本和体积;第二,ESP32的处理能力足够跑一个轻量级Web服务器,不需要额外接树莓派之类的上位机;第三,开发环境成熟,ESP-IDF、Arduino框架、MicroPython都能玩,网上资料一抓一大把,踩坑也有前人垫着。

这里要特别说明一下“Web上位机”这个词。传统意义的“上位机”是PC端的上位机软件,通过串口或网络和下位机通信。而这个项目的Web上位机,指的是把管理控制界面做成网页,跑在ESP32内部的Web服务器上。用户不需要安装任何App,只要手机、平板、电脑浏览器能连上同一个局域网,甚至通过端口映射从外网访问,就能直接打开插座的控制面板,实时看到功率、电流、电压、开关状态,还能配置定时任务。这种方案最大的好处就是跨平台、零安装、免维护,而且不依赖任何第三方云服务。

1.2 项目要解决的核心问题

这个项目本质上解决三个层面的问题,缺一不可。

第一层是硬件基础:要把220V交流电安全地控制起来,通过继电器或可控硅实现通断,同时用电流互感器和电压采样电路把电能参数采集出来,送到ESP32的ADC引脚进行数字化。这一层涉及到强弱电混合设计,安全规范非常重要,我后续会详细讲。

第二层是软件控制:ESP32需要运行一个Web服务器,接收浏览器的HTTP请求,解析后控制GPIO电平翻转继电器;同时要周期性地读取ADC采样结果,计算功率、电流等参数,通过WebSocket或轮询方式推送到前端页面实时刷新。这一层是最核心的“上位机”功能,也是我在标题里特别强调的设计主体。

第三层是体验闭环:要让一个完全不懂硬件的人拿到手也会用——插上电自动连网,浏览器输入一个IP地址就能看到简洁明了的控制面板,能配置定时开关,掉电重启后能恢复之前的状态,固件升级不用拆壳。这三个层面叠加起来,才算得上是一个可日常使用的智能插座,而不是一个跑通了Demo的试验板。

1.3 技术选型对比

在正式开工之前,我花了点时间对比了几种主控和通信方案的组合,这里把对比结果直接列出来,给后来者一个参考。

方案组合优点缺点适用场景
ESP32 + Web服务器免App、跨平台、开发效率高局域网访问受限,外网需端口映射家庭局域网控制首选
ESP32 + MQTT + 本地HomeAssistant可接入智能家居中控,自动化强需要单独跑HA服务器,配置门槛高已有HA环境的玩家
ESP32 + BLE App功耗低、配网简单需要开发或下载App,不适合远距离近距离控制场景
ESP8266 + 云平台开发简单,有现成SDK依赖第三方云,有断服风险,隐私堪忧快速原型验证不推荐长期用

从表格能看出来,每一种方案都有自己的优劣势,没有绝对的好和坏。我最终选择ESP32 + Web服务器,不仅因为开发效率高,还因为ESP32的SRAM空间相对充裕一些,能扛得住HTTP Server和WebSocket同时跑,加上文件系统存网页静态资源也够用。如果你只是控制一个灯,ESP8266刷个Tasmota固件就够用了,但要做功率监测、定时任务、Web界面这个量级的功能,ESP32是更稳的选择。

2. 硬件选型与电路设计

2.1 主控板与外壳物料清单

先上一份完整的物料清单,都是我在实际项目中验证过可行的型号,照着买基本不会踩坑。

  • 主控板:ESP32 DevKitC V4开发板(模组可以是ESP32-WROOM-32,4MB Flash即可)
  • 继电器:5V线圈/10A触点容量的模块,我用的是一路松乐SRD-05VDC-SL-C
  • 电流采样:非隔离电流互感器ZMPT101B或者使用ACS712霍尔电流传感器模块
  • 电压采样:经过阻容降压后由ADC读取,精度要求不高的话也可以不接电压采样
  • 电源模块:HLK-PM01 AC-DC降压模块,输入85~265V AC,输出5V/3W
  • 外壳:86型墙壁插座底盒加面板,自己钻孔装LED指示灯
  • 辅助器件:10K微调电位器、0.1uF去耦电容、光耦隔离PC817、MOC3023可控硅驱动光耦

这里必须提醒一句:如果你直接把220V市电引入开发板,绝对不是插一个USB线那么简单。HLK-PM01这类模块输出的是隔离5V,可以直接给ESP32的5V引脚供电,但前提是后端的低压电路和前端的高压电路在物理上要分开,爬电距离要留够,外壳要选阻燃材料。千万别图省事用那种几块钱的非隔离阻容降压模块来给ESP32供电,调试时万用表笔一滑,整个板子加上电脑USB口都可能报废。

2.2 电流采样与功率计算原理

功率计算的核心在于电流采样的准确性。我最初用的是ACS712霍尔电流传感器模块,量程选的是20A版本,输出灵敏度是100mV/A,也就是电流每变化1A,模块输出电压变化0.1V。ESP32内置ADC的参考电压可以配置为3.3V,12位分辨率,对应量化精度理论值是3.3V/4096≈0.8mV。按这个算下来,电流分辨率大约为0.8mV/100mV每安培,约8mA,理论上够用。

ACS712模块输出的是半电压偏置,也就是在0A时输出VCC/2(如果VCC取5V,则是2.5V),这个2.5V不能直接接到ESP32的ADC输入,因为ESP32的ADC输入范围是0~3.3V,接入2.5V到3.9V(对应20A满量程)的电压区间会超量程。所以我给ACS712输出端加了一个分压电阻网络,把2.5~3.9V这个区间映射到1.65~2.58V,这样即使满量程也不会超过3.3V。

具体分压计算:取R1=10K串联、R2=20K接地,输出电压Vout = Vin * R2/(R1+R2) = Vin * 2/3。也就是说2.5V变1.67V,3.9V变2.6V,都在ADC量程内。分压之后灵敏度也相应变为原来的2/3,即约66.7mV/A。这个参数在实际代码校准的时候要写到配置里。

如果你追求更高的计量精度,不想自己算半天,还有一个更省事的选择:直接使用电能计量芯片HLW8032或BL0940。这类芯片内部自带ADC和功率计算逻辑,通过UART直接输出电流、电压、功率、电量等数据,ESP32只需要解析串口数据。相比之下,自己用ADC采样再做DFT计算不仅开发量大,精度还不一定比得上专用芯片。我这里为了演示Web上位机的核心功能,用了ACS712方案,但生产级产品强烈建议用专用计量芯片。

2.3 继电器驱动与安全隔离

ESP32的GPIO输出能力很有限,推不动继电器线圈,所以必须加三极管或ULN2003做驱动。我的电路结构是:GPIO16经过1K电阻接S8050三极管基极,三极管集电极接继电器线圈一端,线圈另一端接5V电源,线圈两端反并联一个1N4007二极管,用于吸收继电器断电瞬间产生的反向电动势。反向电动势如果不处理,轻则干扰ESP32运行导致重启,重则击穿三极管。

控制信号和强电之间我加了PC817光耦做二次隔离。为什么不直接拿GPIO去连强电?因为继电器虽然隔离了线圈和触点,但一旦触点打火或者爬电,高压很容易窜到低压侧,烧掉ESP32就得不偿失了。光耦把电气联系完全切断,即使强电侧发生故障,最多也就是烧掉光耦,不会损伤主控。

安全规范方面有三个硬性要求,我在PCB和外壳制作时严格执行:

  1. 强电和弱电区域的走线间距不小于6mm,高压铜箔和低压铜箔分层布置
  2. 继电器触点出线端用硅胶线引出,加2A保险丝做过流保护
  3. 外壳用3D打印件或阻燃ABS料,所有高压端子用热缩管包裹,裸露金属部分必须不外露

3. Web上位机总体架构设计

3.1 上位机功能模块划分

整套Web上位机的软件逻辑可以划分为四个模块:HTTP配置服务、WebSocket实时服务、定时任务调度、参数采集与校准。每个模块各司其职,又通过统一的事件循环协同工作。

HTTP配置服务负责两个事情:一是首次上电时提供一个配网页面,手机连上ESP32的热点后可以直接填写Wi-Fi的SSID和密码;二是常连接模式下提供控制页面、定时任务页面、设备信息页面的静态资源。配网和数据控制分开,这是很多原厂固件也采用的方式,好处是初次使用门槛低,不需要串口线敲AT指令。

WebSocket实时服务是整个上位机的亮点。传统方式下,前端要看到实时功率数据,最简单的做法是每秒用Ajax轮询一次REST接口。但轮询的缺点很明显:HTTP请求头开销大,ESP32这种资源受限设备扛不住高频率并发请求;而且数据不是主动推送给前端的,实时性差。WebSocket连接建立之后,服务器可以随时把最新的采样数据推给浏览器,浏览器不需要频繁发起新连接,一个连接长期保持,双向通信,对ESP32的处理压力反而更小。

定时任务调度模块负责解析用户配置的定时计划,比如“每天早上7:00开启热水器,持续30分钟后关闭”。ESP32内部维护一个时间基准,通过NTP从公网同步标准时间,到点后触发继电器动作。这里需要注意,ESP32内部没有RTC电池备份,断电后时间会丢失,所以每次上电后必须自动连NTP服务器校时,否则定时功能就是废的。

参数采集与校准模块把ADC原始值换算成真实的电流、功率数据。原始ADC值受温度漂移、器件误差、参考电压波动影响比较大,必须做校准。我的做法是在硬件电路上预留一个微调电位器,用已知功率的负载(比如一个100W灯泡)作为基准,调节电位器使Web界面显示的功率和实际功率一致。这个校准过程我在后面章节会详细演示。

3.2 内存与外设资源规划

ESP32虽然比ESP8266内存大不少,但也不是无限用的。我给网页前端做了一个相对完整的控制面板,静态资源加起来大概是:HTML约8KB、CSS约4KB、JavaScript约12KB,一张ECharts图表库的压缩包就要约300KB。如果你把ECharts整个塞进ESP32的Flash文件系统,4MB Flash会显得有点紧张,而且浏览器加载首屏会明显变慢。

我的优化方案是前端图表不用ECharts,自己用Canvas画一个极简的实时曲线,代码压缩后不到3KB,仅保留功率折线图这一个核心需求。控制面板的整体包体控制在30KB以内,放SPIFFS文件系统完全没压力。如果你实在想用ECharts,也可以把它放到公网CDN上,但这就带来了“断网就不能用”的问题,违背我做本地化控制器的初衷。

内存方面,ESP32的SRAM总共有520KB左右,但实际可用可能只有300多KB,因为蓝牙协议栈、Wi-Fi协议栈、TCP/IP协议栈都会占用内存。WebSocket连接大约占用4~8KB缓冲区,HTTP Server每处理一个请求临时占用2KB左右。如果同时并发五六个浏览器连接,再叠加ADC采样阵列,内存会比较吃紧。我的经验是:用esp_http_server组件的异步API,不要开同步阻塞请求;WebSocket消息帧的长度尽量控制在256字节以内,超过这个值就需要分帧,分帧处理逻辑复杂不说,还容易把内存挤爆。

3.3 通信协议设计

通信协议的设计直接决定了上位机的稳定性和扩展性。先看HTTP REST接口部分:

方法路径功能请求体响应体
GET/api/status获取设备状态{"status":"on","power":156.3,"current":0.68,"voltage":229.5}
POST/api/control控制开关{"status":"on"}同上
GET/api/tasks获取定时任务列表任务JSON数组
POST/api/tasks新增定时任务任务JSON对象创建后的任务
DELETE/api/tasks/{id}删除定时任务删除结果

WebSocket接口这边,我定义了三种消息类型:设备状态推送(服务端到客户端)、控制指令(客户端到服务端)、时间校准请求。所有消息都用JSON封装,结构大概长这样:

{ "type": "status", "payload": { "status": "on", "power": 156.3, "voltage": 229.5, "current": 0.68, "timestamp": 1699523200 } }

JSON的解析在ESP32上不算轻量,之前C代码里用cJSON库,一个接口调用嵌套多了,堆碎片化会比较严重。所以我的建议是:能用字符拼接解决的简单情况,就别全走JSON;WebSocket服务端定时推送状态可以做成固定格式的CSV字符串,比如"on,156.3,229.5,0.68,1699523200",前端用split拆一下就行。这样处理既快又稳,省掉的cJSON解析开销可以让采样频率从每秒1次提升到每秒5次。

4. ESP32端核心代码实现

4.1 开发环境准备

我用的开发框架是ESP-IDF v5.0版本,而不是Arduino框架。不是说Arduino不好,Arduino上手快、库多,非常适合快速原型验证。但做这种包含HTTP Server、WebSocket、文件系统、定时任务调度的完整项目时,ESP-IDF对内存管理、任务优先级、编译时裁剪的控制更精细,出问题好排查,最终稳定性也好很多。

环境搭建方面,最推荐的方式是用VS Code + ESP-IDF扩展插件。新建工程之后,有两个地方需要自己改动配置:

  • menuconfig里把Component config → LWIP → TCP/IP配置的TCP_SEND_BUFFER_SIZE适当调大一点,否则WebSocket大帧会发送失败
  • Partition Table选择Huge App (3MB No OTA),在main分区里编译进Web前端资源

工程目录结构我按下面的方式组织,清晰也方便维护:

project/ ├── main/ │ ├── CMakeLists.txt │ ├── app_main.c // 入口逻辑、任务创建 │ ├── wifi_manager.c // WiFi连接与配网热点模式 │ ├── http_server.c // HTTP REST API │ ├── websocket_server.c // WebSocket实时推送 │ ├── relay_control.c // 继电器GPIO控制与状态管理 │ ├── power_monitor.c // ADC采样与功率计算 │ ├── timer_scheduler.c // 定时任务调度 │ └── storage.c // NVS读写配置 ├── main/web/ │ ├── index.html │ ├── app.js │ └── style.css └── CMakeLists.txt

4.2 继电器控制模块

继电器控制逻辑本身很简单,就是GPIO_write,但我在上面封装了一层状态机。为什么需要状态机?因为继电器的吸合和断开是有物理时间的,典型继电器吸合时间约10ms,断开时间约5ms。在过程中如果反复切换,触点会快速抖动产生电弧,严重缩短继电器寿命。所以我的状态机把“切换中”作为一种状态,在这段时间内屏蔽新的控制指令,强制等待100ms后再接受指令。

开关控制的代码核心如下,已经精简过:

typedef enum { RELAY_OFF, RELAY_TURNING_ON, RELAY_ON, RELAY_TURNING_OFF } relay_state_t; void relay_set(bool target) { if (target == true && state == RELAY_OFF) { gpio_set_level(RELAY_GPIO, 1); state = RELAY_TURNING_ON; vTaskDelay(pdMS_TO_TICKS(100)); state = RELAY_ON; save_status_to_nvs(true); } else if (target == false && state == RELAY_ON) { gpio_set_level(RELAY_GPIO, 0); state = RELAY_TURNING_OFF; vTaskDelay(pdMS_TO_TICKS(100)); state = RELAY_OFF; save_status_to_nvs(false); } }

这个NVS保存状态的操作特别重要。ESP32断电后SRAM里的所有变量都会丢失,如果不保存开关状态,断电再上电之后继电器默认恢复为关闭。但像热水器这种设备,如果断电前是打开的,来电后我们希望它恢复原状态而不是傻乎乎地关闭。所以每次切换状态后都调用save_status_to_nvs(),上电初始化时从NVS读回上次的状态,再决定是否恢复开启。这个细节比想象中重要,至少我自己漏掉之后,三天两头被家里人抱怨“热水器来电后没自动重启”。

4.3 WebSocket服务器端实现

在ESP-IDF框架下使用WebSocket比较简单,组件esp_http_server中已经封装了HTTPD_WS_TYPE协议处理。我先注册一个URI句柄,然后设置handler = ws_handler,当客户端连接时,该handler会收到三种类型的WebSocket事件:HTTPD_WS_EVENT_CONNECTED、HTTPD_WS_EVENT_DISCONNECTED、HTTPD_WS_EVENT_DATA。

这个handler回调是在httpd task上下文中执行的,不能在回调里做耗时操作。所以我把每次收到客户端数据包之后要做的事情(比如解析控制指令、切换继电器)通过一个FreeRTOS Queue发送给一个工作线程去处理,立刻返回。典型的做法如下:

static esp_err_t ws_handler(httpd_req_t *req) { if (req->handler == HTTPD_WS_EVENT_CONNECTED) { printf("WebSocket client connected\n"); // 连接建立后立刻推送一次当前状态 ws_send_status(req); } else if (req->handler == HTTPD_WS_EVENT_DISCONNECTED) { printf("WebSocket client disconnected\n"); } else if (req->handler == HTTPD_WS_EVENT_DATA) { // 解析客户端发来的控制指令 char buf[128] = {0}; int len = httpd_ws_recv_frame(req, (httpd_ws_frame_t*)&frame, MAX_LEN); if (len > 0) { // 将指令推入队列,避免阻塞 xQueueSend(control_queue, buf, 0); } } return ESP_OK; } void ws_control_task(void *param) { char msg[128]; while (1) { if (xQueueReceive(control_queue, msg, portMAX_DELAY)) { if (strstr(msg, "ON")) relay_set(true); else if (strstr(msg, "OFF")) relay_set(false); } } }

定时推送这里我再多写几句。我和很多开发者交流过,大家都容易犯一个错误:在同一个httpd task里既处理HTTP请求又定时推WebSocket帧,结果就是任务一旦被一个慢请求卡住,所有推送全部阻塞。正确做法是创建一个独立任务,用vTaskDelay做周期定时,每200ms把最新采样数据通过httpd_ws_send_frame_async发送出去。异步发送的好处是即使某个客户端接收端卡住,也不会阻塞其他客户端的发送。

4.4 ADC采样与功率计算核心代码

ADC采样要避免一个典型坑:ESP32的ADC输入引脚不要悬空调试,必须接一个确定的直流偏置电压,否则读数会乱跳。我这里是ACS712输出经过分压电阻后直接接入ADC引脚,分压网络本身就给了一个稳定的偏置,问题不大。但如果后续调试时拔掉了传感器线,ESP32的ADC读数会满量程乱跳,这时候程序里的滤波算法要能自动剔除异常值。

ADC采样我用了多次采样求平均的软件方式,配合一个简单的滑动窗口滤波器:

#define ADC_SAMPLE_COUNT 64 float read_current_amp() { uint32_t sum = 0; for (int i = 0; i < ADC_SAMPLE_COUNT; i++) { sum += adc1_get_raw(ADC1_CHANNEL_0); vTaskDelay(pdMS_TO_TICKS(1)); // 留出采样保持时间 } float adc_avg = (float)sum / ADC_SAMPLE_COUNT; float voltage_at_pin = adc_avg / 4095.0 * 3.3; float sensor_voltage = voltage_at_pin / 0.6667; // 还原分压前的电压 float current_a = (sensor_voltage - 2.5) / 0.1; // ACS712 20A量程,100mV/A return current_a; }

功率计算这里有一个很容易被忽略的问题:家用市电是交流电,电压和电流之间存在相位差,特别是带了感性负载(比如电机、变压器)的时候,直接用“电压有效值 × 电流有效值”算出来的是视在功率,单位是VA,不是实际做功的有功功率(W)。像我做的这种简易插座,如果不做相位采样,显示出来的功率数值比实际耗电偏高,尤其空载或者轻载时偏差比例很大。彻底解决这个问题要同时采样电压和电流波形,做FFT计算相位差,开发量和代码复杂度会大很多。我的取舍是:项目定位是“监测与控制”而不是“计量级电表”,视在功率误差在可接受范围内,Web界面上明确标注“视在功率”,避免误导。如果你的项目需要计量级精度,建议直接换HLW8032这类专用计量芯片,省心得多。

5. 前端Web界面设计与交互

5.1 页面布局与视觉风格

Web前端我不打算做得花里胡哨,智能插座的核心交互就几个:看开关状态、按开关、看功率曲线、配置定时。所以页面布局遵循极简原则:顶部一个设备状态栏,中间一个大大的开关按钮,下面一个Canvas实时功率折线图,再往下是定时任务表格和添加表单。

配色方案上,我选了深色主题:#1e1e1e作为背景,#ff9800作为主色调,开关状态用亮绿色和灰色区分。深色主题的好处是夜里看控制界面不刺眼,OLED电视遥控器那种体验。字体用系统默认的无衬线字体,不引外部字体文件,减少加载负担。

这里有个移动端适配的坑:ESP32挂载的Web服务器处理并发能力有限,如果同一个Wi-Fi网络里有多个设备同时打开控制页面,ESP32可能响应变慢甚至崩溃。我的做法是前端做一个“连接互斥”提示,当第二个客户端连上WebSocket时,第一个客户端会收到一个提示“已有其他设备正在控制”,然后自动只保留只读模式。这个细节虽然代码不多,但在家庭环境里特别实用,能避免两个人同时操作导致的混乱。

5.2 实时数据刷新的两种方式对比

在实时数据刷新这块,我对比了两种方案的实现成本:

方案一:REST接口轮询。前端每500ms请求一次/api/status,拿到JSON数据更新DOM。这种方式实现很简单,一个setInterval加一个fetch就搞定了。但对ESP32这种单片机来说,每秒2个HTTP请求维持一段时间没毛病,但如果有多个客户端同时轮询,HTTP Server的handler很快就会被占满,其他请求全部排队超时。

方案二:WebSocket长连接。建立连接后,ESP32主动推送数据,前端只需要监听onmessage事件。这种方式对单片机更友好,因为一个连接长时间占用,没有反复建立TCP连接的开销。但如果某一个移动设备在WebSocket连接状态下息屏了,TCP连接可能已经断了但服务端不知道,会继续尝试往这个socket发送数据,导致发送缓冲区塞满阻塞其他任务。

最终我选择的是方案二为主、方案一兜底的混合策略:正常情况下靠WebSocket推送;当检测到WebSocket断线后,前端自动降级到10秒一次的REST轮询,直到重新建立WebSocket连接。这样既保证了实时性的体验,又兼顾了极端情况下的可用性。这个降级逻辑写起来也不复杂,一个状态变量加两个定时器就能搞定。

5.3 前端控制指令与防误触设计

开关控制的POST请求是一个高风险操作——你点一下“开”,继电器真的会去吸合220V电器的电路。为了防误触,我在前端做了两个保护层:

第一层是按钮本身的“滑动确认”:用户不能直接点击开关图标来切换状态,必须按住开关按钮并向下滑动到指定区域才执行切换,类似手机上的滑块解锁。这个交互在触屏设备上体验很自然,误触概率大幅降低。

第二层是WebSocket控制指令加重放保护:前端每次发起控制指令时,生成一个单调递增的序列号附带在JSON里,服务端收到指令后只处理大于最后一次已执行序列号的指令。这样即使某条WebSocket消息因网络抖动重发了多次,继电器也只会执行一次切换,不会出现“开-关-开”这种讨厌的抖动。

这两层保护看起来是锦上添花,实际用下来非常救命。我有一次深夜半睡半醒状态,用手机控制电暖器,如果没有滑动确认,一个翻身压到屏幕就直接触发了。所以诚心建议所有做智能硬件Web控制界面的朋友,都把防误触做进设计文档,而不是当作可选项。

6. 配网模式与状态恢复机制

6.1 首次上电的SoftAP配网设计

智能插座第一次上电时,它不知道你的Wi-Fi密码,直接联网是不可行的。常见的解决方案有三种:手机App通过蓝牙配网、串口调试工具通过AT指令配网、Web配网页面。我的项目选择了Web配网,因为用户不需要装任何App,也不需要数据线,拿起手机浏览器就能解决。

配网流程是这样的:ESP32首次检测到NVS里没有保存有效的Wi-Fi凭据时,进入SoftAP模式,创建一个名为“ESP32-Socket-Config”的热点(SSID)。手机连接这个热点后,浏览器访问192.168.4.1就会弹出一个本地网页,让用户选择自己的Wi-Fi网络并输入密码。提交后,ESP32尝试连接路由器,连接成功就返回提示并退出配网模式,把凭据加密存储到NVS中。

这个流程现在看起来顺理成章,但我在实现时踩过一个坑:ESP32进入SoftAP模式后的默认IP地址是192.168.4.1,但很多手机浏览器会强制跳转到某些门户验证页面,或者提示“网络没有互联网连接”后自动断开热点。解决方法是前端配网页面加一段自动检测逻辑:页面加载时先尝试fetch一个本地的探针文件/ping,如果失败则提示用户关闭“自动断开无互联网热点”的开关。另外最好在热点名前加上标识,让用户一眼认出这是自家设备。

6.2 NVS状态存储设计

NVS(Non-Volatile Storage)是ESP32上用于存储小量键值对数据的Flash分区,非常适合存放配网信息、开关状态、定时任务等配置。我用它存了以下几组键:

键名类型说明
wifi_ssidstring路由器名称
wifi_passwordstring路由器密码
relay_statusbool上次继电器状态
task_countu8定时任务数量
task_1~task_8blob定时任务配置
calibratedbool是否已完成功率校准

NVS写入次数有寿命限制,这一点容易被忽视。ESP32的NVS底层是Flash,虽然有多级磨损均衡设计,但频繁写入(比如每秒存一次功率值)仍会加速Flash磨损。我的设计原则是只存配置和控制状态,不存高频采集数据。功率采样数据是瞬态的,重启后丢了也无所谓,没必要占用Flash写入次数。

6.3 断电重启后的快速恢复流程

上电恢复的完整逻辑分成五个步骤:

  1. 读取NVS中保存的Wi-Fi凭据,尝试连接路由器,连接超时时间为10秒
  2. 连接成功后,通过SNTP同步标准时间,等待时间校准完成
  3. 读取relay_status,如果上次是开启状态且定时任务逻辑决定现在应该开启,则恢复继电器为开启;否则保持关闭
  4. 加载定时任务列表到内存,重置下一次任务触发时间
  5. 启动HTTP服务器和WebSocket服务器,开始监听控制请求

这个流程中,任何时候失败都不能导致系统卡死。比如Wi-Fi密码在NVS里是旧密码,路由器换了新密码,这时候恢复流程会陷入连接超时循环,此时应该触发“配置恢复模式”——检测到连续三次连接失败后自动重新打开SoftAP配网,同时保持继电器处于安全关闭状态。我特意把这条逻辑写在主循环的最前面,确保用户在设备异常时手里永远有一把“钥匙”:手机连热点重新配置。

7. 定时任务调度机制

7.1 任务模型与数据结构

定时任务的模型我设计得比市面上大多数智能插座灵活一点:每条任务包含触发条件、动作、持续时间三个要素。触发条件支持两种:每周固定时间触发(比如每周一到周五的早上7:30)和单次倒计时触发(比如5分钟后开启,持续30分钟后关闭)。动作只有开和关两个,但配合持续时间就能实现“开启15分钟后自动关闭”这种最常见的场景。

任务数据用结构体存储,方便序列化和读取:

typedef struct { uint8_t id; bool enabled; uint8_t days_of_week; // 位掩码:bit0~bit6对应周一~周日 uint8_t hour; uint8_t minute; bool action; // true=开,false=关 uint16_t duration_min; // 动作保持时间,0表示永久保持 } timer_task_t;

这里有一个设计细节:为什么不用std::vector或链表,而用固定数组?因为ESP32的堆内存是碎片化的,频繁动态分配小结构体容易产生内存碎片,跑几天后可能分配失败。我的做法是预分配8个任务的数组,够绝大多数家庭场景用了。如果超过8条任务,前端会提示“任务数已达上限”,从产品角度讲这完全可以接受。

7.2 调度器的时间基准与触发逻辑

触发逻辑的核心是:每次循环或定时器回调里,遍历全部已启用的任务,把当前时间(时、分、星期)和任务配置比对,精确到分钟级即可。如果匹配,就执行对应动作并算出持续时间的到期时刻,挂入一个“等待到期”队列。到期后再次执行反向动作。

为什么不需要秒级精度?家庭用电控制场景,提前或延迟几十秒开关设备,对热水器、照明、电暖器这类负载来说没有实质影响。而且秒级精度要求意味着每次循环都需要高精度时间基准,ESP32虽然也有高精度定时器,但那会额外引入一个定时器中断,代码复杂度上升,实际收益却几乎没有。所以我的设计是每10秒检查一次任务列表,到点了就触发,误差控制在10秒以内。

SNTP校时的细节也提一下:ESP32默认的SNTP获取时间之后只更新软件时钟,不会保存到RTC备份寄存器的电池上。所以断电重启后时间会恢复到UTC初始值,必须重新同步。在代码里,我设置了一个“时间已同步”的标志位,NTP同步成功之前,定时任务调度器暂停运行,避免在错误时间触发任务。这个标志在Web界面上也有提示“时间同步中”,用户一看就知道为什么任务没触发。

7.3 任务冲突处理与防抖策略

定时任务最怕什么?怕两条任务同一时刻触发相反的动作——比如任务A说7:30开启,任务B说7:30关闭,那继电器到底执行哪个?我的策略是定义一个优先级规则:先处理关闭动作,再处理开启动作;如果同一时刻有多条任务触发,按任务ID从小到大依次执行。这样做的实际效果是“关闭优先”,因为从安全角度讲,不确定要不要开的时候,保持关闭总是更安全的选择。

还有一个坑是触发去重。调度器每10秒扫描一次,如果某一条任务在7:30:00被触发,7:30:10的下一次扫描又检查到当前时间还是7:30且星期匹配,就会再次触发,导致继电器在短时间内反复切换。我的解决方法是每条任务记录一个last_trigger_date,同一自然日内只允许触发一次,不管中途是否出现时钟回拨或扫描重入。这个bug我当初排查了很久,现象就是“开着的灯莫名其妙自己闪了两下”,后来打日志才发现是重复触发。

8. 常见问题与排查技巧实录

8.1 WebSocket连接不稳定、频繁掉线

这是整个项目里我遇到最多的问题,也是社区里问得最多的问题。现象是浏览器打开控制页面后,功率曲线更新几秒钟就停住,刷新页面又恢复,过一会又卡住。

排查过程分三步走。第一步,我先在ESP32日志里打点,确认是服务器主动关闭了连接还是客户端断开。日志显示服务器并没有调用关闭函数,但客户端那边已经触发了onclose。第二步,我用Wireshark抓包看了看,发现客户端在长时间没有收到服务器帧时,根据TCP Keep-Alive机制会主动断开。第三步,定位到根因:ESP32的WebSocket服务端没有发送心跳帧,而浏览器的WebSocket协议规范要求是服务端应该周期性地发Ping或数据帧来维持连接活性。默认情况下,浏览器超过一分钟没收到任何帧,就会判定连接超时。

解决办法是在ESP32的WebSocket推送逻辑里加上心跳机制:如果没有什么数据要发,每隔30秒主动发送一个{"type":"ping"}消息。前端收到ping后回复一个pong。实测下来这个改动让连接稳定性从几分钟提升到几天不掉线,效果立竿见影。

8.2 功率读数偏差大或者乱跳

功率读数一直跳,不一定就是代码问题。我自己就犯过好几个低级错误,这里列出来给大家做个速查。

首先是电源质量问题。ACS712模块用5V供电时,如果5V电源来自HLK-PM01且负载电流波动本身比较大,模块的输出偏置电压会跟着电源一起波动,导致0A时读数不是稳定的2.5V而是2.4~2.6V之间乱窜。解决办法是给模块加独立的LDO稳压,比如AMS1117-3.3或者HT7333,把供电做干净。

其次是ADC参考电压误差。ESP32的内部ADC参考电压精度大概在±6%左右,不同芯片之间差异明显,而且还受温度影响。如果你读到的电流值整体偏大或偏小一个固定比例,大概率就是这个问题。解决办法是用一个已知精度的万用表测出实际偏置电压,写入校准参数里,在代码中做线性修正。

第三是地线干扰。ACS712模块和ESP32的共地必须可靠,地线要粗且短,最好使用同一个电源系统地。如果ACS712的地和ESP32的地之间压差超过0.1V,ADC读数的抖动肉眼可见。

最后一个坑就是交流电中的高频噪声。ACS712的带宽有80kHz,会把开关电源、调光器等设备产生的高频噪声一并采进来,反映在ADC读数上就是毛刺。我的处理办法是在模块输出端加一个RC低通滤波器,R取1K、C取100nF,截止频率约1.6kHz,对50Hz工频信号没影响,但能削掉大部分高频毛刺。

8.3 配网页面打不开或者提交后连不上路由器

配网页面打不开的典型原因是手机连接ESP32热点时自动跳到了系统自带的门户页面,而不是打开浏览器访问192.168.4.1。我的建议是:配网页面在HTML里直接加一个JavaScript脚本,页面加载后立即尝试fetch一个本地探针地址,如果失败说明浏览器的请求没有到达ESP32,就弹一个醒目的提示“如果页面长时间无响应,请手动关闭Wi-Fi的网络自动登录开关”。这个提示对于不熟悉智能手机操作的用户来说很有价值。

提交配网信息后连不上路由器的原因则复杂些。最常见的是Wi-Fi密码包含特殊字符,前端提交时没有做URL编码,导致ESP32收到的密码被截断。另一个常见原因是ESP32对5G频段的兼容性问题,早期ESP32模组不支持5G频段,只支持2.4G;而现代路由器默认开启双频合一,手机连着5G,ESP32在2.4G频段扫描不到。解决办法是配网页面里提示用户确保路由器开启2.4G频段,或者暂时关闭双频合一功能。

8.4 常见问题速查表

故障现象可能原因排查命令 / 操作
Web界面打不开手机和ESP32不在同一网段arp -a或路由器管理页查看ESP32的IP
连上热点但页面不显示DNS劫持到门户登录页手动在浏览器输入192.168.4.1
继电器不动作GPIO接线松脱用万用表测GPIO电平是否有3.3V
功率显示为0ACS712供电异常测模块输出端对地电压是否为2.5V左右
开机后继电器自动开NVS保存状态逻辑检查relay_status默认值与恢复逻辑
定时任务不触发系统时间未同步查看Web界面“时间同步”是否完成
WebSocket频繁掉线没有心跳机制添加Ping/Pong逻辑
上电后自动进入配网模式NVS未存储密码确认代码中写入NVS后是否调用nvs_commit

9. 项目测试与稳定性验证

9.1 测试环境与负载场景

整个系统做完之后,我连续跑了近两个月的老化测试,测试条件包括:一个500W的电暖器作为阻性负载、一个180W的冰箱作为感性负载、以及一个空载状态下的功耗监测。测试期间故意模拟了三次停电来电(手动拉闸再合闸),观察系统是否能自动恢复。

测试过程中还做了一件事:把ESP32的Web服务器暴露在局域网中,每天有各种设备自动扫描端口。日志显示有不间断的HTTP扫描请求,但没有发现设备崩溃或异常重启的现象。这说明硬件设计里的防浪涌、防静电保护以及软件上的异常处理基本到位。

需要说明的是,我的测试没有覆盖“外网控制”这个场景。如果你需要在外网访问控制页面,建议通过路由器端口映射把ESP32的80端口映射出去,但一定要使用强密码的Wi-Fi网络,并且不要在公网上暴露无鉴权的控制接口,否则你的插座很容易被别人远程“接管”。严谨的产品级方案应该是通过内网穿透工具或者自建云服务器中转,并加入账户认证体系,但这就超出一个试验项目的范畴了。

9.2 WebSocket并发连接的压测结果

为了确认系统在多个客户端同时在线时还能正常工作,我用Python脚本模拟了6个WebSocket客户端同时连接并持续接收数据。最终测得的稳定状态是:6个连接在线时,服务器CPU占用率约42%,ESP32空闲内存剩余约98KB,所有连接都能正常收到每秒5次的推送数据,消息延迟平均不到20ms。继续加到10个连接时,延迟开始明显增大,但系统没有崩溃。这个结果对家庭使用场景来说是够用的,毕竟很少会同时开10个设备来看插座状态。

压测还暴露了一个问题:WebSocket服务端在客户端异常断开(比如手机突然锁屏、App被强杀)后,TCP连接不会立刻感知到断开,导致部分内存被幽灵连接占用。长时间运行后,连接占用的累积效应可能导致内存耗尽。我的对策是为每个连接设置一个超时定时器,如果超过90秒没有收到客户端的任何帧,就主动断开这条连接并释放资源。实际效果非常明显,连续运行三周后内存占用保持稳定。

9.3 功耗与发热实测

ESP32正常工作时的Wi-Fi发射功耗差不多是160mA@3.3V,加上继电器吸合时的线圈电流约70mA@5V,整个系统功耗在1W以内。这个发热量对封闭的86底盒来说有一点点大,但实测连续满载运行24小时后,外壳温度稳定在42度左右,没有烫手的感觉。如果你的外壳散热条件不好,建议主动降频或者减少Wi-Fi发射功率,代价是响应速度稍微慢一点,但外壳温度能降下来好几度。

这里要特别提醒:继电器长期吸合时线圈本身也会发热。SRD-05VDC-SL-C线圈电阻约70欧姆,5V供电时的线圈损耗约0.36W,这个热量在密闭空间里会累积。如果有条件,继电器可以选用带磁保持功能的型号,比如松乐的磁保持继电器,吸合后不需要持续供电维持,只有切换瞬间才需要脉冲电流,整体发热会小很多,但驱动电路相对复杂一些,要用双路脉冲控制。

10. 项目扩展方向与经验总结

10.1 可以继续做的几个方向

这个项目做完之后,完全可以当成一个基础平台继续扩展。我自己梳理了几个方向,优先级从高到低排:

第一是接入HomeAssistant。ESP32原生支持MQTT,把插座接入HA后,就能通过HA的自动化规则实现更复杂的联动,比如“室内温度低于18度且时间在晚上8点之后,打开电暖器”。HA对WebSocket推送的需求不强,主要是MQTT广播状态,代码改动量不大。

第二是电量统计与报表。如果在硬件上换成HLW8032计量芯片,就能精确采集有功功率、电量累计数据,Web前端可以增加日报、月报的柱状图,用于分析家庭用电分布。这个功能有实用价值,而且HLW8032本身很便宜,一颗大概几块钱。

第三是多设备管理。现在Web上位机只管理一个插座,如果家里有五六个插座,每个都有自己的IP地址,体验就比较割裂。下一步可以做一个小型管理端,把多个设备的状态汇聚到一个页面上集中管理。但这需要一台常开的服务器(比如NAS、树莓派)来跑汇聚服务,不是纯单片机方案了。

第四是OTA固件升级。ESP-IDF原生支持OTA升级,可以做一个Web端的固件上传页面,浏览器里选择编译好的bin文件直接刷写。这个功能我建议做实操时优先加,因为后续每次改代码不用再拆壳用串口线刷了。

10.2 个人实操中的几点体会

做完这个项目,我最大的体会是:智能硬件的价值不在于把某个操作从物理按键变成手机点按,而在于自动化、定时化、远程化带来的效率提升。如果一个智能插座只是把开关从墙上搬到手机屏幕上,那意义真的有限。把继电器控制、功率监测、定时调度、状态恢复这套链路完整跑通之后,家里的热水器、电暖器、鱼缸灯都变得“有脑子”了,这种满足感是买现成产品体会不到的。

第二点体会是关于调试效率的。ESP32的开发调试最大的瓶颈是周期长:改一行代码,编译上传可能要一两分钟,跑起来还要观察几十秒才能确认效果。所以我后来尽量在架构设计阶段就把问题想清楚,用状态机、任务队列这些成熟的软件工程方法,而不是每次靠线上打日志来试错。项目里的状态恢复、心跳机制、任务去重这些逻辑,早期如果不做,后期补起来成本成倍增加。

第三点建议给所有做这类项目的朋友:强电部分务必足够谨慎。能用电工胶带包裹的地方就裹上,能用隔离电源就不用非隔离电源,外壳能加绝缘垫就加上。我自己调试时有过一次不小心碰到继电器触点,220V打了一个小火花,当时心都凉了半截。安全不是嘴上说说,设计时少偷一点懒,就能少一次担惊受怕。

10.3 最后分享一个实用小技巧

最后分享一个小技巧:在Web前端加一个“设备诊断”按钮,点击后ESP32会把当前所有状态打包成JSON返回,包括Wi-Fi信号强度(RSSI)、内存剩余量、运行时间、上次重启原因、NVS读取次数等。这个功能平时用不上,但在远程排查问题时价值极大。我遇到过一次现场人员说“设备好像崩溃了”,远程一查发现其实是路由器的DHCP分配了新的IP地址,设备本身没崩,是用户还在用旧的IP地址访问,自然连不上。有了诊断页面,这类模糊问题几分钟就能定位。

这个项目从规划到完成大概花了两周业余时间,核心代码才一千多行,不算多,但每一行都是“花时间换稳定”的结果。如果看到这篇文章的朋友准备做类似的ESP32智能硬件,我真心建议直接把Web上位机方案作为默认选择——它不是最炫酷的,但一定是最实用、最容易调试、最难被废弃的方案。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询