Thread 协议实战:低功耗 Mesh 网络搭建指南
Thread 协议实战:低功耗 Mesh 网络搭建指南
Thread 是近年来物联网领域最值得关注的技术之一。作为 Google Nest 牵头推出的基于 IPv6 的低功耗 Mesh 网络协议,Thread 正在快速成为智能家居设备互联的”通用语言”。今天这篇文章,我就带大家从零开始搭建一个 Thread Mesh 网络,用 ESP32-H2 或 ESP32-C6 实现边界路由器,让多个传感器节点自动组网、互相通信。
什么是 Thread?为什么值得关注?
Thread 是一种基于 IEEE 802.15.4 标准的无线 Mesh 网络协议,运行在 2.4GHz 频段。它的核心优势可以总结为三点:
第一,原生支持 IPv6。每个 Thread 设备都有独立的 IPv6 地址,可以直接与互联网通信,不需要额外的协议转换层。这一点和 Zigbee 完全不同——Zigbee 设备需要网关做地址映射,而 Thread 设备天生就是”互联网公民”。
第二,自愈能力强。Thread 采用 Mesh 拓扑结构,设备之间可以互相中继转发数据。如果某个节点掉线,网络会自动寻找新的路由路径,不会导致整个网络瘫痪。
第三,功耗极低。Thread 设备在休眠状态下电流可以降到微安级别,一节纽扣电池就能让传感器工作数年。这对于电池供电的智能家居设备来说至关重要。
目前 Thread 已经获得苹果、谷歌、亚马逊、三星等巨头的支持,并且成为 Matter 协议的底层网络技术。可以说,掌握 Thread 就是掌握了未来智能家居的钥匙。
硬件准备清单
要搭建一个完整的 Thread 网络,你需要以下硬件:
| 组件 | 推荐型号 | 数量 | 价格参考 |
|---|---|---|---|
| 边界路由器 | ESP32-H2-DevKitC-1 或 ESP32-C6-DevKitC-1 | 1块 | ¥25-35 |
| 终端节点 | ESP32-H2-MINI-1 模块或 ESP32-C6 开发板 | 2-3块 | ¥15-25/块 |
| USB 转串口线 | Type-C 数据线 | 3条 | ¥10/条 |
| 面包板 + 杜邦线 | 通用 830 孔面包板 | 1套 | ¥15 |
| 传感器(可选) | DHT22 温湿度传感器、PIR 人体感应模块 | 各1个 | ¥10-15 |
选型建议:
-
ESP32-H2 是乐鑫专门为 Thread/Zigbee 设计的芯片,功耗更低,适合纯 Thread 应用
-
ESP32-C6 同时支持 Wi-Fi 6 和 Thread,适合做边界路由器(Border Router),因为它可以同时连接 Wi-Fi 和 Thread 网络
环境搭建:ESP-IDF + OpenThread
第一步:安装 ESP-IDF
如果你还没安装 ESP-IDF,按以下步骤操作:
# 克隆 ESP-IDF 仓库
git clone -b v5.2 --recursive https://github.com/espressif/esp-idf.git
cd esp-idf
# 安装工具链
./install.sh esp32h2,esp32c6
# 设置环境变量
. ./export.sh
第二步:确认 OpenThread 支持
ESP-IDF v5.0 以上版本已经内置了 OpenThread 协议栈。你可以通过以下命令检查:
idf.py --list-targets | grep -E "esp32h2|esp32c6"
如果看到 esp32h2 和 esp32c6,说明支持已就绪。
实战:搭建 Thread 边界路由器
边界路由器(Border Router)是 Thread 网络与外部网络(通常是 Wi-Fi 或以太网)之间的桥梁。它负责地址分配、路由转发和网络管理。
硬件接线
如果你用 ESP32-C6 作为边界路由器,接线很简单:
-
USB-C 线连接开发板到电脑
-
板载天线已经集成,无需外接
代码实现
在 ESP-IDF 中,Thread 边界路由器的示例代码位于 examples/openthread/ot_br 目录。我们基于这个示例进行修改:
#include
#include
#include "esp_log.h"
#include "esp_event.h"
#include "esp_netif.h"
#include "esp_wifi.h"
#include "esp_openthread.h"
#include "esp_openthread_border_router.h"
#include "esp_openthread_netif_glue.h"
#include "openthread/instance.h"
#include "openthread/tasklet.h"
static const char *TAG = "Thread_BR";
// Wi-Fi 配置
#define WIFI_SSID "your_wifi_ssid"
#define WIFI_PASS "your_wifi_password"
static void wifi_event_handler(void *arg, esp_event_base_t event_base,
int32_t event_id, void *event_data)
{
if (event_id == WIFI_EVENT_STA_START) {
esp_wifi_connect();
} else if (event_id == WIFI_EVENT_STA_DISCONNECTED) {
ESP_LOGW(TAG, "Wi-Fi disconnected, retrying...");
esp_wifi_connect();
} else if (event_id == IP_EVENT_STA_GOT_IP) {
ip_event_got_ip_t *event = (ip_event_got_ip_t *)event_data;
ESP_LOGI(TAG, "Wi-Fi connected, IP: " IPSTR, IP2STR(&event->ip_info.ip));
}
}
static void init_wifi(void)
{
ESP_ERROR_CHECK(esp_netif_init());
ESP_ERROR_CHECK(esp_event_loop_create_default());
esp_netif_create_default_wifi_sta();
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_wifi_init(&cfg));
esp_event_handler_instance_t instance_any_id;
esp_event_handler_instance_t instance_got_ip;
ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT,
ESP_EVENT_ANY_ID,
&wifi_event_handler,
NULL,
&instance_any_id));
ESP_ERROR_CHECK(esp_event_handler_instance_register(IP_EVENT,
IP_EVENT_STA_GOT_IP,
&wifi_event_handler,
NULL,
&instance_got_ip));
wifi_config_t wifi_config = {
.sta = {
.ssid = WIFI_SSID,
.password = WIFI_PASS,
},
};
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config));
ESP_ERROR_CHECK(esp_wifi_start());
}
void app_main(void)
{
ESP_LOGI(TAG, "Starting Thread Border Router...");
// 初始化 Wi-Fi
init_wifi();
// 初始化 OpenThread
esp_openthread_platform_config_t config = {
.radio_config = {
.radio_mode = RADIO_MODE_NATIVE,
},
.port_config = {
.storage_partition_name = "nvs",
.netif_queue_size = 10,
.task_queue_size = 10,
},
};
ESP_ERROR_CHECK(esp_openthread_init(&config));
// 启动边界路由器
ESP_ERROR_CHECK(esp_openthread_border_router_init());
ESP_LOGI(TAG, "Thread Border Router started!");
ESP_LOGI(TAG, "Use 'otcli' commands to manage the network");
// 主循环
while (1) {
otTaskletsProcess(esp_openthread_get_instance());
usleep(100000);
}
}
编译和烧录
# 设置目标芯片
idf.py set-target esp32c6
# 编译
idf.py build
# 烧录(替换为你的串口号)
idf.py -p /dev/ttyACM0 flash monitor
烧录完成后,你会在串口监视器中看到 Thread 边界路由器启动的日志。
实战:创建 Thread 网络并添加节点
第一步:在边界路由器上创建网络
通过串口连接到边界路由器,使用 OpenThread CLI 命令创建网络:
# 查看当前状态
> state
leader
# 查看网络配置
> dataset active
Active Timestamp: 1
Channel: 15
Channel Mask: 0x07fff800
Ext PAN ID: dead00beef00cafe
Mesh Local Prefix: fdde:ad00:beef:0::/64
Network Key: 00112233445566778899aabbccddeeff
Network Name: OpenThread-ESP
PAN ID: 0x1234
Security Policy: 672, onrcb
如果还没有配置,可以用以下命令初始化网络:
# 初始化新的数据集
> dataset init new
# 设置网络名称
> dataset networkname MyThreadNet
# 设置 PAN ID
> dataset panid 0x1234
# 设置网络密钥(可选,默认会生成)
> dataset networkkey 00112233445566778899aabbccddeeff
# 提交数据集
> dataset commit active
# 启用接口
> ifconfig up
# 启动 Thread 协议
> thread start
第二步:配置边界路由器功能
# 启用边界路由功能
> br enable
# 查看边界路由器状态
> br status
第三步:添加终端节点
在另一块 ESP32-H2/C6 上烧录终端节点固件(使用 examples/openthread/ot_cli 示例):
# 设置目标
idf.py set-target esp32h2
# 编译烧录
idf.py build flash
烧录完成后,通过 CLI 加入网络:
# 配置与边界路由器相同的网络参数
> dataset networkname MyThreadNet
> dataset panid 0x1234
> dataset networkkey 00112233445566778899aabbccddeeff
> dataset commit active
# 启动接口和 Thread
> ifconfig up
> thread start
# 查看状态
> state
child
# 查看 IPv6 地址
> ipaddr
fdde:ad00:beef:0:7c68:55ca:4c34:7e9e
fe80:0:0:0:7c68:55ca:4c34:7e9e
如果看到 state 显示为 child 或 router,说明设备已经成功加入网络!
节点间通信实战
Thread 网络最大的魅力在于节点之间可以直接通信。下面是一个简单的 UDP 通信示例。
发送端代码
#include "openthread/udp.h"
#include "openthread/instance.h"
void send_udp_message(otInstance *aInstance, const char *destAddr, uint16_t port, const char *message)
{
otError error;
otMessage *message;
otMessageInfo messageInfo;
otIp6Address destination;
// 解析目标地址
otIp6AddressFromString(destAddr, &destination);
// 创建消息
message = otUdpNewMessage(aInstance, NULL);
if (message == NULL) {
ESP_LOGE(TAG, "Failed to allocate message");
return;
}
// 追加数据
error = otMessageAppend(message, message, strlen(message));
if (error != OT_ERROR_NONE) {
otMessageFree(message);
return;
}
// 配置消息信息
memset(&messageInfo, 0, sizeof(messageInfo));
messageInfo.mPeerPort = port;
messageInfo.mPeerAddr = destination;
// 发送
error = otUdpSendDatagram(aInstance, message, &messageInfo);
if (error == OT_ERROR_NONE) {
ESP_LOGI(TAG, "Message sent to %s:%d", destAddr, port);
}
}
接收端代码
static otUdpSocket sUdpSocket;
void handleUdpReceive(void *aContext, otMessage *aMessage, const otMessageInfo *aMessageInfo)
{
uint8_t buf[128];
int length = otMessageRead(aMessage, otMessageGetOffset(aMessage), buf, sizeof(buf) - 1);
if (length > 0) {
buf = '\0';
ESP_LOGI(TAG, "Received: %s from [%s]:%d",
buf,
otIp6AddressToString(&aMessageInfo->mPeerAddr, NULL, 0),
aMessageInfo->mPeerPort);
}
}
void initUdpServer(otInstance *aInstance, uint16_t port)
{
otSockAddr listenSockAddr;
memset(&sUdpSocket, 0, sizeof(sUdpSocket));
otUdpOpen(aInstance, &sUdpSocket, handleUdpReceive, NULL);
memset(&listenSockAddr, 0, sizeof(listenSockAddr));
listenSockAddr.mPort = port;
otUdpBind(&sUdpSocket, &listenSockAddr, OT_NETIF_THREAD);
ESP_LOGI(TAG, "UDP server listening on port %d", port);
}
常见问题排查
问题一:设备无法加入网络
症状:终端节点一直显示 detached 状态
排查步骤:
-
$1
-
$1
-
$1
-
$1
问题二:节点 IPv6 地址无法 ping 通
症状:从边界路由器无法 ping 通子节点
排查步骤:
-
$1
-
$1
-
$1
-
$1
问题三:功耗过高
症状:电池很快耗尽
解决方案:
// 启用 sleepy 模式
otLinkModeConfig mode;
mode.mRxOnWhenIdle = false;
mode.mSecureDataRequests = true;
mode.mDeviceType = false;
otThreadSetLinkMode(esp_openthread_get_instance(), mode);
// 设置轮询间隔(毫秒)
otLinkSetPollPeriod(esp_openthread_get_instance(), 5000);
进阶:与 Home Assistant 集成
Thread 网络最大的应用场景是智能家居。通过 Home Assistant 的 Thread 集成,你可以:
-
$1
-
$1
-
$1
总结
Thread 协议为物联网设备提供了一种标准化的、低功耗的、自愈的 Mesh 网络解决方案。通过本文的实战,你应该已经掌握了:
-
Thread 协议的核心概念和优势
-
使用 ESP32-H2/C6 搭建边界路由器
-
创建 Thread 网络并添加终端节点
-
节点间的 UDP 通信实现
-
常见问题排查方法
Thread 和 Matter 的结合正在重塑智能家居生态。作为 Maker,现在正是学习 Thread 的最佳时机。动手搭建你的第一个 Thread 网络吧!
参考资源: