arduino-esp32 OpenThread sensor_collector:构建 Thread Leader + Commissioner 多传感器 UDP 汇聚服务
2026/9/14 2:33:58 网站建设 项目流程

arduino-esp32 OpenThread sensor_collector:构建 Thread Leader + Commissioner 多传感器 UDP 汇聚服务

【免费下载链接】arduino-esp32Arduino core for the ESP32 family of SoCs项目地址: https://gitcode.com/GitHub_Trending/ar/arduino-esp32

本篇基于 arduino-esp32 仓库中 OpenThread 库的UDP_SensorNetwork示例,深入讲解服务端 sketchsensor_collector的完整实现:它如何组建或恢复一张 Thread 网络、以 Commissioner 角色为 Joiner 传感器开放入网窗口、在 UDP 5050 端口接收多节点遥测帧,并用"应用层序列号 + 权威 ACK + 节点表生命周期管理"构建一个可对大量传感节点提供服务的高节点 UDP 汇聚服务。读完本文,你可以直接复刻该 demo 的部署顺序与调参方法,并从源码层面理解 Commissioner 提权、Joiner 窗口、序列号去重与掉线驱逐等关键机制在sensor_collector.ino中的落地方式。

sensor_collector 在 UDP Sensor Network 中的定位

UDP_SensorNetwork是一个"多对一"(many-to-one)遥测示例,完全构建在 Arduino OpenThread Native API(OThread+OThreadUDP)之上。示例由两个 sketch 组成,架构如下:

sensor_node 1 ----\ sensor_node 2 -----+--> sensor_collector (Leader + Commissioner + UDP sink) sensor_node N ----/
Sketch角色
sensor_collector (Leader + UDP sink)服务端:ThreadLeader + Commissioner,并在端口5050上提供高节点 UDP 汇聚。
sensor_node (Joiner sensor client)客户端:ThreadJoiner(可选Sleepy End Device)+ 周期性 UDP 传感器上报。

sensor_collector是 sensor_node 的对应端(counterpart),一台 collector 可以在同一张网络上服务任意多个 sensor 节点。它做五件事:

  1. 组建或恢复名为ESP_OT_SENSOR_NET的 Thread 网络(重启后从 NVS 恢复 DataSet);
  2. 申请Commissioner角色,为 PSKdJ01NME打开 Joiner 入网窗口;
  3. 在 UDP 端口5050上监听传感器遥测帧;
  4. 跟踪最多MAX_SENSORS个节点 ID,把静默节点标记为离线、驱逐过期记录,并发送应用层序列号 ACK;
  5. 一旦 collector 自身从网络掉线,自动重启 Thread 并重开 UDP 套接字。

完整的组网流程、线缆协议(wire protocol)与运行步骤见 UDP Sensor Network 总览 README。

支持的目标平台与 IDF 依赖

支持的 SoC

SoCThread状态
ESP32-H2支持Supported
ESP32-C6支持Supported
ESP32-C5支持Supported

必需的 sdkconfig 特性

sensor_collector的 ci.yml 中声明了该示例的编译前提,与下表一一对应:

特性作用
CONFIG_OPENTHREAD_ENABLED=y构建 OpenThread 协议栈。
CONFIG_SOC_IEEE802154_SUPPORTED=y确认 SoC 具备 802.15.4 射频。
CONFIG_OPENTHREAD_COMMISSIONER=y启用 Commissioner API 用于 Joiner 准入。

其中CONFIG_OPENTHREAD_COMMISSIONER是 collector 独有依赖——在 OThread.cpp 中,startCommissioner()addJoiner()stopCommissioner()getCommissionerState()等 Commissioner 相关接口全部被#if CONFIG_OPENTHREAD_COMMISSIONER包裹,未开启该开关时这些 API 根本不会被编译出来。而 sensor_node 侧对应的开关是CONFIG_OPENTHREAD_JOINER=y,两个开关不要混淆。

