arduino-esp32 实战用 OpenThread SRP 服务发布 UDP 打造 Thread 智能灯ThreadDNSSD UDP Light 的 light 端【免费下载链接】arduino-esp32Arduino core for the ESP32 family of SoCs项目地址: https://gitcode.com/GitHub_Trending/ar/arduino-esp32本文以 OpenThread 示例中的 light 草图为主体讲解如何在 ESP32 Thread SoCH2 / C6 / C5上加入一个已有的 OTBR Thread 网络通过 SRP 客户端把_otlight._udp服务发布到边界路由器并用轻量 UDP 服务响应ON/OFF/TOGGLE/STATUS命令驱动板载 RGB LED。读完本文你能完整理解该示例的入网流程、DNSSD 服务发布的事件驱动机制、UDP 命令协议以及SRP 发布失败/断网后从loop()自愈重发布的工程化状态机设计并能按文档完成烧录、联调与故障定位。示例定位ThreadDNSSD UDP Light 实验中的服务方该示例属于ThreadDNSSD_UDP_Light端到端实验见 组级 README由三个草图协作完成草图角色light本文主角Thread 节点SRP 发布_otlight._udp UDP 灯服务端端口5051switchThread 节点queryService发现灯BOOT 按键发送TOGGLE/STATUSweb仅 Wi-Fi通过http://otlight-ui.local经 OTBRAdvertising Proxy OMR 路由访问同一盏灯[Thread switch] --OThreadDNSSDUDP-- [Thread light] --SRP-- [OTBR] | Adv Proxy mDNS [Browser] --HTTP-- [Wi-Fi web] --ESPmDNSUDP-------------与 UDP Light Switch 实验不同这一组的板子以 Network Key 加入一个已存在的 OTBR 网络草图内不启动 Commissioner / Joiner。light 是唯一的服务方它加入网络后向 OTBR 上的 SRP 服务器注册主机名与 UDP 服务供网内的 switch 浏览发现和 Wi-Fi 侧经 OMR 路由访问。端口 5051 是有意为之应避开 5683 / 5684CoAP和 61631Thread TMF CoAP防止与栈内协议端口冲突。支持平台与编译前提支持目标SoCThreadRGB LED状态ESP32-H2yes必需SupportedESP32-C6yes必需SupportedESP32-C5yes必需SupportedRGB LED 不是可选项——草图通过rgbLedWrite(RGB_BUILTIN, ...)直接点亮板载 RGB 灯珠作为灯的物理执行器源码见 light.ino 的fadeTo/applyLamp没有 RGB LED 的板子无法按预期运行。必需 IDF 特性sdkconfig特性作用CONFIG_OPENTHREAD_ENABLEDyOpenThread 协议栈CONFIG_SOC_IEEE802154_SUPPORTEDy802.15.4 射频仅 H2/C6/C5 等带 15.4 的 SoC 满足CONFIG_OPENTHREAD_SRP_CLIENTyOThreadDNSSD服务发布所依赖的 SRP 客户端这组配置与 ci.yml 中声明的requires完全一致意味着 CI 在缺少这些开关时会跳过编译该示例。从 OThreadDNSSD.h 头部的条件编译宏也能确认整个 DNSSD API 只有在SOC_IEEE802154_SUPPORTED且CONFIG_OPENTHREAD_ENABLED且CONFIG_OPENTHREAD_SRP_CLIENT同时开启时才可见。前提条件OTBR 与擦除策略运行前需要一个已开启 SRP 的 OTBROpenThread Border Router挂在同一 Thread 网络上。SRP 服务器通常由 Border Router 承载light 的OThreadDNSSD发布请求最终要送达该 SRP 服务器才算成功。把草图中的OT_NETKEY改成 OTBR 的 Network Key。示例默认值为00112233445566778899aabbccddeefflight.ino L31若不匹配waitAttached(60000)超时会打印FAIL: not attached并永久停住见下文故障排查。烧录时优先选择 Tools → Erase Flash: Sketch Only。这样 NVS 中保存的 SRP ECDSA 密钥会被保留避免重刷后 SRP 服务器认为同一密钥重新注册而报OT_ERROR_DUPLICATED。入网流程只用 Network Key 加入 OTBRsetup()的入网序列light.ino L167-L197OThread.begin(false); DataSet ds; ds.clear(); ds.setNetworkKey(OT_NETKEY); OThread.commitDataSet(ds); OThread.networkInterfaceUp(); OThread.start(); // waitAttached(): 轮询 otGetDeviceRole() // 直到角色变为 Child / Router / Leader 之一60 s 超时要点OThread.begin(false)初始化 OpenThread 实例commitDataSet把含 Network Key 的 Dataset 写入后networkInterfaceUp()start()开始组网状态机。waitAttached()L82-L91每 200 ms 轮询一次OThread.otGetDeviceRole()只要角色属于Child / Router / Leader三种已附着角色即返回60 秒超时则调用halt()死循环停止串口输出FAIL: not attached (check Network Key vs OTBR)。这是示例唯一在入网阶段停住的失败点。附着成功后立即OtUdp.begin(LIGHT_PORT)绑定 UDP。若绑定失败同样haltFAIL: UDP begin。注意示例的失败分级策略L93-L94 注释只有入网和 UDP 绑定两类致命错误才停住而 SRP 发布的begin()/addService()失败不 halt而是置s_needReadvertise标志交由loop()在冷却期后重试。这样即使 SRP 服务器还没就绪UDP 服务也始终在线。SRP 服务发布OThreadDNSSD 事件驱动服务发布的核心在startAdvertise()light.ino L114-L139static bool startAdvertise(const char *reason) { s_ignoreLocalRemoved true; OThreadDNSSD.end(); // 先注销旧主机/服务 s_ignoreLocalRemoved false; s_announced false; if (!OThreadDNSSD.begin(kHostName)) return false; // ot-light if (!OThreadDNSSD.addService(otlight, udp, LIGHT_PORT)) return false; // _otlight._udp:5051 (void)OThreadDNSSD.addServiceTxt(otlight, udp, cmds, on,off,toggle,status); Serial.println(Waiting for OT_DNSSD_EVENT_ANNOUNCED...); return true; }逐行对应到 OThreadDNSSD.h 的 API 语义begin(kHostName)配置本地 SRP 主机名并启用 SRP 自动启动。头文件明确false 是本地/配置层面的失败不代表 SRP 服务器未就绪且要求OThread.begin()已调用、最好在已附着角色下使用。addService(otlight, udp, 5051)以 ESPmDNS 风格发布服务。服务类型前后下划线可选ot/_ot均可库会剥除前导下划线最终编码为_otlight._udp。同一 serviceproto 重复调用是幂等更新。addServiceTxt(otlight, udp, cmds, on,off,toggle,status)附加 TXT 记录把支持命令集暴露在 DNS-SD 元数据里便于发现方switch / web自省协议。TXT 槽位默认每服务最多 4 条OT_DNSSD_MAX_TXT_ENTRIES键长 16、值长 32 字节超限会在 include 头文件前以#define覆盖。事件回调只置标志不做事setup()中注册OThreadDNSSD.onServiceEvent(onDnsEvent)。回调实现L102-L111static void onDnsEvent(ot_dnssd_event_t event, otError error, void *context) { (void)context; // OpenThread task (or caller task for end()-generated REMOVED): flags only. if (event OT_DNSSD_EVENT_REMOVED s_ignoreLocalRemoved) { return; } s_event event; s_err error; s_gotEvent true; }这里有两条值得注意的实现细节回调运行在 OpenThread 任务上end()触发的REMOVED除外它在调用者任务即loop()/setup()上触发——头文件 L181-L193 明确要求不要在回调内调用其他 OThreadDNSSD 方法只置标志/拷贝状态。因此示例只在回调里更新三个volatile变量真正的处理延迟到loop()。s_ignoreLocalRemoved屏蔽自激 REMOVEDstartAdvertise()开头调用OThreadDNSSD.end()会立即在本任务上派发一个REMOVED事件。若不加屏蔽这个由自己主动触发的 REMOVED 会被loop()误判为服务被服务器移除从而多余地再排一次重发布。该窗口期标志正是为区分被动失效与主动重建。三类事件在loop()中的处理逻辑L212-L234事件处理OT_DNSSD_EVENT_ANNOUNCED置s_announced true、清除s_needReadvertise打印PASS: ANNOUNCED as ot-light _otlight._udp:5051OT_DNSSD_EVENT_ERROR若错误是OT_ERROR_DUPLICATED或OT_ERROR_SECURITY置s_nameConflict true且不自动重试同名冲突必须人工换主机名/清状态其他错误s_announced false等待冷却后重发布OT_DNSSD_EVENT_REMOVED服务消失s_announced false等待冷却后重发布[OThreadDNSSD.h](https://link.gitcode.com/i/0fbb4a0f537540b0a6d483b95581d8fb#L59-L61)头部注释也印证了库的策略边界Name conflicts (OT_ERROR_DUPLICATED) are reported to the sketch; the library does not rename. Prefer unique hostnames and keep NVS across reflash.库报告冲突但绝不自动改名策略留给草图。UDP 服务端绕开 lwIP 的最轻 Thread UDP灯控通道使用OThreadUDPOThreadUDP.h它是直接构建在otUdpSocketAPI 上的 ArduinoUDP实现不经过 lwIP是 Thread 侧最轻量的 UDP 通路。两个可构建期覆盖的容量参数值得了解OT_UDP_MAX_PACKET_SIZE默认 512 字节单个入站数据报入队上限超限截断OT_UDP_RX_QUEUE_DEPTH默认 4parsePacket()之间可排队的入站数据报数量队列满时丢旧包。OtUdp.begin(LIGHT_PORT)会把套接字绑定到 IPv6 any 地址OT_IN6ADDR_ANY即::的 5051 端口覆盖所有接口。Wire 协议命令在serviceUdp()L141-L165中按整串文本精确匹配strcmp缓冲区 32 字节收到的数据报动作回复ON灯开ACK ONOFF灯关ACK OFFTOGGLE反转灯状态ACK ON/ACK OFFSTATUS不改变状态STATE ON/STATE OFF其他忽略串口打印Ignoring unknown command无回复通过reply()以单播发往OtUdp.remoteIP()/OtUdp.remotePort()即来源地址/端口不做广播。物理执行器侧applyLamp(true)会把亮度从 0 以 2 ms 步长渐变到 248关闭则渐隐到 0fadeToL54-L65用rgbLedWrite(RGB_BUILTIN, level, level, level)写白光。UDP 与 SRP 的生命周期解耦文档强调 UDP starts before SRP completes源码上也确实如此setup()的顺序是先附着 → 先OtUdp.begin()→ 后startAdvertise(initial)整个loop()的重发布流程只操作OThreadDNSSD从不stop()/重新begin()UDP 套接字。因此 OTBR 重启、SRPend()/begin()重建期间UDP 服务始终在线——只要发现方手里有灯的 OMR 地址控制通道不因 SRP 故障而中断。这也是该示例相对阻塞式waitForAnnounce写法的工程价值所在。自愈状态机loop() 里的重发布逻辑loop()L199-L259每 20 ms 一轮除了处理 UDP 和 DNSSD 事件外还维护两个独立的健康信号附着丢失检测比较上一轮与本轮的角色若从已附着跌落OTBR 重启/信号丢失打印Lost attach (role...) — will re-advertise when attached again清除s_announced并置s_needReadvertiseAnnounce 完整性巡检即使曾收到过ANNOUNCED若OThreadDNSSD.isAnnounceComplete()变 false该函数每次调用实时读取otSrpClientGetHostInfo/otSrpClientGetServices见 OThreadDNSSD.h L366-L373打印Announce incomplete — scheduling re-advertise并重新排队。重发布的触发门控L242-L248if (s_needReadvertise s_attached !s_nameConflict) { if (now - s_lastAdvertiseMs kReadvertiseCooldownMs) { // 默认 15 s s_needReadvertise false; (void)startAdvertise(recovery); } }即必须同时满足已附着 未处于命名冲突 距上次发布尝试 ≥ 15 skReadvertiseCooldownMs才执行startAdvertise(recovery)。这与 ThreadDNSSD_Advertise_Callback 示例是同一套模式库负责报告草图负责决策重试、放弃、改名。唯一例外是s_nameConflictOT_ERROR_DUPLICATED/OT_ERROR_SECURITY它会永久抑制自动重试直到人工干预——因为用同一主机名无限重试只会反复撞车。loop()末尾每 10 秒打印一次状态心跳roleChild announce1 needReadvertise0 lampON是运行期最直观的体检表。预期串口输出正常启动路径与 README 给出的样例一致ThreadDNSSD_UDP_Light / light Waiting to attach... Attached as Child UDP listening on port 5051 (MLEID fd..) Advertise (initial) as ot-light... Waiting for OT_DNSSD_EVENT_ANNOUNCED... PASS: ANNOUNCED as ot-light _otlight._udp:5051 RX [fd..]:xxxxx - TOGGLE roleChild announce1 needReadvertise0 lampONOTBR 重启后的典型恢复序列是先看到EVENT: ERROR或Announce incomplete随后Advertise (recovery)...再出现一个PASS。注意UDP listening ... (MLEID fd..)里打印的是OThread.getMeshLocalEid()即 mesh-local EIDswitch 经 SRP 发现解析到的实际是 OMR 地址fd..前缀的 OMR 前缀可把解析出的 OMR 粘贴给 web 端做LIGHT_IPV6_FALLBACK见 switch README。自定义参数常量用途OT_NETKEY必须与 OTBR 的 Network Key 一致kHostNameSRP 主机名默认ot-lightLIGHT_PORTUDP 监听端口默认 5051kReadvertiseCooldownMs恢复发布尝试之间的等待默认 15 s如果修改了kHostName或LIGHT_PORT必须在 switch 以及任何 web / PC 客户端镜像同样的服务类型与端口否则发现端会查不到实例。另外按 OThreadDNSSD.h L252-L253 的建议多台 light 共享同一 OTBR 时应为每台设备使用唯一主机名从根上规避OT_ERROR_DUPLICATED。故障排查整合 README 的排障表与源码行为现象可能原因 / 行为FAIL: not attachedNetwork Key 与 OTBR 不匹配草图halt停住FAIL: UDP begin本地 UDP 绑定错误草图halt停住FAIL: OThreadDNSSD.begin/addService非致命由loop()在 15 s 冷却后自动重试不 halt始终没有PASS: ANNOUNCED网络上还没有 SRP 服务器——UDP 仍在监听等待或检查srp server serviceOT_ERROR_DUPLICATED命名冲突——换唯一主机名、Sketch Only 擦除、或清理 OTBR SRP 软状态不会自动重试OTBR 重启后 switch 发现 0 个实例等 light 打印Advertise (recovery)与新的PASS后再试启动顺序建议来自组级 README先起 OTBR → 再烧 light等 SRP announce→ 最后 switch / web。延伸阅读ThreadDNSSD UDP Light — 组级总览light switch web 的完整实验拓扑、端口选择理由与启动顺序switch客户端侧如何用OThreadDNSSD.queryService(otlight, udp)浏览并单播控制灯ThreadDNSSD_Advertise_Callback本示例重发布模式的原型事件回调式 advertiseThreadDNSSD 示例集advertise / query / remove 各 API demo以及 Flash 擦除与 SRP 命名冲突的处理说明OThreadDNSSD.h / OThreadUDP.h发布与查询 API 的完整头文件文档池容量宏、事件语义、结果获取器稳定性约定。本示例代码采用 Apache License 2.0 授权。【免费下载链接】arduino-esp32Arduino core for the ESP32 family of SoCs项目地址: https://gitcode.com/GitHub_Trending/ar/arduino-esp32创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
