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-11块¥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"

如果看到 esp32h2esp32c6,说明支持已就绪。

实战:搭建 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 显示为 childrouter,说明设备已经成功加入网络!

节点间通信实战

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

  2. $1

  3. $1

  4. $1

问题二:节点 IPv6 地址无法 ping 通

症状:从边界路由器无法 ping 通子节点

排查步骤

  1. $1

  2. $1

  3. $1

  4. $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

  2. $1

  3. $1

总结

Thread 协议为物联网设备提供了一种标准化的、低功耗的、自愈的 Mesh 网络解决方案。通过本文的实战,你应该已经掌握了:

  • Thread 协议的核心概念和优势

  • 使用 ESP32-H2/C6 搭建边界路由器

  • 创建 Thread 网络并添加终端节点

  • 节点间的 UDP 通信实现

  • 常见问题排查方法

Thread 和 Matter 的结合正在重塑智能家居生态。作为 Maker,现在正是学习 Thread 的最佳时机。动手搭建你的第一个 Thread 网络吧!

参考资源