部署前提与启动顺序

  • 必须先烧录本 collector,再烧录任何 sensor_node 开发板。
  • 在烧录或复位传感器之前,先等待串口出现Commissioner ACTIVECollector listening on UDP port 5050两条日志。
  • 首次启动时,collector 会 provision 一个全新的 Active Dataset;此后每次启动都从 NVS 中恢复已存储的 dataset,因此复位 collector 后恢复的是同一张 Thread 网络。已被 commission 过的传感器同样会恢复 NVS 中的 dataset,正常掉电复位后不需要重新打开 joiner 窗口。

结合 UDP Sensor Network 总览 的 How to Run,完整操作是:

  1. 在一块开发板上烧录sensor_collector,串口监视器波特率设为115200
  2. 等待其打印Collector listening on UDP port 5050
  3. 在其余一块或多块开发板上烧录sensor_node
  4. 确认每个传感器打印status=ACKED、collector 打印到接收到的采样。

核心流程源码走读

以下以 sensor_collector.ino 为准展开。

1) Thread 网络:恢复 NVS dataset 或 provision 新网络

startThreadNetwork()的核心逻辑(sensor_collector.ino):

OThread.begin(false); if (OThread.hasActiveDataset()) { // 从 NVS 恢复已持久化的 Active Dataset } else { // 首次上电:用固定参数构建 DataSet 并提交 DataSet ds; ds.initNew(); ds.setNetworkName(OT_NETWORK_NAME); // "ESP_OT_SENSOR_NET" ds.setChannel(OT_CHANNEL); // 15 ds.setPanId(OT_PAN_ID); // 0xABCE ds.setExtendedPanId(OT_EXTPANID); ds.setNetworkKey(OT_NETKEY); // 128-bit OThread.commitDataSet(ds); } OThread.networkInterfaceUp(); OThread.start(); // 轮询等待设备角色达到 CHILD 以上(最多 24 * 2.5s = 60s) while (tries-- && OThread.otGetDeviceRole() < OT_ROLE_CHILD) { delay(2500); }

要点:

  • 网络参数是固定 DataSet——网络名ESP_OT_SENSOR_NET、信道 15、PAN ID0xABCE、扩展 PAN ID 与 128 位网络密钥均写死在文件头部(常量定义)。collector 与传感器必须使用同一套 dataset 常量,PSKD也要一致(sensor_node 侧使用同样的"J01NME"CHANNEL_HINT = 15)。
  • 如果修改了OT_*常量却不生效,是因为持久化在 NVS 中的 Active Dataset 优先级更高——需要擦除 NVS 或执行 OpenThreadfactoryreset后新常量才会生效。
  • 等待 attach 的循环以角色判断为终止条件:OT_ROLE_CHILD及更高级别(Router、Leader)都算成功。collector 因为没有父节点,最终会以 Leader 身份上线。

2) Commissioner 提权与 Joiner 窗口

// 2) Petition Commissioner and open the joiner window. OThread.startCommissioner(); OThread.addJoiner("J01NME", JOINER_WINDOW_SEC); // PSKd "J01NME",默认开 3600 s

startCommissioner()在 setup() 中执行;若首次失败,loop() 会在设备角色达到 CHILD 后每 5 秒重试一次,直到成功。

从源码实现看(OThread.cpp),这一过程比表面上复杂:

  • OpenThread::startCommissioner(uint32_t timeoutMs = 30000)(声明见 OThread.h)调用otCommissionerStart()发起petition,然后通过一个二进制信号量阻塞等待状态回调,直到状态变为OT_COMMISSIONER_STATE_ACTIVE(petition 被 Leader 接受)或OT_COMMISSIONER_STATE_DISABLED(被拒绝),超时默认 30 s;
  • 若返回OT_ERROR_ALREADY(已经是 ACTIVE)会直接按成功处理;petition 被拒绝则返回OT_ERROR_REJECTED,示例中会打印startCommissioner failed: %d
  • addJoiner(pskd, timeoutSec = 120, eui64 = nullptr)(OThread.cpp)调用otCommissionerAddJoiner(),以 ASCII PSKd"J01NME"打开入网窗口,JOINER_WINDOW_SEC默认 3600 s;本例不传eui64,即窗口对任意 EUI-64的 Joiner 开放,任何持有正确 PSKd 的 sensor_node 都可以加入。

