ESP32智能插座Web上位机:DIY免App控制与功率监测
发布时间:2026/9/5 8:04:12来源:尧图网络
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-324MB 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.3V12位分辨率对应量化精度理论值是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。具体分压计算取R110K串联、R220K接地输出电压Vout Vin * R2/(R1R2) Vin * 2/3。也就是说2.5V变1.67V3.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和外壳制作时严格执行强电和弱电区域的走线间距不小于6mm高压铜箔和低压铜箔分层布置继电器触点出线端用硅胶线引出加2A保险丝做过流保护外壳用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库一个接口调用嵌套多了堆碎片化会比较严重。所以我的建议是能用字符拼接解决的简单情况就别全走JSONWebSocket服务端定时推送状态可以做成固定格式的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.txt4.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状态存储设计NVSNon-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 断电重启后的快速恢复流程上电恢复的完整逻辑分成五个步骤读取NVS中保存的Wi-Fi凭据尝试连接路由器连接超时时间为10秒连接成功后通过SNTP同步标准时间等待时间校准完成读取relay_status如果上次是开启状态且定时任务逻辑决定现在应该开启则恢复继电器为开启否则保持关闭加载定时任务列表到内存重置下一次任务触发时间启动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.1VADC读数的抖动肉眼可见。最后一个坑就是交流电中的高频噪声。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而现代路由器默认开启双频合一手机连着5GESP32在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_commit9. 项目测试与稳定性验证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发射功耗差不多是160mA3.3V加上继电器吸合时的线圈电流约70mA5V整个系统功耗在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上位机方案作为默认选择——它不是最炫酷的但一定是最实用、最容易调试、最难被废弃的方案。
网站建设高端定制企业官网