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

arduino-esp32 OpenThread Native API:用 ThreadScan_Async 实现非阻塞 Thread 网络扫描 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 APIOThreadScanOThreadNetworkInfo无需解析 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_ENABLEDy编译 OpenThread 协议栈CONFIG_SOC_IEEE802154_SUPPORTEDy确保 SoC 具备 802.15.4 射频这两项与 ci.yml 中声明的requires完全一致也是 OThreadScan.h 整个头文件被编译的宏条件#if SOC_IEEE802154_SUPPORTED CONFIG_OPENTHREAD_ENABLED。二、前置条件先起一个 Leader在 RF 范围内必须先有一台设备作为Leader运行否则扫描无果。标准流程是在第一块 ESP32-H2 / C6 / C5 开发板上烧录 LeaderNode组网示例串口监视器选择115200波特率等待串口打印出Role: Leader在第二块开发板上烧录 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 ID8 字节panIdIEEE 802.15.4 PAN IDextAddress响应方扩展 MAC 地址channel802.15.4 信道11..26rssi接收信号强度dBmlqi链路质量指示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 | extPandead00beef00cafe | pan1234 | aabbccddeeff0011 | ch15 | -45 dBm | lqi255 | joinableyes 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()传入 OThreadDiscoverFilterspanIdFilter默认广播0xffff即不过滤、joinerOnly、eui64Filter本示例未使用但在做定向发现时可用。五、源码级原理OThreadScan 的异步状态机示例的可信度来自 OThreadScan.cpp 中严谨的状态管理以下是最值得理解的四点。5.1 完成判定只信最终回调不信 API 查询scanComplete()的实现OThreadScan.cpp判断顺序是未触发 →FAILED_done已置位 → 返回错误码或结果数超过setScanTimeout()设定时间 →FAILED否则 →RUNNING。其中_done只在onDiscoverResult(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_RESULTS16个唯一网络后续唯一网络只走onResult()流式回调不再入存储并打印告警存储 vector 在discoverNetworks()持锁之前预先reserve回调内push_back不会触发重分配避免在 OpenThread API 锁内做堆分配。需要更大容量时可在#include OThreadScan.h之前#define OT_DISCOVER_MAX_RESULTS 32ThreadScan_Discover 示例中即有演示。5.3 joinable 的判定方式joinable字段对 MLE Discovery 有特殊含义otActiveScanResult.mIsJoinable只在 802.15.4 信标主动扫描时被填充而otThreadDiscover()的 MLE 发现是通过Steering Data 布隆过滤器表达可加入性的。由于公开的otSteeringData*辅助函数需要OPENTHREAD_CONFIG_MESHCOP_STEERING_DATA_API_ENABLEArduino 构建中默认关闭OThreadScan.cpp 直接检查结构体Steering Data 长度为 0 或超出上限则不可加入否则任一非零字节即判定joinable true。这解释了示例输出中joinableyes的物理含义——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 接口未 updiscoverNetworks()之前必须先调用OThread.networkInterfaceUp()完全没有串口输出则检查波特率是否为 115200、USB 口是否正确。七、延伸Thread Network Discovery 组 README三种发现模式的对比、结果读取时机规则getResult()/getResultCount()只能在发现完成后调用与存储上限说明ThreadScan_Discover阻塞式discoverNetworks()以及OT_DISCOVER_MAX_RESULTS的自定义示例ThreadScan_CallbackonResult()/onComplete()流式回调模式OThreadScan.h / OThreadScan.cppNative API 的完整声明与实现含索引式便捷 getternetworkName(i)、rssi(i)、isJoinable(i)等与原始getActiveScanResult(i)接口WiFiScanAsyncWi-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),仅供参考