
简介SMLReader是一套基于ESP8266/ESP32的智能电表数据接入方案面向物联网开发者与智能家居爱好者解决D0 SML接口电力数据难以融入现有MQTT体系的问题。资源包包含完整Arduino/C工程源码、头文件、配置文件与说明文档共29个文件、大小839KB其中png/jpg演示硬件接线与运行效果h/cpp实现SML解析、OBIS识别与MQTT发布readme/md提供编译与配置指南还附带平台IO配置文件与持续集成脚本。已有267人学习。通过该项目可掌握ESP8266读取D0接口、解析SML信息结构、映射OBIS码并发布MQTT主题的完整链路适合作为能源监控、家庭自动化DIY的参考模板也能加深对智能电网通信协议的理解。 家里智能电表装好之后我一开始只是当个“数字版本的电表”用后来越看越心痒想把这些数据实时接进 Home Assistant做用电趋势和峰谷统计。市面上现成的方案不是贵就是封闭直到我按 SMLReader 这个项目的思路折腾了一版一块 ESP8266 开发板配合电表 D0 数据口的光电探头把智能电表通过 D0 SML 协议周期性推送的计量数据读出来、解析成可读数值再以 MQTT 消息的形式发布到局域网里的 broker这就是一个非常典型的“D0 SML 到 MQTT 网关”。这个方案硬件成本低、逻辑清楚适合想搞家庭能源监控、又不想被厂商云平台绑死的人。1. 为什么是“D0 SML ESP8266 MQTT”这个组合1.1 先搞清电表那头输出的是什么不看协议直接买模块十有八九会踩坑。家里的智能电表通常不开放上网口走 Modbus但基本都会留一个专门用于本地读数的数据口。欧洲和国内不少电表走的是 IEC 62056-21 标准里的 D0 口数据格式则用 SMLSmart Message Language来传输。SML 是一种基于字节的二进制消息语言电表会定时把当前电压、电流、功率、累计电量、分时电量等信息打包成一帧从 D0 口发出来。D0 口在硬件上是 20mA 电流环常见工作模式是电表主动推数据也就是“只发不收”所以我们的网关只需要单向读取就行。对比其他读表方式脉冲输出只能得到相对变化要自己换算成电量断电重启后还会丢基准Modbus 接口往往不开放、需要账户权限而 D0 SML 基本是“插上就能读”数据完整且自带 OIOBIS对象标识非常适合作家庭网关的第一手数据源。1.2 ESP8266 为什么够用有人上来就问“要不要上 ESP32、树莓派更稳”说实话单纯完成“SML 读取 MQTT 发布”这个工作ESP8266 绰绰有余。D0 SML 的数据速率通常只有 9600 波特折算下来每秒不到 1KBESP8266 的 UART 完全跑得动。需要的算力也只是扫描帧头、提取字节、拼一下 JSON这类任务用 ESP32 甚至有点浪费。更重要的一点是 ESP8266 的生态太成熟了Arduino 框架下直接操作 HardwareSerial 和 PubSubClient十几行代码就能把 MQTT 跑起来板子便宜丢了不心疼如果哪天固件写崩了重新烧录也就几十秒的事。选择 MQTT 而非 HTTP 的原因也很实际电表是周期性或者变化时推送数据MQTT 天然适合这种高频次、小体量的消息在局域网里搭一个 Mosquitto 或者 EMQXHome Assistant、Node-RED、Grafana 都可以通过订阅同一个 topic 拿到数据不用给每个平台单独写一遍 HTTP 接口。可以说这个网关本质上是把“电表的二进制口”翻译成“家居平台能听懂的 MQTT 普通话”。2. D0 硬件接入最容易翻车的环节2.1 光电探头模式还是串口直连模式接入方式主要看电表提供的物理接口。D0 口常见两种形态一种是表盘上有红外/光电窗口需要用光电探头贴在表盘上读取另一种是直接引出了串口或者接线端子可以用杜邦线接到微控制器。光电探头方案的安全性和便利性最好。现在网上有成品光电探头用电工胶布贴在电表光电口上、对准窗口就行不需要断电接线。这类探头内部已经把电流环信号转换成了 3.3V 或 5V 电平的 UART 信号我实测接 Wemos D1 mini 的 RX 引脚就能直接读到数据。接线端子方案则需要确认电平。D0 电流环输出的常见配置是 20mA 回路有的厂家模块内部会做电平转换直接输出 3.3V TTL也有的是 5V TTL这种情况下直接接 ESP8266 会存在过压风险稳妥做法是串一个 1kΩ 电阻再分压或者用光耦做一次隔离。我自己第一次踩坑就是拿 5V 输出直接怼到 GPIO3板子倒是没烧但偶尔会收到一堆乱码后来加了一级电阻分压才彻底稳定。注意如果是外接端子接线前务必确认电表 D0 接口的公共参考电平不要想当然认为“GND 就是电池负极”。工业电流环经常有“高低端灌电流”的区别接错了轻则读不到数据重则烧板子。2.2 供电和布线的细节ESP8266 虽然功耗不高但 WiFi 发射瞬间电流能到 300mA 以上。如果用一个质量一般的手机充电头或者电脑 USB 口供电很容易出现“平时正常、一联网就重启”的诡异现象。我的建议是至少用 1A 以上输出、纹波小的 5V 适配器并尽量缩短供电线长度。如果你打算长期挂在配电箱里还可以考虑用 HLK-PM01 这类 AC-DC 模块从市电取电但这里涉及强电操作建议没有充分把握的朋友不要自己搞安全第一。布线方面D0 光电探头到 ESP8266 的距离越短越好。虽然波特率不高但是工业环境里配电箱附近可能有变频器、电磁阀这类干扰源长线缆会引入噪声。实测中我把探头线控制在 50cm 以内配合绞线几乎没有误码。如果确实要拉长线可以考虑用屏蔽双绞线并把屏蔽层单端接地。3. SML 数据帧到底长什么样3.1 帧头帧尾、CRC 与消息类型SML 帧不是直接用文本发出来的而是一串二进制字节流。常见电表输出的帧结构是帧头固定为1B 1B 1B 1B 01 01 01 01帧尾固定为1B 1B 1B 1B 1A 03 05帧体里可以有多条消息最常见的两条是GetList.Request和GetList.Response部分厂商会在帧尾前附带 CRC16 校验字节所以解析思路就清晰了持续从串口读字节寻找帧头序列进入“缓存模式”直到遇到帧尾序列取中间数据按位解析即可。这就是 SML 解析器的核心状态机逻辑并不需要真的把整棵 SML 树都实现出来。每帧里真正有价值的数据是GetList.Response消息中一串ListOf.ValueList条目。每个条目里包含一个 6 字节的 OBIS 编号比如01 00 01 08 00 FF展开后就是1-0:1.8.0*255也就是“累计正向有功电能”。接下来跟着的是数值长度、数值类型和数值本身按 big-endian 读取即可。3.2 OBIS 对象与单位换算不同电表推送的 OBIS 对象数量不一样少则十几个多则几十个。列几个最常用到的OBIS 代码含义常见单位1-0:1.8.0*255累计正向有功电能kWh1-0:2.8.0*255累计反向有功电能光伏等kWh1-0:1.8.1*255T1 峰段电能kWh1-0:1.8.2*255T2 谷段电能kWh1-0:16.7.0*255当前总有功功率W1-0:31.7.0*255L1 相电流A1-0:51.7.0*255L2 相电流A1-0:71.7.0*255L3 相电流A1-0:32.7.0*255L1 相电压V1-0:52.7.0*255L2 相电压V1-0:72.7.0*255L3 相电压V需要特别提醒OBIS 编号基本是行业标准但具体每个值的缩放系数受电表厂商影响。比如同样是累计电能有的表直接发 Wh有的表发 1/1000 Wh还有的发带符号的 int64。偷懒做法是直接把 8 字节转成 int64 后除以 1000000但严谨做法是解析 SML 条目里的单位字段和 scaler 字段用它们决定小数位的偏移。我自己调试时是先对拍电表屏幕把功率、电压的原始字节和显示值做对比确认缩放系数后再硬编码进配置。毕竟家庭场景里电表型号固定不需要做通用抽象。3.3 一段极简的解析思路在 ESP8266 上跑完整 SML 树解析库不是不行但资源开销偏高。我的做法是在扫描到帧头后将帧体按字节缓存然后开始“找 OBIS”// 伪代码示意解析思路 if (findPattern(buf, {0x01, 0x00, 0x01, 0x08, 0x00, 0xFF})) { // 向后读 1 字节 length判断类型后读取 8 字节 big-endian int64_t energy readBigEndianInt64(buf, pos); energyWh energy / 1000.0; // 按实际表计缩放 }实际工程里要注意处理“一帧里同一个 OBIS 出现多次”的情况比如分时电能会同时存在 1.8.0、1.8.1、1.8.2。我的做法是按 OBIS 代码作为 map 的 key后出现的值覆盖先出现的值最后统一发布。4. 固件实现与 MQTT 发布4.1 串口读取与帧扫描状态机我用的开发板是 WeMos D1 MiniUART0 RX 在 GPIO3。接好光电探头后直接初始化串口Serial.begin(9600, SERIAL_8N1); // 如果读不到数据可以尝试 SERIAL_8E1 或 SERIAL_7E1SML 默认帧速是 9600 8N1但个别表会先以 300 波特率发送协商指令再切到 9600这种就需要额外做波特率切换逻辑。我在实际调试中遇到的情况是电表直接以 9600 持续推送因此没有做 300 波特率协商如果你遇到“完全没有数据”可以拿一个 USB-TTL 先接电脑用串口工具抓一下原始字节流确认实际波特率再下手。帧扫描状态机用四个状态即可空闲、准备帧头、接收中、帧结束。帧头检测用滑动窗口比对不需要等到完整帧头才记录因为电表可能从帧中间开始发送前几个字节是残缺的。uint8_t minorState 0; const uint8_t SM_HEADER[] {0x1B,0x1B,0x1B,0x1B,0x01,0x01,0x01,0x01}; const uint8_t SM_END[] {0x1B,0x1B,0x1B,0x1B,0x1A,0x03,0x05}; bool checkPattern(uint8_t *buf, int len, const uint8_t *pattern, int plen) { if (len plen) return false; for (int i 0; i plen; i) if (buf[len - plen i] ! pattern[i]) return false; return true; }用类似checkPattern的方法在每次收到新字节时把数据追加到 ring buffer 并检查后缀就能在内存占用极小的情况下完成帧采集。帧结束后再交给解析函数解析完清空缓冲区等待下一帧。4.2 Topic 设计与 JSON 负载MQTT 网关不只是“把数据发出去”就完了topic 和负载设计直接影响后面 Home Assistant 或者 Node-RED 用起来顺不顺手。我的建议是采用层级结构把站点和表计分开tele/electricity/SMLReader/raw完整原始 JSON便于调试tele/electricity/SMLReader/energy只包含电能相关值tele/electricity/SMLReader/power只包含功率、电压、电流瞬时值负载用 JSON 字符串示例{ timestamp: 1742188800, power_w: 286.5, energy_total_kwh: 12345.678, energy_t1_kwh: 9876.5, energy_t2_kwh: 2469.178, voltage_l1_v: 231.2, current_l1_a: 1.24 }发布时保留retain标志这样 Home Assistant 重启订阅后能立刻拿到最新值不用等下一帧推送。如果担心刷屏可以在 ESP8266 端做一个去重缓存只有数值变化超过阈值时才发送不过我在实际中直接保底“每 10 秒发布一次总数据”简单直接也足够实时。4.3 对接 Home Assistant如果不想暴露一堆原始 JSON topic推荐直接使用 MQTT Discovery。ESP8266 连接成功后向homeassistant/sensor/electricity/config发布一段 JSONHome Assistant 会自动创建一个叫“electricity”的传感器{ name: Electricity Meter, state_topic: tele/electricity/SMLReader/energy, value_template: {{ value_json.energy_total_kwh }}, unit_of_measurement: kWh, device_class: energy, unique_id: smlreader_energy_total }这样在 Lovelace 界面里就能直接看到一个实时变化的电量卡片。我另外建了功率、电压的 discovery展示效果更丰富还能配合 InfluxDB Grafana 做长期历史趋势。4.4 重连和看门狗家庭网络环境里 ESP8266 最容易出的问题就是 WiFi 掉线后不自动重连或者连着连着 MQTT 不发了。我的处理是三层保活WiFi 层启用wifi_station_set_hostname()并设置静态 IP减少 DHCP 交互风险MQTT 层setKeepAlive(15)回调里判断连接断开后每 5 秒尝试重连且设置client.disconnect()清理旧连接系统层在loop()中实时检测网络断开时长超过 60 秒没有恢复就主动ESP.restart()另外ESP8266 默认的 WiFi 省电模式会经常进入 sleep导致延迟高甚至丢包。实测在 SMLReader 这种需要持续实时性的场景里我把WiFi.setSleep(false)关掉省电模式后丢包率明显下降。5. 常见问题与排查实录5.1 完全读不到数据先别怀疑代码先怀疑物理链路。用电脑 USB-TTL 接 D0 探头打开串口监视器波特率 9600看能不能看到类似1B 1B 1B 1B 01 01 01 01的原始字节。如果电脑上都看不到说明接线方向、电平、探头位置有问题。常见的几个坑是光电探头没有对准电表光学窗口、胶带没贴严实漏光、电表 D0 端子需要短接某个使能引脚才输出、波特率是 2400 或 300。排查顺序我建议是“电脑串口工具先确认物理链路再轮到 ESP8266 固件”。5.2 能读到帧头但解析出的数值是乱码多半是电平或串口参数不匹配。有一种很隐蔽的情况是 D0 输出的逻辑电平反相也就是空闲时为低、有数据时为高普通 UART 解析会得到明显乱码。这种可以加一个逻辑非门或者用光耦反向电路解决有的模块也会提供“开漏输出”模式配合上位机串口的外部上拉就能纠正。如果只是偶发的乱码则可能是供给 ESP8266 的电源纹波太大或者光电探头引线太长收到干扰。我给配电箱内网关加了一颗 100μF 电解电容和 0.1μF 瓷片电容并联干扰问题就消失了。5.3 数值对不上、电能总是差距一个数量级这类问题的核心是缩放系数没对上。SML 里的数值虽然是 int64但单位可能不一样。我遇到过功率直接以 W 为单位输出也遇到过以 10^-6 kW 为单位输出的表如果代码里写死除以 1000就会差 1000 倍。排查方法是手动抓一帧对比屏幕上同时刻显示的值和原始字节转出的十进制数确定比例关系。如果出现负的累计电能注意符号位扩展int64_t转uint64_t时高位符号会带来负数结果必要时做绝对值处理或者预留符号判断。5.4 WiFi 丢包和 MQTT 掉线ESP8266 的 WiFi 丢包原因很多但家庭网关场景下最常见是供电不足和省电模式。我建议先用固定电源代替劣质 USB 口供电再关掉 WiFi 休眠最后检查路由器是否开启了“AP 隔离”这类阻止局域网互访的功能。如果 MQTT 掉线频繁可以观察 broker 的日志。常见原因是客户端没有在 keepalive 周期内发 PINGREQ因为主循环被delay()长时间阻塞。解决办法是把发布逻辑改成分时调度避免一个耗时的 MQTT publish 阻塞串口读取我甚至把解析和发布拆成了两个阶段串口中断收帧、主循环发布。6. 最后说点实际体会E家搞能源监控以来这个 D0 SML 网关是我用过的几个方案里最省心的。虽然前后折腾了三个晚上主要时间花在电平适配和缩放系数标定上但一旦跑稳后续基本不用管。整个过程最大的心得是遇到协议类项目不要一开始就抱着“完整实现标准”的心态先抓关键字节、跑通最小闭环再慢慢补充功能和容错这样推进速度会快很多。另外如果你不想维护自写固件ESPHome 里也有现成的 sml 组件可以直接把光电探头接到支持 UART 的 ESP32/ESP8266 上配置一段 YAML 就能接入 Home Assistant。自写固件的好处是可控性强、代码量少、方便按自己需求二次开发看你的目标了。总之D0 SML 到 MQTT 这条路我已经帮你蹚过一遍想复刻的朋友直接从硬件接线和串口抓包开始很快就能见到第一帧电表数据在 MQTT topic 里出现。本文还有配套的精品资源点击获取