arduino-esp32 OpenThread 阻塞式 Thread 网络发现(ThreadScan_Discover)实战指南
2026/9/14 15:57:08 网站建设 项目流程

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 网络所用的同一原语。

该示例的核心行为:

  1. OThread.begin(false)启动 OpenThread(从 NVS 自动加载数据集);
  2. 通过networkInterfaceUp()拉起 IPv6 接口(无需启动 Thread);
  3. loop()中每 10 秒调用一次阻塞式discoverNetworks()
  4. 打印每个OThreadNetworkInfo(网络名、Extended PAN ID、PAN ID、扩展地址、信道、RSSI、LQI、joinable 标志);
  5. 每次扫描结束后用scanDelete()释放结果存储。

1.1 支持的芯片

SoCThread状态
ESP32-H2yesSupported
ESP32-C6yesSupported
ESP32-C5yesSupported

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 网络。推荐做法:

  1. 在第一块 ESP32-H2 / C6 / C5 开发板上烧录 LeaderNode(组网节点) 示例,串口监视器波特率设为115200
  2. 等待串口输出Role: Leader(它构建完整运营数据集——网络名ESP_OpenThread、信道 15、PAN ID0x1234、扩展 PAN ID、网络密钥——并提交并成为新分区 Leader);
  3. 将本示例烧录到第二块板卡。

启动顺序(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):参数OThreadAutoStartfalse时不自动启动 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
≥ 00..N成功,返回去重后的网络数量

4.2 阻塞流程与锁/信号量

从 OThreadScan.cpp 的discoverNetworks()实现看,完整流程为:

  1. 锁外准备:惰性创建完成信号量_doneSem,并prepareResultStorage()预分配容量为OT_DISCOVER_MAX_RESULTS的结果 vector——注释特意说明"堆分配不能在持有 OT API 锁时执行";
  2. 加锁启动:通过esp_openthread_lock_acquire获取 API 锁后(OtLockRAII 包装),检查实例有效性;若_inProgress || otThreadIsDiscoverInProgress(inst)则立即返回OT_DISCOVER_RUNNING;将_channel==0视为全信道、合法信道(11..26)映射为位掩码,随后调用otThreadDiscover(inst, channelMask, panIdFilter, joinerOnly, eui64Filter, handleDiscoverResult, this)
  3. 阻塞等待xSemaphoreTake(_doneSem, pdMS_TO_TICKS(_timeoutMs)),超时默认 30000 ms(OT_DISCOVER_DEFAULT_TIMEOUT_MS,见 OThreadScan.h),超时或_startError非零均返回OT_DISCOVER_FAILED
  4. 完成信号: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,示例串口表格逐列对应这些字段:

字段类型含义
networkNamechar[OT_NETWORK_NAME_MAX_SIZE+1]\0结尾的 Thread 网络名
extendedPanIduint8_t[8]8 字节 Extended PAN ID(extendedPanIdStr()输出 16 位小写 hex)
panIduint16_tIEEE 802.15.4 PAN ID(示例中以%04x打印)
extAddressuint8_t[8]响应方扩展地址
channeluint8_t802.15.4 信道(11..26)
rssiint8_t接收信号强度(dBm)
lqiuint8_t链路质量指示
threadVersionuint8_t4-bit MLE Thread 版本
joinablebool是否允许加入(见 4.4 节)
nativeCommissionerboolNative 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); // 单位 ms

6.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,不过滤),joinerOnlyeui64Filter默认关闭。本阻塞示例未使用过滤器,需要定向扫描时可自行配置。

7. 故障排查

启动顺序:先启动 LeaderNode(组网节点) 并等待Role: Leader,再烧录本示例。

现象可能原因
no Thread networks foundLeader 未运行或不在射频范围内——用另一块板启动 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=yCONFIG_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),仅供参考

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

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

立即咨询