arduino-esp32 OpenThread Native API:用 ThreadScan_Async 实现非阻塞 Thread 网络扫描
2026/9/14 18:23:38 网站建设 项目流程

arduino-esp32 OpenThread Native API:用 ThreadScan_Async 实现非阻塞 Thread 网络扫描

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

本文基于 ThreadScan_Async 示例 讲解 arduino-esp32 中基于 OpenThread Native API 的 Thread 网络异步发现方案:如何通过OThreadScan.discoverNetworks(true)启动一次不阻塞主循环的 MLE Discovery,并在loop()中轮询scanComplete()获取扫描结果。读完后你将掌握完整的示例代码逐行解读、OThreadScan状态机与返回值语义、底层otThreadDiscover()的回调/去重/去超时实现,以及 ESP32-H2 / C6 / C5 上的配置与排错方法。

一、示例定位:Thread 网络发现的三种模式之一

ThreadScan_Async 属于 Thread Network Discovery 示例组。该组共三个 sketch,全部基于类型化的 Native API(OThreadScan+OThreadNetworkInfo,无需解析 CLI 字符串),底层统一封装otThreadDiscover()(即 ESP-IDF OpenThread 的discoverCLI 命令):

示例模式
ThreadScan_Discover阻塞式discoverNetworks()
ThreadScan_Async非阻塞:discoverNetworks(true)+scanComplete()轮询
ThreadScan_Callback流式onResult()/onComplete()回调

每个结果包含 Thread 身份信息(网络名称、Extended PAN ID、可加入标志)和 IEEE 802.15.4 链路字段(扩展地址、PAN ID、信道、RSSI、LQI)——这正是 Matter 在配网阶段列举 Thread 网络所使用的同一原语。ThreadScan_Async 的价值在于:它演示了与 Wi-FiWiFiScanAsync相同的非阻塞轮询范式,扫描期间loop()可以继续处理其他任务,而不是像阻塞模式那样停在那里等待。

支持的目标板

SoC支持 Thread状态
ESP32-H2支持
ESP32-C6支持
ESP32-C5支持

必需的 IDF 特性(sdkconfig)

特性作用
CONFIG_OPENTHREAD_ENABLED=y编译 OpenThread 协议栈
CONFIG_SOC_IEEE802154_SUPPORTED=y确保 SoC 具备 802.15.4 射频