Commissioner 是 Thread 中唯一有权打开 Joiner 窗口的角色,因此这个"先当 Leader、再当 Commissioner"的组合恰好让 collector 成为入网准入的权威节点。同一组合但没有 UDP 流量的精简版本可参考 CommissionerNode 示例。

3) UDP 监听与序列号 ACK 协议

// 3) Bind UDP on port 5050 and process incoming frames in loop(). OtUdp.begin(COLLECTOR_PORT); // 5050

OtUdp是 OThreadUDP(class OThreadUDP : public UDP),即绑定在 Thread 网络接口(MLD,Mesh-Local Address 域)上的 UDP 套接字,而非 WiFi/Ethernet 上的普通 UDP。

入站帧为 ASCII 格式(wire protocol):

id=<nodeId>,seq=<u32>,temp_centi=<i32>,batt_mv=<u16>

示例帧:

id=3CAAB123,seq=42,temp_centi=2387,batt_mv=3810

collector 的 ACK 格式:

OK,<nodeId>,<seq>

processPacket()(sensor_collector.ino)是整个示例的协议核心,它用sscanf解析四个字段后执行一套完整的序列号状态机:

帧类型判定条件collector 行为
首次帧packetCount == 0接受读数
新帧seq > lastSeq接受读数,更新lastSeq
重复帧seq == lastSeq记为(dup),不覆盖读数
节点重启seq == 1 && lastSeq > 1视为新流重新开始,接受并清零重复计数
过期/乱序帧seq < lastSeq(且不满足重启条件)不覆盖最新读数,计为 stale

关键设计是collector 是序列号的权威方:无论收到什么帧,ACK 里回的都是 collector自己存储的lastSeq,而不是收到的seq(sendAck() 与注释)。这样 sensor_node 端(见 sensor_node.ino 的 parseAckSeq 与 RESYNC 逻辑)可以用 collector 的存储值向前滚动(RESYNC)或保持原序列重试(NO_ACK),实现应用层重同步——传感器侧持有"已被确认的最后序列号"s_seq,每次发送s_seq + 1,只有收到 ACK 才前进。

端口注意事项:应用流量务必使用 UDP 端口5050,避开61631(Thread TMF CoAP)与5683/5684(CoAP)。若把应用套接字绑定到这些保留端口,可能收到二进制的 Thread 管理流量,在串口上表现为DROP malformed

4) 节点表:离线判定、记录驱逐与统计

节点表是纯 RAM 结构:static SensorRecord s_records[MAX_SENSORS],每个记录包含used / online / nodeId / lastSeq / lastTempCenti / lastBatteryMv / packetCount / duplicateCount / lastSeenMs / lastIp(结构定义)。新节点到来时按nodeId线性查找,找不到则分配空槽;表满时直接丢弃并打印DROP table full (MAX_SENSORS=%u)

auditNodes() 在每个报告周期(默认 30 s)执行两级老化:

  1. age > NODE_OFFLINE_MS(默认 95 s,约 3 个丢失的 30 s 采样):节点online置 false,打印node %s -> OFFLINE (silent %lus)
  2. age > NODE_EVICT_MS(默认 30 min):整个记录被释放,槽位归还给新节点。

配套的 printCompactReport() 打印角色、已知/在线节点数与累计收包、丢包计数。注意节点表不持久化:collector 复位后表会清空,这是预期行为——传感器会在 collector 从 NVS 恢复网络后通过重传把表重建起来。

5) 角色看门狗:掉线自愈

loop() 中每 5 秒检查一次设备角色,若掉到OT_ROLE_CHILD以下(Detached/Disabled),触发 restartThreadNetwork():

停止 UDP 套接字 -> OThread.stop() -> OThread.start()(复用 NVS 中持久化的 dataset) -> 最多 20s 轮询等待重新 attach -> 重新 begin(5050)

即"collector 掉线时自动重启 Thread 并重建 UDP socket",无需人工干预。对应地,sensor_node 侧也有自己的forceReattach()与 3 次连续NO_ACK后强制重挂逻辑(sensor_node.ino),两端各自自愈。

期望的串口输出

按 README 描述,正常运行时的典型输出为:

