arduino-esp32 OpenThread CommissionerNode 实战:Leader + Commissioner 双角色安全组建 Thread 网络
【免费下载链接】arduino-esp32Arduino core for the ESP32 family of SoCs项目地址: https://gitcode.com/GitHub_Trending/ar/arduino-esp32
本文基于 arduino-esp32 仓库中 CommissionerNode 示例文档 及其源码 CommissionerNode.ino 展开,讲解如何用 OpenThread Native API 在一块 ESP32-H2 / C6 / C5 开发板上组建一个全新的 Thread 网络:本地构建 Operational Dataset 并使其成为 Leader,申请 Commissioner 角色,再通过物理按钮触发addJoiner()打开加入窗口(Joiner Window),让第二块设备仅凭共享密钥 PSKd 就能安全入网。读完本文,你将完整掌握 Commissioner 侧的调用顺序、关键宏配置、sdkconfig 要求、串口输出判读与常见故障排查方法。
1. 定位:CommissionerNode 是"服务端"
CommissionerNode 与 JoinerNode 示例 是一对双板演示,属于 Thread Commissioning 分组总览 中的"Leader + Commissioner"角色。传统做法是把 network key 直接烧录进新设备的 sketch,而本演示的关键点是:新设备(Joiner)事先不知道网络密钥、网络名、PAN ID 中的任何一项,它在 MeshCoP/DTLS 认证握手中只出示 PSKd,网络信息通过空中下发。网络密钥在链路上从不以明文出现——它是在握手过程中由 PSKd 派生的。
CommissionerNode 具体做了五件事(与文档一致):
- 用硬编码的
DataSet(网络名、信道THREAD_CHANNEL、PAN ID、扩展 PAN ID、网络密钥)组建一个全新 Thread 网络; - 通过
networkInterfaceUp()+start()成为新分区的Leader; - 一旦 attached,自动申请Commissioner角色;
- 仅在按下按钮(
JOIN_BUTTON_PIN,默认 BOOT)时调用addJoiner(PSKD, 120)打开加入窗口,接受任何出示 PSKdJ01NME的 Joiner; - 在
loop()中每 5 秒打印角色、RLOC、地址与 Commissioner 状态。
重要提示(原文档强调):Commissioner 角色在 attach 后会自动开始,但在你按下按钮之前不会接受任何 Joiner。必须等串口出现
Press the button on GPIO ... to open the joiner window之后再按JOIN_BUTTON_PIN。这一"申请角色"与"打开窗口"的分离是 Thread 入网安全的核心:角色是持续状态,窗口是限时、按需开启的授权入口。
2. 支持的芯片与 sdkconfig 要求
2.1 支持的 Target
| SoC | Thread 支持 | 按钮引脚 | 状态 |
|---|---|---|---|
| ESP32-H2 | 支持 | BOOT_PIN | Supported |
| ESP32-C6 | 支持 | BOOT_PIN | Supported |
| ESP32-C5 | 支持 | BOOT_PIN | Supported |
如果开发板的按键不在 BOOT 引脚上,可以在 sketch 顶部覆盖JOIN_BUTTON_PIN。从 CommissionerNode.ino 的注释可以看到BOOT_PIN是 Arduino 核心按板型提供的宏:C6/H2 上为 GPIO9,C5 上为 GPIO28,BOOT 键为低电平有效,因此 sketch 中用INPUT_PULLUP上拉后检测下降沿。
2.2 必需的 IDF 特性(sdkconfig)
| 特性 | 作用 |
|---|---|
CONFIG_OPENTHREAD_ENABLED=y | 构建 OpenThread 协议栈 |
CONFIG_SOC_IEEE802154_SUPPORTED=y | 确保 SoC 具备 802.15.4 射频 |
CONFIG_OPENTHREAD_COMMISSIONER=y | 使能 Commissioner API |
这三项与示例目录下的 ci.yml 完全一致——验证测试正是按这些 requires 配置编译运行的。
从 OThread.cpp 的源码结构看,整段 Commissioner 实现(startCommissioner、addJoiner、stopCommissioner、getCommissionerState)都包裹在#if CONFIG_OPENTHREAD_COMMISSIONER条件编译中,即不开启该 Kconfig 时这些 API 根本不存在,这是"必须显式使能"的源码级依据。
3. 前置条件与操作顺序
- 先烧录本 CommissionerNode sketch,再烧录 JoinerNode(客户端)。
- 等待串口显示 attached 且
Commissioner ACTIVE后,按下 BOOT 键打开加入窗口,然后才启动(或复位)Joiner。 - 窗口保持打开 120 秒(
JOINER_WINDOW_SEC);窗口过期后再按一次按钮即可重新打开。 - 固定
THREAD_CHANNEL(默认 15),使其与 Joiner 侧的信道提示一致,避免 Joiner 全信道扫描。
4. 工作流程解析(对照源码)
文档给出的最小工作流与 CommissionerNode.ino 的实际实现一一对应:
// 1) 组建 Thread 网络并成为 Leader。 threadCommissionerNode.begin(false); dataset.initNew(); dataset.setNetworkName("ESP_OT_Joiner"); dataset.setChannel(THREAD_CHANNEL); dataset.setPanId(0x1234); // ... ext PAN ID, network key ... threadCommissionerNode.commitDataSet(dataset); threadCommissionerNode.networkInterfaceUp(); threadCommissionerNode.start(); // 2) attached 之后申请 Commissioner(在 loop() 中)。 threadCommissionerNode.startCommissioner(); // 3) 按钮按下时:打开加入窗口。 if (joinButtonPressed()) { threadCommissionerNode.addJoiner("J01NME", 120); }4.1 setup():构建并下发 Operational Dataset
setup()(第 73–103 行)的关键细节:
threadCommissionerNode.begin(false):参数false表示不自动从 NVS 加载数据集——因为本演示要的是"从零组建",不是恢复旧网络。dataset.initNew()生成一个合法的新数据集骨架,随后逐项覆盖:- 网络名
"ESP_OT_Joiner"; - 扩展 PAN ID 8 字节
{0xDE, 0xAD, 0x00, 0xBE, 0xEF, 0x00, 0xCA, 0xFE}(即串口输出中的dead00beef00cafe); - 网络密钥 16 字节
00 11 22 ... ff(演示用,生产环境必须更换); dataset.setChannel(THREAD_CHANNEL)与setPanId(0x1234)固定物理层参数。
- 网络名
commitDataSet(dataset)→networkInterfaceUp()→start()三步把栈拉起来,设备随即进入"等待成为 Leader"状态。
关于固定信道:initNew()本身会选择一个有效(近随机)信道,网络没有setChannel也能组建;固定它只是为了与 Joiner 侧的THREAD_CHANNEL提示对齐,让 Joiner 免扫描直接入网。
4.2 loop():attached 后申请 Commissioner,按键开窗口
loop()(第 105–188 行)按三段逻辑运转:
(a)条件申请角色。每轮先读otGetDeviceRole(),只要不是OT_ROLE_DETACHED/OT_ROLE_DISABLED就视为 attached。首次 attached 且尚未申请时调用:
otError err = threadCommissionerNode.startCommissioner(/*timeoutMs=*/30000); if (err != OT_ERROR_NONE) { Serial.printf("Commissioner petition failed (err=%d). Retrying in 5 s...\r\n", err); delay(5000); return; }失败时 5 秒后重试,成功后置commissionerStarted = true并打印提示===>>>Press the button on GPIO %u to open the joiner window.。
(b)按键触发addJoiner。按键检测函数joinButtonPressed()(第 62–71 行)用静态变量记录上一轮电平,检测高→低的下降沿并delay(50)去抖,保证一次物理按压只返回一次true。按下后执行:
otError err = threadCommissionerNode.addJoiner(PSKD, JOINER_WINDOW_SEC); if (err == OT_ERROR_NONE) { Serial.printf("Joiner window OPEN: PSKd \"%s\" accepted for %lu s.\r\n", PSKD, (unsigned long)JOINER_WINDOW_SEC); }(c)每 5 秒打印状态而不阻塞按键。loop()尾部用millis()节流:未满 5 秒时delay(20); return;,把 CPU 让给按键检测;满 5 秒则打印角色、RLOC16、网络名、信道、PAN ID、扩展 PAN、Mesh Local EID、Leader RLOC、Node RLOC,以及由getCommissionerState()映射出的DISABLED / PETITION / ACTIVE状态。若角色发生变化(例如从 Detached 变为 Leader),还会调用clearAllAddressCache()清除地址缓存。
4.3 底层实现:startCommissioner 与 addJoiner 做了什么
OThread.cpp 中这两个同步封装的源码揭示了它们的行为边界:
startCommissioner(timeoutMs)是同步阻塞调用:- 内部通过
otCommissionerStart(mInstance, commissionerStateCallback, ...)向 Leader 发起 petition; - 状态回调
commissionerStateCallback在 OpenThread 任务中运行,当状态变为ACTIVE(成功)或回落到DISABLED(被拒绝)时释放二进制信号量; - 调用方在
timeoutMs(默认 30 s,本示例显式传 30000 ms)内等待该信号量; - 超时则自动调用
otCommissionerStop()并返回OT_ERROR_RESPONSE_TIMEOUT;已处于 ACTIVE 时返回OT_ERROR_ALREADY,封装层将其折算为OT_ERROR_NONE。
- 内部通过
addJoiner(pskd, timeoutSec, eui64)是非阻塞转发:持OtLock后直接调用otCommissionerAddJoiner(mInstance, eui64, pskd, timeoutSec)。eui64传nullptr(默认值)即"接受任何出示该 PSKd 的设备";传入具体 EUI-64 则可白名单化到单一设备。PSKd 为空指针时返回OT_ERROR_INVALID_ARGS。
对照 OpenThread 库 README 的 Commissioner 章节,API 签名为:
otError startCommissioner(uint32_t timeoutMs = 30000); otError addJoiner(const char *pskd, uint32_t timeoutSec = 120, const otExtAddress *eui64 = nullptr); void stopCommissioner(); otCommissionerState getCommissionerState() const;其中getCommissionerState()在实例未初始化或锁获取失败时一律返回OT_COMMISSIONER_STATE_DISABLED,这也是示例串口打印中"未申请成功就显示 DISABLED"的原因。
5. 预期串口输出
按 README 给出的完整预期输出(115200 波特率):
=== Joiner Demo - Commissioner Node === Thread network started, waiting to become Leader... Attached as Leader. Petitioning Commissioner... Commissioner ACTIVE. ===>>>Press the button on GPIO 9 to open the joiner window. ============================================== Role: Leader RLOC16: 0x0000 Network Name: ESP_OT_Joiner Channel: 15 PAN ID: 0x1234 Extended PAN: dead00beef00cafe Mesh Local EID: fdde:ad00:beef:0:.... Leader RLOC: fdde:ad00:beef:0:.... Node RLOC: fdde:ad00:beef:0:.... Commissioner: ACTIVE <-- press the BOOT button here --> Joiner window OPEN: PSKd "J01NME" accepted for 120 s. Bring up the JoinerNode sketch now.其中RLOC16: 0x0000与Leader RLOC指向fdde:ad00:...前缀(由扩展 PAN IDdead00beef00cafe派生的 mesh-local 前缀fdde:ad00:beef)是自举分区的典型特征——Leader 的 RLOC 恒为 0。出现Joiner window OPEN之后,才是烧录/复位 JoinerNode 的时机。
6. 构建期宏定制
sketch 顶部的四个宏均可用-D编译标志覆盖(见 CommissionerNode.ino 第 30–54 行):
| 宏 | 默认值 | 用途 |
|---|---|---|
THREAD_CHANNEL | 15 | 802.15.4 信道(11..26)。必须与 Joiner 侧THREAD_CHANNEL一致。 |
JOIN_BUTTON_PIN | BOOT_PIN | 打开加入窗口的低电平有效按键 GPIO。 |
PSKD | J01NME | addJoiner()接受的 Pre-Shared Key for Device。 |
JOINER_WINDOW_SEC | 120 | 每条addJoiner()记录的有效时长(秒)。 |
PSKd 的格式约束(来自 OpenThread README):ASCII 字符串,6–32 个字符,使用 base32-thread 字母表——数字与大写字母,排除I、O、Q和数字0;双端必须完全一致。源码注释还提醒:JOINER_WINDOW_SEC对演示绑定的单 Joiner 足够宽裕,生产环境应缩短窗口并配合eui64参数做设备级白名单。
7. 故障排查
启动顺序:先烧本 Commissioner sketch,等 Leader + CommissionerACTIVE,在烧 JoinerNode 之前按 BOOT 键;120 s 过期后按再次开窗,然后复位 Joiner。
| 现象 | 可能原因 |
|---|---|
Commissioner petition failed | 设备尚未 attached,或分区内已有其他 Commissioner 活动——重试或先停掉另一个 Commissioner。 |
| Joiner 一直不出现 | 加入窗口未打开——先按按钮确认串口出现Joiner window OPEN,再启动 JoinerNode。 |
| 加入窗口过期 | 窗口仅 120 s——再按一次按钮重新打开,然后复位 Joiner。 |
addJoiner failed | PSKd 非法——须为 6–32 字符 ASCII,base32-thread 字母表(不含0、I、O、Q);源码层还可能因 Commissioner 表满而失败(OT_ERROR_NO_BUFS)。 |
| Joiner 在错误信道上 | 两个 sketch 必须使用相同THREAD_CHANNEL。 |
从实现细节补充两条排查思路:startCommissioner()返回非OT_ERROR_NONE时常见为OT_ERROR_REJECTED(petition 被拒,即分区内已有 Commissioner 或状态回落DISABLED)与OT_ERROR_RESPONSE_TIMEOUT(30 s 内无响应,封装层已自动otCommissionerStop(),所以 5 秒后重试是安全的);串口若始终停在Status: Detached/Disabled - waiting for network start...,说明start()之后的角色尚未推进,先确认 sdkconfig 中CONFIG_SOC_IEEE802154_SUPPORTED=y且板型为 H2/C6/C5。
8. 相关示例与延伸阅读
- Thread Commissioning 分组总览——两块板的完整运行顺序与双端排查表(含
CONFIG_OPENTHREAD_JOINER=y的 Joiner 侧要求)。 - JoinerNode(客户端)——无本地 DataSet、仅凭 PSKd 走
startJoiner()入网的配套 sketch。 - UDP Light Switch — light(服务端)——Commissioner + UDP 灯服务端的组合演示。
- Native 示例总目录 与 SimpleThreadNetwork——后者是直接共享网络密钥的简单组网方式,可对照理解"为什么要做 Commissioning"。
- OpenThread 库 README——
startJoiner/startCommissioner/addJoiner的 API 参考、三个超时的配对建议(startJoiner(..., timeoutSec_for_commissioner * 1000 + 10000))与错误码清单。
本示例遵循 Apache License 2.0(见仓库 LICENSE.md)。
【免费下载链接】arduino-esp32Arduino core for the ESP32 family of SoCs项目地址: https://gitcode.com/GitHub_Trending/ar/arduino-esp32
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考