这两项与 ci.yml 中声明的requires完全一致,也是 OThreadScan.h 整个头文件被编译的宏条件(#if SOC_IEEE802154_SUPPORTED && CONFIG_OPENTHREAD_ENABLED)。

二、前置条件:先起一个 Leader

在 RF 范围内必须先有一台设备作为Leader运行,否则扫描无果。标准流程是:

  1. 在第一块 ESP32-H2 / C6 / C5 开发板上烧录 LeaderNode(组网示例),串口监视器选择115200波特率;
  2. 等待串口打印出Role: Leader
  3. 在第二块开发板上烧录 ThreadScan_Asketch 并同样以 115200 打开串口,确认发现输出。

一个容易忽略的要点(组 README 中特别强调):发现(discovery)并不需要启动 Thread 协议栈OThread.begin(false)之后只要调用OThread.networkInterfaceUp()拉起 IPv6 接口即可。这与 Matter 预配网(pre-commission)阶段的发现行为一致。

三、示例代码逐行解读

完整源码见 ThreadScan_Async.ino。README 给出的核心骨架如下:

// 1) 协议栈 + IPv6 接口。 OThread.begin(false); OThread.networkInterfaceUp(); // 2) 启动异步发现(返回 OT_DISCOVER_RUNNING)。 OThreadScan.setScanTimeout(30000); OThreadScan.discoverNetworks(true); // 3) 在 loop() 中轮询,同时执行其他工作。 int16_t status = OThreadScan.scanComplete(); if (status >= 0) { const OThreadNetworkInfo &net = OThreadScan.getResult(0); OThreadScan.scanDelete(); }

下面是完整实现的逐段解析。

3.1 状态变量与启动函数

static bool discoverPending = false; static void startAsyncDiscover() { Serial.println("Thread discovery async start"); OThreadScan.setScanTimeout(30000); int16_t rc = OThreadScan.discoverNetworks(true); if (rc == OT_DISCOVER_RUNNING) { discoverPending = true; } else if (rc == OT_DISCOVER_FAILED) { Serial.println("discovery failed to start"); discoverPending = false; } else { Serial.printf("discovery returned immediately with %d network(s)\r\n", rc); discoverPending = false; } }

discoverNetworks(true)的返回值有三种形态,示例用三分支穷尽处理:

  • OT_DISCOVER_RUNNING(值为-1):扫描已启动,稍后轮询;
  • OT_DISCOVER_FAILED(值为-2):启动失败(接口未 up、已在扫描中等);
  • 非负整数:同步返回了结果数量(异步模式下正常不会走到,但防御性处理)。

这两个哨兵常量定义在 OThreadScan.h,注释明确说明其命名约定与 Wi-Fi 侧的WIFI_SCAN_RUNNING/WIFI_SCAN_FAILED完全对齐——这是"WiFiScanAsync 风格"说法的直接出处。

3.2 结果打印与内存释放

static void printResults(int count) { if (count == 0) { Serial.println("no Thread networks found"); } else { Serial.printf("%d network(s):\r\n", count); for (int i = 0; i < count; ++i) { const OThreadNetworkInfo &net = OThreadScan.getResult(i); Serial.printf( " [%d] %s | extPan=%s | pan=%04x | %s | ch=%u | %d dBm | lqi=%u | joinable=%s\r\n", i, net.networkName, net.extendedPanIdStr().c_str(), net.panId, net.extAddressStr().c_str(), net.channel, net.rssi, net.lqi, net.joinable ? "yes" : "no"); } } // 每次扫描完成后(包括 0 结果)都释放预留容量。 OThreadScan.scanDelete(); }

OThreadNetworkInfo结构(见 OThreadScan.h)承载单条发现结果:

字段含义
networkNameThread 网络名称(以\0结尾)
extendedPanIdExtended PAN ID(8 字节)
panIdIEEE 802.15.4 PAN ID
extAddress响应方扩展 MAC 地址
channel802.15.4 信道(11..26)
rssi接收信号强度(dBm)
lqi链路质量指示
threadVersion4 位 MLE Thread 版本
joinable是否允许加入(MLE 发现下由 Steering Data 判定)
nativeCommissionerNative Commissioner 标志

getResult(i)返回的是内部存储的引用,有效期持续到scanDelete()被调用;scanDelete()通过std::vector::swap与空容器交换,真正释放扫描期预留的堆内存(对齐 Wi-Fi/Zigbee 的scanDelete()行为)。源码注释特别指出:scanDelete()在最终发现回调完成(内部_done置位)之前是 no-op,过早调用会与完成路径竞态,因此"每次扫描完成后(含 0 结果)都调用一次"是正确的用法。

3.3 完成处理与自动重启

static void handleDiscoverComplete(int16_t status) { if (status == OT_DISCOVER_FAILED) { Serial.println("async discovery failed — restarting"); } else { Serial.println("async discovery done"); printResults(status); } discoverPending = false; }

setup()中先初始化串口、拉起协议栈与 IPv6 接口,然后立即发起第一次发现;loop()是核心状态机:

void loop() { if (discoverPending) { int16_t status = OThreadScan.scanComplete(); if (status < 0) { if (status == OT_DISCOVER_FAILED) { handleDiscoverComplete(status); delay(2000); startAsyncDiscover(); } } else { handleDiscoverComplete(status); delay(2000); startAsyncDiscover(); } } else { startAsyncDiscover(); } delay(250); Serial.println("loop running..."); }

逻辑要点:

  • 扫描进行中(scanComplete()返回OT_DISCOVER_RUNNING)时什么都不做,只让loop()继续"干活"(示例中打印loop running...占位,实际项目里这里就是放其他业务代码的地方);
  • status >= 0表示完成,数值本身即结果数量,直接传给printResults(status)
  • 完成或失败后delay(2000)再自动重启下一轮发现,形成持续巡检循环;
  • 每轮delay(250)兼作轮询节流。

3.4 预期串口输出

成功路径:

Setup done — starting async discovery Thread discovery async start loop running... loop running... async discovery done 1 network(s): [0] ESP_OpenThread | extPan=dead00beef00cafe | pan=1234 | aabbccddeeff0011 | ch=15 | -45 dBm | lqi=255 | joinable=yes Thread discovery async start loop running...

失败路径(无法启动或超时):

discovery failed to start async discovery failed — restarting

四、参数定制

调用作用
OThreadScan.setScanTimeout(30000)整体超时,单位毫秒(本示例在startAsyncDiscover()中设置;默认值 30000 ms,由OT_DISCOVER_DEFAULT_TIMEOUT_MS定义)
OThreadScan.setChannel(15)只扫描单个信道;省略或传0表示扫全部信道

从 OThreadScan.cpp 的discoverChannelMask()可以看到信道参数的精确语义:0映射为channelMask = 0(交给 OpenThread 使用全部支持信道),11..26映射为1U << channel的单信道掩码,超出 11..26 范围直接判为参数非法,discoverNetworks()返回OT_DISCOVER_FAILED

此外,头文件还支持通过setDiscoverFilters()传入 OThreadDiscoverFilters(panIdFilter默认广播0xffff即不过滤、joinerOnlyeui64Filter),本示例未使用,但在做定向发现时可用。

五、源码级原理:OThreadScan 的异步状态机

示例的可信度来自 OThreadScan.cpp 中严谨的状态管理,以下是最值得理解的四点。

5.1 完成判定只信"最终回调",不信 API 查询

scanComplete()的实现(OThreadScan.cpp)判断顺序是:未触发 →FAILED_done已置位 → 返回错误码或结果数;超过setScanTimeout()设定时间 →FAILED;否则 →RUNNING

其中_doneonDiscoverResult(nullptr)(OpenThread 以空指针参数回调,标志发现结束)中置位。源码注释解释了原因:不能依赖otThreadIsDiscoverInProgress()推断完成,因为 OpenThread 可能在"最终回调派发之前"就报告自己处于空闲,提前判定完成会丢失结果计数。超时判定则基于millis() - _startedMs > _timeoutMs,这正是setScanTimeout()对异步模式生效的机制。

5.2 结果去重:同 Extended PAN ID 保留最强 RSSI

每条 Discovery Response 到达时(onDiscoverResult),先按 Extended PAN ID 查重:

  • 若同一 Thread 网络来自另一台路由器/信标,仅当新 RSSI 更高时替换既有记录;
  • 若存储已满(默认上限OT_DISCOVER_MAX_RESULTS=16个唯一网络),后续唯一网络只走onResult()流式回调,不再入存储并打印告警;
  • 存储 vector 在discoverNetworks()持锁之前预先reserve,回调内push_back不会触发重分配(避免在 OpenThread API 锁内做堆分配)。

需要更大容量时,可在#include "OThreadScan.h"之前#define OT_DISCOVER_MAX_RESULTS 32(ThreadScan_Discover 示例中即有演示)。

5.3 joinable 的判定方式

joinable字段对 MLE Discovery 有特殊含义:otActiveScanResult.mIsJoinable只在 802.15.4 信标主动扫描时被填充,而otThreadDiscover()的 MLE 发现是通过Steering Data 布隆过滤器表达可加入性的。由于公开的otSteeringData*辅助函数需要OPENTHREAD_CONFIG_MESHCOP_STEERING_DATA_API_ENABLE(Arduino 构建中默认关闭),OThreadScan.cpp 直接检查结构体:Steering Data 长度为 0 或超出上限则不可加入,否则任一非零字节即判定joinable = true。这解释了示例输出中joinable=yes的物理含义——Leader 的 MeshCoP Steering Data 非全零。

5.4 阻塞与异步共用同一条完成路径

discoverNetworks(false)(阻塞模式)内部通过xSemaphoreTake(_doneSem, pdMS_TO_TICKS(_timeoutMs))等待同一把完成信号量;_doneSem在最终回调(aResult == nullptr)中xSemaphoreGive。因此阻塞/异步/回调三种模式最终都由onDiscoverResult的完成分支统一收尾:置_done、给信号量(唤醒阻塞调用)、调用onComplete()(若注册)。这也解释了为什么头文件注释要求不要在onResult()/onComplete()回调内调用scanDelete()等其他方法——这些回调运行在 OpenThread 任务上下文且持有 API 锁,释放结果应回到loop()中在scanComplete()完成后进行。

六、故障排查

启动顺序:先启动 LeaderNode 并等到Role: Leader,再烧录本示例。

现象可能原因
一直停在loop running...扫描仍在进行——到超时或完成前的正常状态
async discovery failedLeader 不在附近或超时——调大setScanTimeout()
discovery failed to start已有扫描在进行中,或网络接口未 up
结果中没有网络Leader 未运行——先在另一块板上启动 Leader

组 README 还补充了两条通用排查项:discovery failed也可能是 IPv6 接口未 up(discoverNetworks()之前必须先调用OThread.networkInterfaceUp());完全没有串口输出则检查波特率是否为 115200、USB 口是否正确。

七、延伸

  • Thread Network Discovery 组 README:三种发现模式的对比、结果读取时机规则(getResult()/getResultCount()只能在发现完成后调用)与存储上限说明;
  • ThreadScan_Discover:阻塞式discoverNetworks(),以及OT_DISCOVER_MAX_RESULTS的自定义示例;
  • ThreadScan_Callback:onResult()/onComplete()流式回调模式;
  • OThreadScan.h / OThreadScan.cpp:Native API 的完整声明与实现,含索引式便捷 getter(networkName(i)rssi(i)isJoinable(i)等)与原始getActiveScanResult(i)接口;
  • WiFiScanAsync:Wi-Fi 侧同一套异步轮询范式,API 约定(RUNNING/FAILED哨兵值)完全对齐。

本示例遵循 Apache License 2.0。

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

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

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

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

立即咨询