=== sensor_collector: Thread Leader + UDP sink === Attached as Leader. Commissioner ACTIVE. PSKd "J01NME" open for 3600 s. Collector listening on UDP port 5050 RX [fdde:ad00:beef:0:....]:5050 <- 'id=3CAAB123,seq=1,temp_centi=2387,batt_mv=3810' TX ACK -> 'OK,3CAAB123,1' Fleet: known=1 online=1 total_rx=1 dropped=0

实际 sketch 打印的格式略有不同(如RX node=... seq=... temp=...C batt=...mV from [...][collector] role=... nodes=... online=... packets=... dropped=...),但判断部署成功的标志是稳定的:出现Commissioner ACTIVECollector listening on UDP port 5050后,传感器每 30 s 一帧、每帧都有OK,<nodeId>,<seq>应答。

可调参数一览

所有可调常量集中在 sensor_collector.ino 文件头部:

常量作用
PSKDCommissioner 接受的 Joiner 密钥,sensor_node 必须一致("J01NME")。
JOINER_WINDOW_SECaddJoiner()窗口有效期(默认 3600 s)。
OT_CHANNEL802.15.4 信道(默认 15)。
OT_PAN_ID16 位 PAN ID(默认0xABCE)。
OT_EXTPANID扩展 PAN ID 字节序列(默认CE 01 00 00 DE AD BE EF)。
OT_NETKEY128 位网络密钥。
OT_NETWORK_NAMEThread 网络名("ESP_OT_SENSOR_NET")。
COLLECTOR_PORTUDP 监听端口(默认 5050)。
MAX_SENSORS内存节点表容量(默认 256)。
NODE_OFFLINE_MS节点被标记离线前的静默时长(默认 95 s)。
NODE_EVICT_MS节点记录被驱逐前的静默时长(默认 30 min)。
REPORT_PERIOD_MS舰队统计打印周期(默认 30 s)。

修改 dataset 相关常量(OT_*PSKD)时记住:若 NVS 中已有旧 dataset,旧数据优先,需要擦除 NVS 或factoryreset才能让新常量生效;两侧(collector 与 node)的PSKD、信道等必须同步修改。

故障排查

启动顺序:先烧录本 collector,等到Collector listening on UDP port 5050再烧录 sensor_node 开发板;对任何在 collector 就绪前启动的传感器执行复位。

现象可能原因
没有任何传感器能 attach(首次 commission)Commissioner join 窗口未开——先等Commissioner ACTIVE再启动传感器。
Collector 复位后节点表丢失预期行为——表在 RAM 中;collector 从 NVS 重连后传感器会重建表。
来自 RLOC 地址的DROP malformed应用绑定到了 Thread 保留端口——用5050,不要用 61631/5683/5684。
修改OT_*常量不生效Dataset 已持久化在 NVS——先擦 flash 或factoryreset再期望新常量生效。
Collector 掉线角色看门狗会自动重启 Thread 并重开 UDP socket——检查射频环境与构建开关(CONFIG_OPENTHREAD_COMMISSIONER=y等)。

此外,sensor_node 侧 README/总览文档 还补充了几条客户端视角的现象:传感器NO_ACK(collector 未绑定、dataset/PSKd 不匹配或端口错误);collector 复位后传感器卡住(传感器在丢失 ACK 后会强制重挂,可等待或复位);传感器掉电重启后能工作但首次入网失败(预期——已 commission 的传感器走 NVS 恢复,joiner 窗口只用于首次 commission 或擦除 NVS 之后)。

相关资源

  • UDP Sensor Network 总览——线缆协议与多节点架构;
  • sensor_node(Joiner + 可选 SED 传感器客户端);
  • CommissionerNode——同样的 Leader + Commissioner 组合,但不带 UDP 流量;
  • Native UDP 示例总览——所有 Native UDP demo 与端口约定;
  • API 实现:OThread.cpp(Commissioner 部分)、OThread.h、OThreadUDP.h。

本示例基于 Apache License 2.0 许可。

【免费下载链接】arduino-esp32Arduino core for the ESP32 family of SoCs项目地址: https://gitcode.com/GitHub_Trending/ar/arduino-esp32

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

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

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

立即咨询