arduino-esp32 OpenThread 阻塞式 Thread 网络发现(ThreadScan_Discover)实战指南
【免费下载链接】arduino-esp32Arduino core for the ESP32 family of SoCs项目地址: https://gitcode.com/GitHub_Trending/ar/arduino-esp32
本文围绕 arduino-esp32 仓库中的ThreadScan_Discover示例(Native API 阻塞式 Thread 网络发现),完整讲解如何在 ESP32-H2 / C6 / C5 上仅拉起 IPv6 接口、周期性调用OThreadScan.discoverNetworks()发现周边 Thread 网络,并结合OThreadScan源码剖析结果去重、超时与锁机制,帮助读者掌握可直接复制运行的阻塞扫描方案及其全部可调参数。
1. 示例定位:阻塞式(blocking)Thread 发现
ThreadScan_Discover是 Thread Network Discovery 演示组 三个示例之一,采用与 Wi-FiWiFiScan相同的阻塞式交互模式:调用即等待,返回后即可直接读取结果。该组三个示例对照如下:
| 示例 | 模式 |
|---|---|
| ThreadScan_Discover(本文) | 阻塞式发现(discoverNetworks()同步等待) |
| ThreadScan_Async | 非阻塞,用scanComplete()轮询 |
| ThreadScan_Callback | 流式onResult()/onComplete()回调 |
从源码注释看(OThreadScan.h),OThreadScan封装的是 OpenThread 的otThreadDiscover()(对应 ESP-IDF OpenThread CLI 的discover命令),即 Thread MLE Discovery 原语——结果同时包含 Thread 身份字段(网络名、Extended PAN ID、可加入标志)和 IEEE 802.15.4 链路字段(扩展地址、PAN ID、信道、RSSI、LQI)。这也是 Matter 在配网(commissioning)阶段列举 Thread 网络所用的同一原语。
该示例的核心行为:
- 以
OThread.begin(false)启动 OpenThread(不从 NVS 自动加载数据集); - 通过
networkInterfaceUp()拉起 IPv6 接口(无需启动 Thread); - 在
loop()中每 10 秒调用一次阻塞式discoverNetworks(); - 打印每个
OThreadNetworkInfo(网络名、Extended PAN ID、PAN ID、扩展地址、信道、RSSI、LQI、joinable 标志); - 每次扫描结束后用
scanDelete()释放结果存储。
1.1 支持的芯片
| SoC | Thread | 状态 |
|---|---|---|
| ESP32-H2 | yes | Supported |
| ESP32-C6 | yes | Supported |
| ESP32-C5 | yes | Supported |
1.2 必需的 IDF 配置(sdkconfig)
| 配置项 | 作用 |
|---|---|
CONFIG_OPENTHREAD_ENABLED=y | 编译进 OpenThread 协议栈 |
CONFIG_SOC_IEEE802154_SUPPORTED=y | 确认 SoC 具备 802.15.4 射频 |
这两个配置与示例目录下的 ci.yml 中声明的requires完全一致。源码层面也能印证这一点:OThreadScan.h 的整个 API 都被#if SOC_IEEE802154_SUPPORTED && CONFIG_OPENTHREAD_ENABLED包裹,未启用时该类根本不存在。
2. 前置条件:先让 Leader 组网
阻塞式发现本身不需要本机加入任何网络,但要拿到有意义的结果,射频范围内必须存在活跃 Thread 网络。推荐做法:
- 在第一块 ESP32-H2 / C6 / C5 开发板上烧录 LeaderNode(组网节点) 示例,串口监视器波特率设为115200;
- 等待串口输出
Role: Leader(它构建完整运营数据集——网络名ESP_OpenThread、信道 15、PAN ID0x1234、扩展 PAN ID、网络密钥——并提交并成为新分区 Leader); - 将本示例烧录到第二块板卡。
启动顺序(Leader 先、扫描板后)在原文档和 SimpleThreadNetwork 组总览 中都被反复强调,是因为扫描结果(网络名、XPAN 等)全部来自 Leader 广播的 MLE Discovery Response。
3. 完整示例代码解读
完整源码见 ThreadScan_Discover.ino,可整体复制运行:
#include <Arduino.h> #include "OThread.h" // This sketch stores up to 32 unique networks (library default is 16). // Define OT_DISCOVER_MAX_RESULTS before including OThreadScan.h. #define OT_DISCOVER_MAX_RESULTS 32 #include "OThreadScan.h" static void printNetwork(const OThreadNetworkInfo &net, int index) { Serial.printf("%2d", index + 1); Serial.print(" | "); Serial.print(net.joinable ? "1" : "0"); Serial.print(" | "); Serial.printf("%-16.16s", net.networkName); Serial.print(" | "); Serial.print(net.extendedPanIdStr()); Serial.print(" | "); Serial.printf("%04x", net.panId); Serial.print(" | "); Serial.print(net.extAddressStr()); Serial.print(" | "); Serial.printf("%2u", net.channel); Serial.print(" | "); Serial.printf("%3d", net.rssi); Serial.print(" | "); Serial.printf("%3u", net.lqi); Serial.println(); } static void discoverBlocking() { Serial.println("Thread discovery start"); int n = OThreadScan.discoverNetworks(); // 阻塞式扫描 Serial.println("Thread discovery done"); if (n == OT_DISCOVER_FAILED) { Serial.println("discovery failed (interface down, lock failure, or timeout)"); } else if (n == OT_DISCOVER_RUNNING) { Serial.println("discovery already in progress (OT_DISCOVER_RUNNING)"); } else { if (n == 0) { Serial.println("no Thread networks found"); } else { Serial.print(n); Serial.println(" network(s) found"); Serial.println("Nr | J | Network Name | Extended PAN | PAN | MAC Address | CH | dBm | LQI"); for (int i = 0; i < n; ++i) { printNetwork(OThreadScan.getResult(i), i); delay(10); } } // Release reserved capacity after every completed scan (including 0 results). OThreadScan.scanDelete(); } Serial.println("-------------------------------------"); } void setup() { Serial.begin(115200); OThread.begin(false); // 不加载 NVS 数据集 OThread.networkInterfaceUp(); // 仅拉起 IPv6 接口,不启动 Thread Serial.println("Setup done — IPv6 interface up, Thread start not required"); } void loop() { discoverBlocking(); delay(10000); // 每 10 秒一轮 }关键要点逐条对应:
OThread.begin(false):参数OThreadAutoStart为false时不自动启动 Thread 协议、不从 NVS 恢复数据集(见 OThread.cpp 中begin()的栈初始化流程:配置原生 radio 模式、NVS 存储分区、任务队列后,通过握手信号量等待 OpenThread 工作线程完成初始化)。发现扫描只需要栈可用 + 接口 up,不需要 attach。networkInterfaceUp():拉起 IPv6 接口是discoverNetworks()的硬性前提(OThreadScan.h 的@note明确标注 "Requires the IPv6 interface to be up")。- 结果读取规则:
getResult(i)/getResultCount()只能在发现完成后调用(阻塞返回值 ≥ 0、scanComplete()≥ 0 或onComplete()之后);扫描进行中请使用onResult()流式获取。 scanDelete()必须每轮都调:示例中即使 0 结果也调用,注释写明 "Release reserved capacity after every completed scan (including 0 results)"。
4. 源码深入:OThreadScan的返回值约定与内部机制
4.1 返回值常量
OThreadScan.h 定义了两个状态码(与 Wi-Fi 扫描约定一致):
| 常量 | 值 | 含义 |
|---|---|---|
OT_DISCOVER_RUNNING | -1 | 发现仍在进行(等价WIFI_SCAN_RUNNING) |
OT_DISCOVER_FAILED | -2 | 发现失败或未触发(接口未 up、获取锁失败、超时;等价WIFI_SCAN_FAILED) |
≥ 0 | 0..N | 成功,返回去重后的网络数量 |
4.2 阻塞流程与锁/信号量
从 OThreadScan.cpp 的discoverNetworks()实现看,完整流程为:
- 锁外准备:惰性创建完成信号量
_doneSem,并prepareResultStorage()预分配容量为OT_DISCOVER_MAX_RESULTS的结果 vector——注释特意说明"堆分配不能在持有 OT API 锁时执行"; - 加锁启动:通过
esp_openthread_lock_acquire获取 API 锁后(OtLockRAII 包装),检查实例有效性;若_inProgress || otThreadIsDiscoverInProgress(inst)则立即返回OT_DISCOVER_RUNNING;将_channel==0视为全信道、合法信道(11..26)映射为位掩码,随后调用otThreadDiscover(inst, channelMask, panIdFilter, joinerOnly, eui64Filter, handleDiscoverResult, this); - 阻塞等待:
xSemaphoreTake(_doneSem, pdMS_TO_TICKS(_timeoutMs)),超时默认 30000 ms(OT_DISCOVER_DEFAULT_TIMEOUT_MS,见 OThreadScan.h),超时或_startError非零均返回OT_DISCOVER_FAILED; - 完成信号:OpenThread 对每条Discovery Response 回调
onDiscoverResult(result),最终回调result == nullptr时置_done = true并给信号量(OThreadScan.cpp)。scanComplete()的注释特别指出:不要用otThreadIsDiscoverInProgress()推断完成,因为 OpenThread 可能在最终回调投递前就已 idle。
4.3 结果去重与 RSSI 择优
同一网络可能从多个路由器/信标收到多份 Discovery Response。onDiscoverResult通过findResultByExtendedPanId()按Extended PAN ID匹配:命中且新 RSSI 更强时替换旧记录,即"同网多源只保留信号最强的一条"(OThreadScan.cpp)。存储达到OT_DISCOVER_MAX_RESULTS上限后,超出部分只流经onResult()回调、不入库。每条 Response 都会触发onResult回调——这正是 Matter/OpenThread 的流式模型。
4.4joinable标志的判定
值得注意的实现细节:MLE Discovery 的可加入性来自Steering Data 布隆过滤器,而非 beacon 的 Joining Permitted 位。由于公开的otSteeringData*辅助接口在 Arduino 构建中未启用,OThreadScan.cpp 的discoverResultIsJoinable()直接检查结构体:mSteeringData.mLength为 0 或超过OT_STEERING_DATA_MAX_LENGTH判为不可加入;任一字节非零(即过滤器非全零)判为可加入。fromActiveScanResult()中据此区分来源:out.joinable = in.mDiscover ? discoverResultIsJoinable(in) : in.mIsJoinable;(mIsJoinable仅对 802.15.4 beacon 主动扫描有效)。
4.5OThreadNetworkInfo字段一览
定义于 OThreadScan.h,示例串口表格逐列对应这些字段:
| 字段 | 类型 | 含义 |
|---|---|---|
networkName | char[OT_NETWORK_NAME_MAX_SIZE+1] | 以\0结尾的 Thread 网络名 |
extendedPanId | uint8_t[8] | 8 字节 Extended PAN ID(extendedPanIdStr()输出 16 位小写 hex) |
panId | uint16_t | IEEE 802.15.4 PAN ID(示例中以%04x打印) |
extAddress | uint8_t[8] | 响应方扩展地址 |
channel | uint8_t | 802.15.4 信道(11..26) |
rssi | int8_t | 接收信号强度(dBm) |
lqi | uint8_t | 链路质量指示 |
threadVersion | uint8_t | 4-bit MLE Thread 版本 |
joinable | bool | 是否允许加入(见 4.4 节) |
nativeCommissioner | bool | Native Commissioner 标志 |
5. 预期串口输出
在 Leader 正常组网时(网络名、XPAN 默认值与仓库 Simple Thread Network 演示一致,如 Extended PAN IDdead00beef00cafe),每 10 秒一轮的典型输出:
Setup done — IPv6 interface up, Thread start not required Thread discovery start Thread discovery done 1 network(s) found Nr | J | Network Name | Extended PAN | PAN | MAC Address | CH | dBm | LQI 1 | 1 | ESP_OpenThread | dead00beef00cafe | 1234 | aabbccddeeff0011 | 15 | -45 | 255 -------------------------------------四种典型分支:
- Leader 不在附近:
Thread discovery start Thread discovery done no Thread networks found -------------------------------------- 失败(接口未 up、加锁失败或超时):
Thread discovery start Thread discovery done discovery failed (interface down, lock failure, or timeout) -------------------------------------- 上一轮扫描尚未结束(阻塞模式下理论上少见,但若上一轮超时后 OpenThread 侧仍在跑,会命中 OThreadScan.cpp 的
OT_DISCOVER_RUNNING分支):
Thread discovery start Thread discovery done discovery already in progress (OT_DISCOVER_RUNNING) -------------------------------------6. 可定制参数
6.1 存储结果上限OT_DISCOVER_MAX_RESULTS
库默认每轮最多保存16个唯一(按 Extended PAN ID 去重)网络。本示例为更密的射频环境将上限提到32:
#define OT_DISCOVER_MAX_RESULTS 32 #include "OThreadScan.h"要求:
#define必须出现在#include "OThreadScan.h"之前(可放在.ino里或更早包含的头文件中);也可通过构建系统传-DOT_DISCOVER_MAX_RESULTS=32;- 值越大,每轮扫描前
prepareResultStorage()预分配的两个 vector(_results与_rawResults)占用的 RAM 越多。
6.2 扫描超时setScanTimeout()
默认30000 ms(阻塞等待与异步完成共用该超时)。调用discoverNetworks()之前调整:
OThreadScan.setScanTimeout(60000); // 单位 ms6.3 信道限制setChannel()
OThreadScan.setChannel(15)将扫描限定在单个信道(合法范围11..26);setChannel(0)(默认)扫全部支持信道。从discoverChannelMask()实现看(OThreadScan.cpp),非法信道值会直接导致返回OT_DISCOVER_FAILED。
6.4 发现过滤器setDiscoverFilters()
OThreadScan.h 中的OThreadDiscoverFilters与 ESP-IDF CLIdiscover的默认行为一致:panIdFilter默认OT_PANID_BROADCAST(0xffff,不过滤),joinerOnly、eui64Filter默认关闭。本阻塞示例未使用过滤器,需要定向扫描时可自行配置。
7. 故障排查
启动顺序:先启动 LeaderNode(组网节点) 并等待Role: Leader,再烧录本示例。
| 现象 | 可能原因 |
|---|---|
no Thread networks found | Leader 未运行或不在射频范围内——用另一块板启动 Leader。 |
discovery failed | 接口未 up、加锁失败或超时——确认已调用networkInterfaceUp();尝试调大setScanTimeout()。 |
discovery already in progress | 上一轮扫描仍在进行(OT_DISCOVER_RUNNING)——等待其结束,或在完成后调用scanDelete()。 |
| 网络名 / XPAN 对不上 | 附近有多个Thread 网络——与 Leader 的 Extended PAN ID 逐一比对。 |
| 串口无任何输出 | 串口监视器波特率未设为115200。 |
另外提醒(来自 ThreadScan 组总览):getResult()/getResultCount()只能在发现完成后使用;不要在onResult()/onComplete()回调内部调用scanDelete()等其他OThreadScan方法——回调运行在 OpenThread 任务中且持有 API 锁,释放应放在loop()里、scanComplete()结束之后进行。
8. 相关示例与延伸阅读
- Thread Network Discovery — 组总览:三种 Native 发现模式对比与统一运行步骤;
- ThreadScan_Async:非阻塞
scanComplete()轮询模式; - ThreadScan_Callback:逐网络流式回调模式;
- CLI ThreadScan:同一
discover操作的OThreadCLICLI 等价写法; - LeaderNode / RouterNode:多板测试用的组网节点与入网节点;
- API 定义与实现:OThreadScan.h、OThreadScan.cpp。
适用前提与限制:本指南基于当前仓库的 OpenThread 库与示例,仅对支持 802.15.4 射频的 ESP32-H2 / C6 / C5 有效,且需要CONFIG_OPENTHREAD_ENABLED=y与CONFIG_SOC_IEEE802154_SUPPORTED=y两项 IDF 配置;阻塞式扫描会占用 CPU 最长一个超时周期(默认 30 秒),实时性要求高的场景请改用 Async 或 Callback 模式。
【免费下载链接】arduino-esp32Arduino core for the ESP32 family of SoCs项目地址: https://gitcode.com/GitHub_Trending/ar/arduino-esp32
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考