Home Assistant Matter 测试环境搭建全攻略
如何快速搭建自己的 Home Assistant(家庭助手,以下简称 HA)测试环境?为什么越来越多的工程师用它来测试 Matter 设备?它和苹果家庭(Apple Home)、命令行工具 chip-tool 到底有什么区别?本文将对以上问题进行论述,并手把手带大家从零搭建一套 HA Matter 测试环境:覆盖 Ubuntu 宿主机、OpenThread Border Router(OTBR,OpenThread边界路由器)Docker 容器、Matter Server(Matter 服务器)、nRF52840 Dongle 射频协处理器烧录,以及 iOS/Android 手机配网。设备端统一使用 nRF Connect SDK(NCS)v3.4.0 的 light_bulb(智能灯泡)标准例程,分别演示 Matter over Thread(nRF54L15DK)和 Matter over Wi-Fi(nRF7002DK/nRF5340)两种方案。
本文不讲设备端代码,重点讲 Ubuntu 环境怎么搭、HA 测试流程怎么走、坑在哪里。
为了帮助大家快速搭建 HA Matter 测试环境,我将反复验证通过的完整脚本打包,并放于 4.13 节。您只需将脚本拷贝至 Ubuntu 电脑并直接执行,即可一键完成环境搭建。
本文还同时讲述了如何搭建 chip-tool Matter 测试环境,方便读者在两个测试体系中来回切换。
1. 前言:为什么用 Home Assistant 测 Matter 设备
1.1 Home Assistant 是什么
Home Assistant 是一个开源(open source)的本地化智能家居平台(smart home platform)。它跑在你自己的机器上(PC、树莓派、NAS、Docker 容器均可),将不同品牌和不同协议的设备统一纳管,并提供 Web 界面、手机 App 和自动化引擎。HA 原生集成了众多智能家居协议——Philips Hue、IKEA Tradfri、Samsung SmartThings、涂鸦(Tuya)、Google Assistant 等,Matter 也在其中。对 Matter 设备开发和测试来说,最关键的一点是:HA 同时内置了 Matter 控制器(Matter Controller)和 Thread 边界路由器(Thread Border Router)能力,一套环境就能把"配网 → 控制 → 诊断"全链路跑通。
如下所示,是 HA 的总览界面:

1.2 与商业生态、chip-tool 相比,HA 的优劣势
目前市面上测 Matter 设备主要有三条路径:商业生态(苹果家庭 Apple Home、三星 SmartThings 等)、命令行工具 chip-tool、以及 Home Assistant。我们用一张表对比:
| 维度 | Apple Home | chip-tool | Home Assistant |
|---|---|---|---|
| 定位 | 面向消费者的封闭生态 | Matter SDK 自带协议测试工具 | 开源智能家居平台 |
| 日志 / 可观测性 | 极弱,失败仅提示"无法添加配件" | 极详细,但均为协议层原始日志 | Matter Server + OTBR 日志齐全、可读性好 |
| Thread 边界路由器 | 须购买 HomePod 或 Apple TV | 自建 OTBR | 自建 OTBR |
| 配网体验 | 真实用户体验 | 纯命令行 | 图形界面,接近真实用户体验 |
| 适用场景 | 商业验证 | 协议一致性测试、快速冒烟 | Demo 演示、研发日常测试 |
1.3 用 HA 测 Matter 设备的具体好处
HA 有两大核心价值:一是支持的 Matter 设备品类广——窗帘、能源管理等最新 Matter 设备类型都能纳管,还能同时测试米家、涂鸦等其他生态的设备;二是提供可读的调试日志。苹果家庭是黑盒,配网失败你完全不知道卡在哪一步;chip-tool 虽然日志详尽,但它只是协议工具,其行为不能代表真实智能家居平台。HA 恰好落在二者之间:既有接近真实用户的配网路径和图形界面,又能通过 docker logs matter-server 看清每一步细节。
具体而言:
- 一套环境搞定多品类:HA 可同时接入 Matter over Thread 和 Matter over Wi-Fi 设备,还能横向对比其他协议设备的兼容性。
- Thread 与 Wi-Fi 通吃:两种传输方式的 Matter 设备均能测试。
- 可复现真实用户问题:HA 拥有庞大的终端用户群,用它测试能提前暴露真实场景才会触发的边界问题。
- 全链路可诊断:从 RCP 射频、Thread 组网、SRP 注册、mDNS 解析到 IPv6 转发,每一层均可独立验证——这正是本文第 8 章「诊断与排查」的基础。
1.4 HA 的局限性
凡事有两面,HA 并非银弹:
- 搭建门槛高:Docker + host 网络 + mDNS + IPv6 转发,对 Linux 网络知识要求不低——本文的篇幅主要花在把这些坑说清楚。
- Thread/OTBR 坑点密集:服务注册、设备发现、IPv6 转发,任何一个环节出问题都表现为"配网失败",排查链条长。
- 非 CSA 认证级测试工具:HA 是面向终端用户的开源平台,其 Matter 协议行为与商业生态(Apple / Google / SmartThings)并不完全一致。做 Matter 正式认证(certification),最终需回归 CSA 官方 Test Harness(TH,测试框架)或 chip-tool(见第 7 章)。
一句话总结:做 Demo 和研发测试,HA 是性价比最高的选择;做认证,回官方工具。
2. 架构总览
先把整个系统看清楚。Matter 测试环境横跨多个层次,且 Matter over Thread 与 Matter over Wi-Fi 设备的数据路径不同:
要点:
- Matter over Thread 设备(nRF54L15):通过 802.15.4 射频加入 Thread 网络 → 向 OTBR 的 SRP 服务器注册操作服务 → SRP→Avahi 桥将服务发布到宿主机 mDNS → 手机通过 Wi-Fi 经 Avahi 发现设备。
- Matter over Wi-Fi 设备(nRF7002):直接通过 Wi-Fi 连接局域网,Matter Server 通过 IP 直连与之通信,不经过 Thread 路径,也不依赖 SRP 桥。其 mDNS 服务由设备自身或局域网内 mDNS 基础设施处理。
| 层 | 组件 | 职责 |
|---|---|---|
| 手机 | HA App(iOS / Android) | 扫码触发配网、远程控制 |
| 智能家居平台 | Home Assistant | 设备管理、集成调度、自动化 |
| Matter 控制器 | Matter Server(python-matter-server) | 执行配网流程、与设备建立 CASE 会话 |
| 边界路由 | OTBR(OpenThread Border Router) | Thread 网络 ↔ Wi-Fi 网络的 IPv6 桥接 |
| 射频 | nRF52840 RCP | 802.15.4 射频收发 |
| 设备 | nRF54L15(Thread)/ nRF7002(Wi-Fi) | Matter 终端设备 |
3. 环境准备
3.1 硬件清单
| 硬件 | 用途 | 备注 |
|---|---|---|
| Ubuntu 22.04 PC | 宿主机,运行所有容器 | 本文使用 Ubuntu 22.04 LTS |
| nRF52840 Dongle(或 nRF52840DK) | Thread RCP(射频协处理器) | /dev/ttyACM0 |
| nRF54L15DK | Thread 设备 | 烧录 light_bulb 例程 |
| nRF7002DK / nRF5340 | Wi-Fi 设备 | 烧录 light_bulb 例程 |
| iPhone(iOS 26+) | Matter 配网 | 需安装 HA App |
| Android 手机 | Matter 配网 | 需安装 HA App + Google Play 服务 |
3.2 软件环境
- 宿主机:Ubuntu 22.04.5 LTS
- Docker:29.1.3
- OTBR 镜像:
nrfconnect/otbr:fbde28a(匹配 NCS v3.3.0 / v3.4.0) - NCS:v3.3.0 或 v3.4.0(用于编译 RCP 固件)
- chip-tool:
chip-tool_x64(Linux x86_64 预编译,从 nrfconnect/sdk-connectedhomeip Releases 下载),作为与 HA 并行的另一套控制器体系(详见第 7 章)
3.3 配置 nRF52840 Dongle 为 Thread RCP
OTBR 容器自身不带 802.15.4 射频收发能力,Thread 无线通信必须依赖一颗协处理器。在 RCP(Radio Co-Processor,射频协处理器)模式下,dongle 只承担物理层射频收发,完整的 Thread 协议栈跑在宿主机的 OTBR 容器内,二者通过 Spinel 协议经 UART/USB 通信。这也是 OTBR --radio-url 指向 /dev/ttyACM0 的根本原因——它对应的正是这颗 RCP。
烧录分两步:
第一步:编译 RCP 固件。用 NCS 编译 coprocessor(协处理器)示例:
# 进入 coprocessor 示例目录,编译nRF52840 Dongle版本
west build -b nrf52840dongle/nrf52840 nrf/samples/openthread/coprocessor
注:本文所用版本的 UART 波特率为 1 Mbps。
第二步:烧录固件到 dongle。dongle 必须先进入 DFU(Device Firmware Update,设备固件升级)模式——按住Dongle上的 RESET 键并插入电脑的USB口,红色 LED 开始呼吸闪烁即表示已进入。选择下列两种方式之一烧录:
-
nRF Connect Programmer(图形界面):将
build/coprocessor/zephyr/zephyr.hex拖入 Programmer 烧写,如下所示:
-
nrfutil(命令行):需使用
zephyr.zip包,命令如下:# -p 指向 dongle 串口:Windows 为 COMx,Ubuntu 为 /dev/ttyACMx nrfutil dfu usb-serial -pkg build/zephyr/zephyr.zip -p COM0
关键约束:波特率必须一致。OTBR 的
--radio-url中uart-baudrate=1000000必须与 RCP 固件的波特率匹配(本文统一为 1000000),否则会出现RadioSpinelNoResponse超时错误。
参考文档:Nordic Thread tools — Thread Border Router。
3.4 Ubuntu 环境配置
先确认 Docker 已安装:
docker --version
# Docker version 29.1.3, build 29.1.3-0ubuntu3~22.04.2
若尚未安装,执行:
# 安装 Docker(官方脚本)
curl -fsSL https://get.docker.com | sudo sh
# 将当前用户加入 docker 组,免 sudo 运行
sudo usermod -aG docker $USER
# 重启使权限生效
sudo reboot
加载必要的内核模块:
sudo modprobe ip6table_filter
确保 Avahi(mDNS 守护进程)正常运行:
sudo systemctl enable --now avahi-daemon
注:如果
avahi-publish发布后avahi-browse看不到,说明 Ubuntu 的avahi-daemon包损坏。执行sudo apt purge -y avahi-daemon avahi-utils && sudo apt install -y avahi-daemon avahi-utils后重试。
4. HA Matter 测试环境搭建
整个环境的搭建遵循如下顺序,每一步的输出是下一步的前提:
以下各节逐一展开。为方便重复搭建,全部操作已整合为一键脚本 reset-ha-wifi.sh(完整脚本见 4.13 节)。
4.1 硬件自检
先确认 RCP dongle 已插入并被系统识别:
ls /dev/ttyACM*
# /dev/ttyACM0
若未出现 /dev/ttyACM0,检查 dongle 是否正确插入并已烧录 RCP 固件(见第 3.3 节)。
4.2 清理残留环境
如果此前搭建过但未成功,再次搭建前需要彻底清理旧容器和残留进程,避免端口占用或旧配置干扰:
# 停止并删除三个核心容器
docker stop home-assistant && docker rm home-assistant
docker stop matter-server && docker rm matter-server
docker stop otbr && docker rm otbr
# 杀掉残留的 SRP 桥脚本和 avahi 发布进程
pkill -f srp-bridge
pkill -f avahi-publish
4.3 恢复 Avahi、创建数据目录并准备 PAA 证书
Avahi 恢复。Avahi 是宿主机的 mDNS 守护进程,手机端 HA App 通过它发现 OTBR(_meshcop._udp)和 Matter 设备(_matter._tcp)。若 Avahi 曾被禁用(mask),需要先解除并重启:
sudo systemctl unmask avahi-daemon.socket avahi-daemon.service
sudo systemctl enable avahi-daemon.socket avahi-daemon.service
sudo systemctl restart avahi-daemon
Avahi 功能验证。启动 Avahi 后,不能仅凭 systemctl is-active 判断正常——Avahi 的 DBus 激活链路可能损坏(进程在跑但发布不了服务)。用以下前台测试验证其发布功能确实可用:
# 前台运行 avahi-publish 3 秒,检查是否打印 Established
TEST_OUT=$(timeout 3 avahi-publish -s __test__ _test._tcp 9999 -H test.local TXT=x 2>&1) || true
echo "$TEST_OUT" | grep -q 'Established' && echo "Avahi OK" || echo "Avahi 异常"
若未出现 Established,说明 DBus 激活链路损坏,需 purge 后重装:
sudo apt purge -y avahi-daemon avahi-utils
sudo apt install -y avahi-daemon avahi-utils
# 再次用上述测试确认 Established
创建数据目录。所有容器的持久化数据统一放在 ~/ha-docker/ 下:
# PAA 证书需持久化(避免每次重置重新下载),单独创建目录
mkdir -p ~/ha-docker/otbr/data \
~/ha-docker/matter-server/data \
~/ha-docker/config \
~/ha-docker/matter-server/paa \
~/ha-docker/matter-server/fallback-paa
PAA 证书(Product Attestation Authority)说明。Matter 设备配网时,控制器需要验证设备的 DAC(Device Attestation Certificate,设备认证证书)是否由可信的 PAA 签发。对于 NCS 官方例程(VID=0xFFF1),使用的是 Chip-Test-PAA 测试证书。Matter Server 首次启动时会从 GitHub 自动拉取 PAA 根证书;如果网络不佳导致拉取失败,配网将报 Failed Device Attestation。我们采取两道防线:
- 备份/恢复机制:重置脚本在删除旧数据前会备份
~/ha-docker/matter-server/paa/到/tmp/paa-backup/,建好新目录后恢复。避免每次重置都重新下载。 - 本地兜底证书:将 Chip-Test-PAA 的 PEM 文件预先写入
fallback-paa/。Matter Server 启动后检测到 GitHub 拉取失败时,会自动将兜底证书注入容器的/paa目录并重启生效——这一步由start-matter.sh自动完成(见第 4.8 节)。
提示:如果配网报
Failed Device Attestation,手动执行~/inject-paa-fallback.sh(完整脚本末尾有提示)即可注入兜底证书。
4.4 启动 Home Assistant 容器
我们用 Docker Compose 而非 docker run 管理 HA 容器,便于后续统一启停和配置版本控制。HA 采用 network_mode: host 与宿主机共享网络栈:
# 创建项目目录
mkdir -p ~/ha-docker/config
# HA 配置文件:限制 zeroconf 只监听物理网卡,
# 避免 Docker/虚拟网卡接口干扰 mDNS 自动发现
cat > ~/ha-docker/config/configuration.yaml << 'EOF'
# Loads default set of integrations. Do not remove.
default_config:
# Load frontend themes from the themes folder
frontend:
themes: !include_dir_merge_named themes
automation: !include automations.yaml
script: !include scripts.yaml
scene: !include scenes.yaml
# 限制 zeroconf 只监听物理网卡,避免 198.18.0.1 等虚拟接口干扰 mDNS 发现
zeroconf:
default_interface: true
EOF
# HA default_config 的 automation/script/scene 集成要求这些文件存在
touch ~/ha-docker/config/automations.yaml \
~/ha-docker/config/scripts.yaml \
~/ha-docker/config/scenes.yaml
# 创建 docker-compose.yml
cat > ~/ha-docker/docker-compose.yml << 'EOF'
services:
home-assistant:
container_name: home-assistant
image: ghcr.io/home-assistant/home-assistant:stable
network_mode: host
privileged: true
restart: unless-stopped
volumes:
- ./config:/config
- /etc/localtime:/etc/localtime:ro
- /run/dbus:/run/dbus:ro
EOF
# 启动 HA 容器
cd ~/ha-docker
docker compose up -d
# [+] up 1/1
# ✔ Container home-assistant Started
cd ..
network_mode: host:与宿主机共享网络,使 HA 能够收发 mDNS 广播并发现 Thread 设备。privileged: true:允许容器访问蓝牙、USB 等硬件资源。/run/dbus:/run/dbus:ro:挂载 D-Bus 供蓝牙等系统服务使用。
4.5 启动 OTBR 容器
OTBR 同样采用 host 网络模式,确保其 mDNS 广播能够到达宿主机 Wi-Fi 网卡,从而被手机发现。注意:这与 chip-tool 测试环境下的 OTBR 配置不同(chip-tool 使用 bridge 模式,见第 7 章)。
为便于反复重建,将 OTBR 启动封装为独立脚本 start-otbr.sh(reset-ha-wifi.sh 会自动生成并调用它,你也可以单独执行):
cat > ~/ha-docker/start-otbr.sh << EOF
#!/bin/bash
docker stop otbr 2>/dev/null || true
docker rm -f otbr 2>/dev/null || true
docker run -d \
--name otbr --privileged --network host \
--volume /dev/ttyACM0:/dev/ttyACM0 \
--volume ~/ha-docker/otbr/data:/data \
nrfconnect/otbr:fbde28a \
--radio-url "spinel+hdlc+uart:///dev/ttyACM0?uart-baudrate=1000000" \
-B __IFACE__ --disable-firewall
EOF
# 将 __IFACE__ 替换为实际的 Wi-Fi 接口名(用 ip a 查看)
sed -i "s/__IFACE__/wlp0s20f3/" ~/ha-docker/start-otbr.sh
chmod +x ~/ha-docker/start-otbr.sh
~/ha-docker/start-otbr.sh
# 查看 OTBR 运行状态
docker logs otbr --tail 30
两个关键参数:
--network host:OTBR 和宿主机共享网络栈,mDNS 广播才能到达 Wi-Fi 网段;若改为 Docker bridge 模式,mDNS 将被局限在桥内,手机端无法发现。-B wlp0s20f3:指定 OTBR 将 Thread 服务的 mDNS 广播代理到 Wi-Fi 网卡。wlp0s20f3是本机 Wi-Fi 接口名,你需要替换为实际的接口名(用ip a查看)。
4.6 创建 Thread 网络
OTBR 启动后,先对 RCP 执行出厂重置以清除可能残留的旧 Thread 状态,再创建全新的 Thread 网络:
# 出厂重置 RCP,清除旧 Thread 状态
# 这一步是解决 "SRP update error: operation refused for security reasons" 的关键
docker exec otbr ot-ctl factoryreset
sleep 3
# 初始化并启动 Thread 网络
docker exec otbr bash -c "
ot-ctl dataset init new && # 生成新 Thread 网络参数
ot-ctl dataset commit active && # 提交为活动数据集
ot-ctl ifconfig up && # 启用 Thread 接口
ot-ctl thread start # 启动 Thread 协议栈
"
# 等待网络形成(需多次执行直到 leader)
docker exec otbr ot-ctl state
# leader
# Done
docker exec otbr ot-ctl state 变为 leader 需要一定时间。若长时间停留在 detached 或 disabled,检查 dongle 连接和 RCP 固件波特率是否匹配。
4.7 配置 nftables IPv6 转发
Docker 创建的 nftables IPv6 FORWARD 链默认策略是 DROP,且 wpan0(Thread 网卡)不在放行清单中。这会导致:设备已加入 Thread 网络、SRP 注册成功、Avahi 也能解析服务,但 Matter Server 就是无法与设备建立 IPv6 连接。
# 放行 Wi-Fi 网卡 ↔ Thread 网卡的双向 IPv6 转发
sudo nft add rule ip6 filter FORWARD iifname wlp0s20f3 oifname wpan0 accept
sudo nft add rule ip6 filter FORWARD iifname wpan0 oifname wlp0s20f3 accept
放行前后对比:
$ ping6 -c3 fda2:b6a7:8b1d:1:6d:796:3a74:2afb # 设备 IPv6(放行前)
3 packets transmitted, 0 received, 100% packet loss
$ ping6 -c3 fda2:b6a7:8b1d:1:6d:796:3a74:2afb # 放行后
3 packets transmitted, 3 received, 0% packet loss
注:这些 nftables 规则在系统重启后会丢失。如需持久化,执行
sudo nft list ruleset > /etc/nftables.conf && sudo systemctl enable nftables。
4.8 启动 Matter Server
Thread 网络就绪后,启动 Matter Server。与简化版不同,此处需要挂载 PAA 证书目录并指定 --paa-root-cert-dir,以便 Matter Server 在配网时完成设备认证:
mkdir -p ~/ha-docker/matter-server/data
# 创建 Matter Server 启动脚本(含 PAA 证书挂载和初始化等待)
cat > ~/ha-docker/start-matter.sh << 'STEOF'
#!/bin/bash
set -e
docker stop matter-server 2>/dev/null || true
docker rm -f matter-server 2>/dev/null || true
docker run -d \
--name matter-server --network host \
--volume ~/ha-docker/matter-server/data:/data \
--volume ~/ha-docker/matter-server/paa:/paa \
ghcr.io/home-assistant-libs/python-matter-server:stable \
--primary-interface __IFACE__ \
--paa-root-cert-dir /paa
# 等待 Matter Server 初始化(最多 16s)
echo "[start-matter] Waiting for Matter Server initialization..."
INIT_OK=false
for i in $(seq 1 8); do
if docker logs matter-server 2>&1 | grep -q "Matter Server successfully initialized"; then
echo "[start-matter] Matter Server initialized"
INIT_OK=true
break
fi
sleep 2
done
# GitHub 下载失败时用本地兜底证书(设备 VID=0xFFF1 的 Chip-Test-PAA-FFF1)
if ! docker exec matter-server ls /paa/git_Chip-Test-PAA-FFF1-Cert.pem 2>/dev/null; then
echo "[start-matter] GitHub 不可达,测试 PAA 证书使用本地兜底"
docker cp ~/ha-docker/matter-server/fallback-paa/git_Chip-Test-PAA-FFF1-Cert.pem matter-server:/paa/
docker cp ~/ha-docker/matter-server/fallback-paa/git_Chip-Test-PAA-NoVID-Cert.pem matter-server:/paa/
if $INIT_OK; then
echo "[start-matter] 本地证书已注入,重启 Matter Server 以加载..."
docker restart matter-server
sleep 10
else
echo "[start-matter] 本地证书已注入(Matter Server 仍在初始化中,首次启动慢属正常)"
fi
else
echo "[start-matter] 测试 PAA 证书已从 GitHub 下载"
fi
STEOF
# 将 __IFACE__ 替换为实际 Wi-Fi 接口名
sed -i "s/__IFACE__/wlp0s20f3/" ~/ha-docker/start-matter.sh
chmod +x ~/ha-docker/start-matter.sh
# 运行启动脚本
~/ha-docker/start-matter.sh
# 查看 Matter Server 运行状态
docker logs matter-server --tail 20
# [matter_server.server.helpers.paa_certificates] Fetched 0 PAA root certificates from Git.
# [matter_server.server.server] Matter Server successfully initialized.
要点说明:
--volume ~/ha-docker/matter-server/paa:/paa和--paa-root-cert-dir /paa:指定 PAA 根证书目录,配网时验证设备 DAC。- 初始化等待循环:Matter Server 首次启动需下载依赖和证书,脚本每 2 秒检查一次,最多等 16 秒(8 × 2 秒)。
- 兜底证书注入:若 GitHub 不可达导致 PAA 拉取失败(
Fetched 0 PAA root certificates),自动将第 4.3 节预置的本地测试证书注入容器。注意这里的分支逻辑——若 Matter Server 已初始化完成(INIT_OK=true),注入后需 restart 使其重新加载证书目录;若仍在初始化中,注入即可,无需 restart(首次启动本身就慢,证书会在当前启动流程中被加载)。
4.9 启动 SRP 与 SRP→Avahi 桥接
本步骤仅对 Matter over Thread 设备必要。Wi-Fi 设备的 mDNS 发现走不同路径,不依赖此桥。
设备加入 Thread 网络后,通过 SRP(Service Registration Protocol,服务注册协议)向 OTBR 注册其操作服务(_matter._tcp)。然后我们需要把这些服务发布到宿主机 Avahi,手机端才能通过 mDNS 发现:
# 启用 OTBR 的 SRP 服务器,接收设备注册
docker exec otbr ot-ctl srp server enable
为什么需要 SRP→Avahi 桥?OTBR 容器内部的 mdnsd 在 host 网络模式下会与宿主机 Avahi 争抢 5353 端口,导致 Avahi 能看见 PTR 记录却无法解析 SRV/TXT/AAAA。桥脚本的解决思路是:每 2 秒从 ot-ctl srp server service 拉取设备注册信息,提取实例名、主机名、IPv6 地址和端口,然后通过 avahi-publish 重新发布到宿主机 Avahi:
# 桥脚本核心逻辑(每 2 秒一轮):
# 1. docker exec otbr ot-ctl srp server service → 提取 实例名 / hostname / IPv6 / 端口
# 2. avahi-publish-address <hostname>.local <IPv6> &
# 3. avahi-publish -s <实例名> _matter._tcp <端口> -H <hostname>.local SII=2000 SAI=2000 SAT=4000 &
注意:
avahi-publish-address仅接受.local域名,而 Thread SRP 返回的 hostname 格式为XXXXXXXX.default.service.arpa.,桥脚本内部会做格式转换(.default.service.arpa.→.local)。
清理 chip-tool 残留缓存。若同一台宿主机此前跑过 chip-tool 配网,/tmp/chip_* 中可能残留过期的 KVS(Key-Value Store,键值存储),导致后续配网出现 Incorrect state 错误。每次重建环境时顺手清理:
rm -f /tmp/chip_*
4.10 广播 Thread 前缀(radvd)
手机端 HA App 通过 Wi-Fi 通信时,如需访问 Thread 设备,则必须知道 Thread 网段的 IPv6 前缀才能路由过去。我们用 radvd(Router Advertisement Daemon,路由通告守护进程)在 Wi-Fi 网卡上广播该前缀:
# 从 OTBR 获取 Thread On-Mesh 前缀
PREFIX=$(docker exec otbr ot-ctl netdata show | grep ' paos ' | awk '{print $1}')
# 生成 radvd 配置并重启
cat > /tmp/radvd.conf << EOF
interface wlp0s20f3 {
AdvSendAdvert on;
route $PREFIX { };
};
EOF
sudo cp /tmp/radvd.conf /etc/radvd.conf
sudo systemctl restart radvd
4.11 重启 HA 触发 zeroconf 重新发现
一个容易被忽略的时序问题:HA 的 zeroconf 组件在启动时扫描 _meshcop._udp(OTBR 发现服务),若此时 OTBR 或 meshcop 尚未就绪,HA 会缓存空结果,导致后续添加Thread集成时找不到 OpenThread Border Router 设备。
解决方案:在所有服务就绪后重启一次 HA,触发 zeroconf 重新发现:
docker restart home-assistant
sleep 8
# 轮询等待 HA 启动完成(用首页 HTTP 状态码判断,避免打 /api/ 触发认证封禁)
for i in $(seq 1 15); do
CODE=$(curl -s -o /dev/null -w '%{http_code}' --max-time 2 http://127.0.0.1:8123/)
if [ "$CODE" = "200" ] || [ "$CODE" = "302" ] || [ "$CODE" = "401" ]; then
echo "HA 已就绪 (HTTP $CODE)"
break
fi
echo " 等待 HA 启动... ($i)"
sleep 4
done
4.12 环境自检
在进入配网环节之前,运行如下自检确保所有组件均正常:
# 容器状态一览
docker ps --format "table {{.Names}}\t{{.Status}}"
# 逐项检查
docker exec otbr ot-ctl state 2>/dev/null | grep -q leader \
&& echo "✅ OTBR leader" || echo "⚠️ OTBR 非 leader"
systemctl is-active --quiet radvd && echo "✅ radvd 运行" || echo "⚠️ radvd 未运行"
systemctl is-active --quiet avahi-daemon && echo "✅ avahi 运行" || echo "⚠️ avahi 未运行"
docker ps --format '{{.Names}}' | grep -q home-assistant \
&& echo "✅ HA 运行" || echo "⚠️ HA 未检测到"
docker ps --format '{{.Names}}' | grep -q matter-server \
&& echo "✅ Matter Server 运行" || echo "⚠️ Matter Server 未检测到"
test "$(cat /proc/sys/net/ipv6/conf/all/forwarding)" = "1" \
&& echo "✅ IPv6 转发已开" || echo "⚠️ IPv6 转发未开"
pgrep -f srp-bridge > /dev/null && echo "✅ SRP 桥运行" || echo "⚠️ SRP 桥未运行"
期望输出为全部 ✅。如出现 ⚠️,根据对应组件回顾上文相应章节排查。
4.13 完整脚本
以上全部操作已整合为以下 reset-ha-wifi.sh 脚本,可一键完成环境搭建。脚本已包含各环节的错误处理和超时重试,可反复运行。
安全提醒:运行前请修改脚本头部的
PASS变量(sudo 密码)和IFACE变量(Wi-Fi 接口名,通过ip a查看自己的实际值)。
#!/bin/bash
# ============================================================
# HA + OTBR + Matter Server 完整重置脚本
# 策略:OTBR host 网络 + nftables wpan0 转发 + SRP→Avahi 桥接
# Avahi 保持运行,radvd 广播 Thread 前缀(供手机配网)
# ============================================================
set -e
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; BLUE='\033[0;34m'; NC='\033[0m'
PASS=<你的sudo密码>; IFACE=wlp0s20f3
START_DIR="$(pwd)"
ok() { echo -e "${GREEN}✅ $1${NC}"; }
warn() { echo -e "${YELLOW}⚠️ $1${NC}"; }
err() { echo -e "${RED}❌ $1${NC}"; }
info() { echo -e "${BLUE}>>> $1${NC}"; }
echo "=========================================="
echo " HA + OTBR + Matter Server 重置脚本"
echo "=========================================="
warn "此操作将删除所有容器和数据"
read -p "确认继续?(输入 y 确认): " -n 1 -r
echo; [[ $REPLY =~ ^[Yy]$ ]] || { echo "已取消。"; exit 1; }
# ============================================================
# 硬件检查
# ============================================================
info "检查 nRF52840 Dongle..."
if [ -e /dev/ttyACM0 ]; then
ok "检测到 /dev/ttyACM0"
else
err "未找到 /dev/ttyACM0,请确保 Dongle 已插入!"
exit 1
fi
# ============================================================
# 0. 内核模块 + 清理
# ============================================================
info "加载内核模块 + 清理残留网桥..."
echo $PASS | sudo -S modprobe ip6table_filter 2>/dev/null || true
echo $PASS | sudo -S ip link delete otbr0 2>/dev/null || true
echo $PASS | sudo -S systemctl restart docker; sleep 2
ok "内核模块 & Docker 就绪"
# ============================================================
# 1. 停止旧容器
# ============================================================
info "停止并删除旧容器..."
for c in home-assistant otbr matter-server; do
docker stop $c 2>/dev/null || true
docker rm -f $c 2>/dev/null || true
done
# 清理残留的 avahi-publish 进程
pkill -f srp-bridge 2>/dev/null || true
pkill -f avahi-publish 2>/dev/null || true
ok "旧容器 & 残留进程已清理"
# ============================================================
# 2. 删除旧数据 + 创建目录
# ============================================================
info "删除旧数据(保留 PAA 证书目录)..."
# 保存之前下载的 PAA 证书(DCL + GitHub 测试证书),避免每次重置都重新下载
if [ -d ~/ha-docker/matter-server/paa ]; then
cp -r ~/ha-docker/matter-server/paa /tmp/paa-backup
fi
echo $PASS | sudo -S rm -rf ~/ha-docker
mkdir -p ~/ha-docker/otbr/data ~/ha-docker/matter-server/data ~/ha-docker/config \
~/ha-docker/matter-server/paa ~/ha-docker/matter-server/fallback-paa
# 恢复 PAA 证书
if [ -d /tmp/paa-backup ]; then
cp -r /tmp/paa-backup/* ~/ha-docker/matter-server/paa/
rm -rf /tmp/paa-backup
ok "PAA 证书已恢复"
else
warn "首次运行,PAA 证书需 Matter Server 启动后从网络获取"
fi
# 始终嵌入测试 PAA 证书到 fallback 目录(设备 VID=0xFFF1 需要)
# Matter Server 会尝试从 GitHub 下载;若失败,start-matter.sh 自动注入本地兜底
base64 -d << 'B64EOF' > ~/ha-docker/matter-server/fallback-paa/git_Chip-Test-PAA-FFF1-Cert.pem
LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJ2VENDQVdTZ0F3SUJBZ0lJVHFqb01Z
TFVIQnd3Q2dZSUtvWkl6ajBFQXdJd01ERVlNQllHQTFVRUF3d1AKVFdGMGRHVnlJRlJsYzNR
Z1VFRkJNUlF3RWdZS0t3WUJCQUdDb253Q0FRd0VSa1pHTVRBZ0Z3MHlNVEEyTWpneApOREl6
TkROYUdBODVPVGs1TVRJek1USXpOVGsxT1Zvd01ERVlNQllHQTFVRUF3d1BUV0YwZEdWeUlG
UmxjM1FnClVFRkJNUlF3RWdZS0t3WUJCQUdDb253Q0FRd0VSa1pHTVRCWk1CTUdCeXFHU000
OUFnRUdDQ3FHU000OUF3RUgKQTBJQUJMYkxZM0tJZnlrbzlicklHcW5aT3VKREhLMnAxNTRr
TDJVWGZ2bk8yVEtpanMwRHVxOXFqOG9ZU2hwUQpOVUtXRFVVL01EOGZHVUlkZFI2UGp4cWFt
M1dqWmpCa01CSUdBMVVkRXdFQi93UUlNQVlCQWY4Q0FRRXdEZ1lEClZSMFBBUUgvQkFRREFn
RUdNQjBHQTFVZERnUVdCQlJxL1NKM0gxRWY3TDhXUVpkbkVOemNNYUZ4ZmpBZkJnTlYKSFNN
RUdEQVdnQlJxL1NKM0gxRWY3TDhXUVpkbkVOemNNYUZ4ZmpBS0JnZ3Foa2pPUFFRREFnTkhB
REJFQWlCUQpxb0FDOU5reXFhQUZPUFpUYUswUC84anZ1OG0rdDlwV21EWFBtcWRSRGdJZ0k3
ckkvZzhqNTFSRnRsTTVDQnBICm1Va3B4eXF2Q2hWSTFBMERUVkZMSmQ0PQotLS0tLUVORCBD
RVJUSUZJQ0FURS0tLS0tCg==
B64EOF
base64 -d << 'B64EOF' > ~/ha-docker/matter-server/fallback-paa/git_Chip-Test-PAA-NoVID-Cert.pem
LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJrVENDQVRlZ0F3SUJBZ0lIQzQrNnFO
Mkc3akFLQmdncWhrak9QUVFEQWpBYU1SZ3dGZ1lEVlFRRERBOU4KWVhSMFpYSWdWR1Z6ZENC
UVFVRXdJQmNOTWpFd05qSTRNVFF5TXpReldoZ1BPVGs1T1RFeU16RXlNelU1TlRsYQpNQm94
R0RBV0JnTlZCQU1NRDAxaGRIUmxjaUJVWlhOMElGQkJRVEJaTUJNR0J5cUdTTTQ5QWdFR0ND
cUdTTTQ5CkF3RUhBMElBQkJEdkFxZ2FoN2FCSWZ1bzB4bDQrQWVqRitVS3FLZ29SR2dva1V1
VFBlanQxS1hEbkovM0dremoKWkgvWDlpWlR0OUpKWDh1a3dQUi9oMmlBQTU0SElFcWpaakJr
TUJJR0ExVWRFd0VCL3dRSU1BWUJBZjhDQVFFdwpEZ1lEVlIwUEFRSC9CQVFEQWdFR01CMEdB
MVVkRGdRV0JCUjRYT2NGdUd1UFRtL0hrNnBneTBQcWFXaUMxVEFmCkJnTlZIU01FR0RBV2dC
UjRYT2NGdUd1UFRtL0hrNnBneTBQcWFXaUMxVEFLQmdncWhrak9QUVFEQWdOSUFEQkYKQWlF
QXVlL2JQcUJxVXV3TDhCNWgydTBzTFJWdDIyendGQkFkcTNtUHJBWDZSK1VDSUdBR0hUNDEx
ZzJkU3cxRQpqYTEyRXZmb1hGZ3VQOE1TM0JoNVRkTnpjVjVkCi0tLS0tRU5EIENFUlRJRklD
QVRFLS0tLS0K
B64EOF
ok "测试 PAA 兜底证书已就绪"
# ============================================================
# 2.5 恢复 avahi
# ============================================================
info "检查 Avahi..."
echo $PASS | sudo -S systemctl unmask avahi-daemon.socket avahi-daemon.service 2>/dev/null || true
echo $PASS | sudo -S systemctl start avahi-daemon 2>/dev/null || true
sleep 2
# 测试:前台运行 avahi-publish 2 秒,检查它是否成功 Established
# 如果 DBus 激活链路损坏,avahi-publish 会卡住/报错,不会打印 Established
TEST_OUT=$(timeout 3 avahi-publish -s __test__ _test._tcp 9999 -H test.local TXT=x 2>&1) || true
if echo "$TEST_OUT" | grep -q 'Established'; then
ok "avahi 正常"
else
warn "avahi 异常,purge + reinstall..."
echo $PASS | sudo -S apt purge -y avahi-daemon avahi-utils 2>/dev/null > /dev/null
echo $PASS | sudo -S apt install -y avahi-daemon avahi-utils 2>/dev/null > /dev/null
sleep 3
TEST_OUT=$(timeout 3 avahi-publish -s __test2__ _test._tcp 9999 -H test.local TXT=x 2>&1) || true
if echo "$TEST_OUT" | grep -q 'Established'; then
ok "avahi 修复成功"
else
warn "avahi 修复失败(输出: $TEST_OUT)"
fi
fi
# ============================================================
# 3. HA docker-compose
# ============================================================
info "创建 HA config..."
# 限制 zeroconf 只监听真实网卡,避免 198.18.0.1 等虚假接口拖慢 mDNS 发现
cat > ~/ha-docker/config/configuration.yaml << 'EOF'
# Loads default set of integrations. Do not remove.
default_config:
# Load frontend themes from the themes folder
frontend:
themes: !include_dir_merge_named themes
automation: !include automations.yaml
script: !include scripts.yaml
scene: !include scenes.yaml
# 限制 zeroconf 只监听物理网卡,避免 Docker/虚拟接口干扰 mDNS 发现
zeroconf:
default_interface: true
EOF
# HA default_config 的 automation/script/scene 集成需要这些文件存在
touch ~/ha-docker/config/automations.yaml ~/ha-docker/config/scripts.yaml ~/ha-docker/config/scenes.yaml
info "创建 HA compose..."
cat > ~/ha-docker/docker-compose.yml << 'EOF'
services:
home-assistant:
container_name: home-assistant
image: ghcr.io/home-assistant/home-assistant:stable
network_mode: host
privileged: true
volumes:
- ./config:/config
- /etc/localtime:/etc/localtime:ro
- /run/dbus:/run/dbus:ro
EOF
ok "HA compose 已创建"
# ============================================================
# 4. OTBR 启动脚本(host 网络模式,供 mDNS 到达 WiFi / iPhone)
# ============================================================
info "创建 OTBR 启动脚本..."
cat > ~/ha-docker/start-otbr.sh << EOF
#!/bin/bash
docker stop otbr 2>/dev/null || true
docker rm -f otbr 2>/dev/null || true
docker run -d \
--name otbr --privileged --network host \
--volume /dev/ttyACM0:/dev/ttyACM0 \
--volume ~/ha-docker/otbr/data:/data \
nrfconnect/otbr:fbde28a \
--radio-url "spinel+hdlc+uart:///dev/ttyACM0?uart-baudrate=1000000" \
-B $IFACE --disable-firewall
EOF
chmod +x ~/ha-docker/start-otbr.sh
ok "OTBR 启动脚本已创建"
# ============================================================
# 5. Matter Server 启动脚本
# ============================================================
info "创建 Matter Server 启动脚本..."
cat > ~/ha-docker/start-matter.sh << 'STEOF'
#!/bin/bash
set -e
docker stop matter-server 2>/dev/null || true
docker rm -f matter-server 2>/dev/null || true
docker run -d \
--name matter-server --network host \
--volume ~/ha-docker/matter-server/data:/data \
--volume ~/ha-docker/matter-server/paa:/paa \
ghcr.io/home-assistant-libs/python-matter-server:stable \
--primary-interface __IFACE__ \
--paa-root-cert-dir /paa
# 等待 Matter Server 初始化(最多 16s)
echo "[start-matter] Waiting for Matter Server initialization..."
INIT_OK=false
for i in $(seq 1 8); do
if docker logs matter-server 2>&1 | grep -q "Matter Server successfully initialized"; then
echo "[start-matter] Matter Server initialized"
INIT_OK=true
break
fi
sleep 2
done
# GitHub 下载失败时用本地兜底证书(设备 VID=0xFFF1 的 Chip-Test-PAA-FFF1)
if ! docker exec matter-server ls /paa/git_Chip-Test-PAA-FFF1-Cert.pem 2>/dev/null; then
echo "[start-matter] GitHub 不可达,测试 PAA 证书使用本地兜底"
docker cp ~/ha-docker/matter-server/fallback-paa/git_Chip-Test-PAA-FFF1-Cert.pem matter-server:/paa/
docker cp ~/ha-docker/matter-server/fallback-paa/git_Chip-Test-PAA-NoVID-Cert.pem matter-server:/paa/
if $INIT_OK; then
echo "[start-matter] 本地证书已注入,重启 Matter Server 以加载..."
docker restart matter-server
sleep 10
else
echo "[start-matter] 本地证书已注入(Matter Server 仍在初始化中,首次启动慢属正常)"
fi
else
echo "[start-matter] 测试 PAA 证书已从 GitHub 下载"
fi
STEOF
sed -i "s/__IFACE__/$IFACE/" ~/ha-docker/start-matter.sh
chmod +x ~/ha-docker/start-matter.sh
ok "Matter Server 启动脚本已创建"
# ============================================================
# 6. 启动所有服务(Avahi 必须先绑定 5353,避免 OTBR mdnsd 抢占端口)
# ============================================================
info "确认 Avahi 已绑定 5353 端口..."
for i in $(seq 1 10); do
if echo $PASS | sudo -S ss -tulnp 2>/dev/null | grep ':5353' | grep -q 'avahi'; then
ok "Avahi 已占用 5353"
break
fi
echo " 等待 Avahi 绑定端口... ($i)"
sleep 2
done
info "启动 OTBR..."
~/ha-docker/start-otbr.sh
sleep 5
info "启动 Home Assistant..."
cd ~/ha-docker
docker compose up -d
cd "$START_DIR"
sleep 5
docker ps --format '{{.Names}}' | grep -q otbr && ok "OTBR 容器可见" || warn "OTBR 容器暂未显示"
# ============================================================
# 7. 等待 OTBR 就绪 → 工厂重置 RCP → 创建 Thread 网络
# ============================================================
info "等待 OTBR 初始化..."
sleep 8
# 工厂重置 RCP,清除旧 Thread 状态(避免 SRP "operation refused for security reasons")
info "工厂重置 RCP(清除旧 Thread 状态)..."
docker exec otbr ot-ctl factoryreset 2>/dev/null || true
sleep 3
info "创建 Thread 网络..."
docker exec otbr bash -c "
ot-ctl dataset init new && ot-ctl dataset commit active && ot-ctl ifconfig up && ot-ctl thread start
" 2>/dev/null || true
ok "Thread 网络配置已提交"
info "等待 Thread 网络形成 (leader)..."
for i in $(seq 1 20); do
STATE=$(docker exec otbr ot-ctl state 2>/dev/null | tr -d '\r\n' | grep -oE '^(leader|router|child|detached|disabled)' || echo waiting)
case "$STATE" in
leader|router) ok "Thread 状态: $STATE"; break ;;
*) echo " [${i}s] 当前: $STATE"; sleep 3 ;;
esac
done
if [[ "$STATE" != "leader" && "$STATE" != "router" ]]; then
err "Thread 网络未就绪 (状态: $STATE)"
echo "手动修复:"
echo " docker exec -it otbr bash"
echo " ot-ctl factoryreset && ot-ctl dataset init new && ot-ctl dataset commit active && ot-ctl ifconfig up && ot-ctl thread start"
exit 1
fi
# 打开 wpan0 ↔ WiFi 的 IPv6 转发(Docker nftables 默认 DROP IPv6 FORWARD)
info "配置 nftables IPv6 转发 ($IFACE ↔ wpan0)..."
echo $PASS | sudo -S nft add rule ip6 filter FORWARD iifname $IFACE oifname wpan0 accept 2>/dev/null || true
echo $PASS | sudo -S nft add rule ip6 filter FORWARD iifname wpan0 oifname $IFACE accept 2>/dev/null || true
ok "nftables IPv6 转发已配置"
# ============================================================
# 7.5 启动 Matter Server(Thread 组网完成后 meshcop 已就绪)
# ============================================================
info "启动 Matter Server..."
~/ha-docker/start-matter.sh
sleep 3
docker ps --format '{{.Names}}' | grep -q matter-server && ok "Matter Server 已启动" || warn "Matter Server 启动失败"
# ============================================================
# 7.6 启用 SRP + 启动 SRP→Avahi 桥
# mdnsd 保持运行(meshcop 原生解析正常),SRP 桥补充发布 Matter 操作服务到 Avahi
# ============================================================
info "启用 SRP server..."
docker exec otbr ot-ctl srp server enable 2>/dev/null
ok "SRP server 已启用"
info "启动 SRP→Avahi 桥..."
# 直接写入桥脚本(不依赖外部文件)
cat > ~/srp-bridge.sh << 'BRIDGEOF'
#!/bin/bash
PREV_NAME=""
echo "[bridge] Started"
while true; do
RAW=$(docker exec otbr ot-ctl srp server service 2>/dev/null)
SVC_NAME=$(echo "$RAW" | grep '_matter._tcp.default.service.arpa' | grep -v 'deleted: true' | head -1 | awk '{print $1}' | sed 's/\._matter.*//')
if [ -n "$SVC_NAME" ] && [ "$SVC_NAME" != "$PREV_NAME" ]; then
SECTION=$(echo "$RAW" | grep -A20 "^${SVC_NAME}._matter" | head -20)
SRP_HOST=$(echo "$SECTION" | grep 'host:' | head -1 | awk '{print $2}' | tr -d '\r\n ')
ADDR=$(echo "$SECTION" | grep 'addresses:' | head -1 | grep -oP '\[.*?\]' | tr -d '[]\r\n ')
PORT=$(echo "$SECTION" | grep 'port:' | head -1 | awk '{print $2}' | tr -d '\r\n ')
LOCAL_HOST=$(echo "$SRP_HOST" | sed 's/\.default\.service\.arpa\./.local/')
echo "$(date): Found $SVC_NAME at [$ADDR]:$PORT host=$LOCAL_HOST"
pkill -f "avahi-publish.*${SVC_NAME}" 2>/dev/null || true
pkill -f "avahi-publish-address.*${LOCAL_HOST}" 2>/dev/null || true
if [ -n "$ADDR" ] && [ -n "$PORT" ] && [ -n "$LOCAL_HOST" ]; then
avahi-publish-address "$LOCAL_HOST" "$ADDR" &
sleep 0.5
avahi-publish -s "$SVC_NAME" _matter._tcp "$PORT" -H "$LOCAL_HOST" SII=2000 SAI=2000 SAT=4000 &
echo "$(date): Published $SVC_NAME host=$LOCAL_HOST addr=$ADDR"
else
echo "$(date): ERROR - addr=[$ADDR] port=[$PORT] host=[$LOCAL_HOST]"
fi
PREV_NAME="$SVC_NAME"
fi
sleep 2
done
BRIDGEOF
chmod +x ~/srp-bridge.sh
nohup ~/srp-bridge.sh > /tmp/srp-bridge.log 2>&1 &
sleep 2
if pgrep -f srp-bridge > /dev/null; then
ok "SRP→Avahi 桥已启动 (PID: $(pgrep -f srp-bridge | head -1))"
else
warn "SRP→Avahi 桥启动失败"
fi
# 清除 chip-tool 缓存(避免 Avahi 重连后 "Incorrect state" 错误)
info "清除 chip-tool 缓存..."
rm -f /tmp/chip_*
ok "chip-tool 缓存已清除"
# ============================================================
# 8. 获取 TLV + 更新 radvd
# ============================================================
TLV=$(docker exec otbr ot-ctl dataset active -x 2>/dev/null | tr -d '\r' || true)
echo ""
echo "=========================================="
echo " Thread TLV:"
echo "=========================================="
echo "$TLV"
echo ""
PREFIX=$(docker exec otbr ot-ctl netdata show 2>/dev/null | grep ' paos ' | awk '{print $1}' || true)
if [ -n "$PREFIX" ]; then
info "更新 radvd → $PREFIX"
cat > /tmp/radvd.conf << RADVDEOF
interface $IFACE {
AdvSendAdvert on;
route $PREFIX { };
};
RADVDEOF
echo $PASS | sudo -S cp /tmp/radvd.conf /etc/radvd.conf
echo $PASS | sudo -S systemctl restart radvd
ok "radvd 已更新并重启"
else
warn "无法获取 Thread 前缀,跳过 radvd 更新"
fi
# ============================================================
# 9. 重启 HA(此时 meshcop / Matter 服务均已就绪,zeroconf 才能发现)
# ============================================================
info "重启 Home Assistant(触发 zeroconf 重新发现 meshcop)..."
docker restart home-assistant
sleep 8
# 等待 HA 启动完成(用 HTTP 状态码,避免打 /api/ 触发 auth ban)
for i in $(seq 1 15); do
CODE=$(curl -s -o /dev/null -w '%{http_code}' --max-time 2 http://127.0.0.1:8123/ 2>/dev/null)
if [ "$CODE" = "200" ] || [ "$CODE" = "302" ] || [ "$CODE" = "401" ]; then
ok "HA 已就绪 (HTTP $CODE)"
break
fi
echo " 等待 HA 启动... ($i)"
sleep 4
done
# ============================================================
# 10. 环境自检
# ============================================================
echo ""
echo "=========================================="
echo " 环境自检"
echo "=========================================="
echo ""
# 状态快照(先行输出,便于一眼定位问题)
# mDNS 端口占用
echo ">>> mDNS 5353 端口占用:"
echo $PASS | sudo -S ss -tulnp 2>/dev/null | grep ':5353' | grep -oP 'users:\(\("[^"]+' | sort -u || echo " (无监听进程)"
echo ""
# SRP 桥日志
echo ">>> SRP 桥最新日志:"
tail -5 /tmp/srp-bridge.log 2>/dev/null || echo " (无日志)"
echo ""
# 容器状态
docker ps --format "table {{.Names}}\t{{.Status}}"
echo ""
# 逐项检查
docker exec otbr ot-ctl state 2>/dev/null | grep -q leader && ok "OTBR leader" || warn "OTBR 非 leader"
systemctl is-active --quiet radvd && ok "radvd 运行" || warn "radvd 未运行"
systemctl is-active --quiet avahi-daemon && ok "avahi 运行" || warn "avahi 未运行"
docker ps --format '{{.Names}}' | grep -q home-assistant && ok "HA 运行" || warn "HA 未检测到"
docker ps --format '{{.Names}}' | grep -q matter-server && ok "Matter Server 运行" || warn "Matter Server 未检测到"
test "$(cat /proc/sys/net/ipv6/conf/all/forwarding)" = "1" && ok "IPv6 转发已开" || warn "IPv6 转发未开"
pgrep -f srp-bridge > /dev/null && ok "SRP 桥运行" || warn "SRP 桥未运行"
# meshcop 广播检查(加 timeout 5s 防止阻塞)
if timeout 5 docker exec otbr dns-sd -B _meshcop._udp 2>/dev/null | grep -q 'BorderRouter'; then
ok "OTBR meshcop 广播正常"
else
warn "OTBR meshcop 广播未检测到(稍后重试 avahi-browse -rt _meshcop._udp)"
fi
echo ""
echo "=========================================="
echo " ✅ 重置完成"
echo "=========================================="
echo ""
echo " 浏览器访问: http://$(hostname -I | awk '{print $1}'):8123"
echo ""
echo " 添加集成:"
echo " OpenThread Border Router: http://localhost:8081"
echo " Thread: 手动添加,进入后点击齿轮图标,将网络设为 Preferred Network。"
echo " 验证一致性:终端执行 docker exec -it otbr ot-ctl dataset"
echo " 与 HA 中 Thread 集成详情(感叹号)比对 Channel、PAN ID。"
echo ""
echo " ⚠️ Matter Server 需要较长时间初始化,请等待日志出现:"
echo " Matter Server successfully initialized"
echo " 运行: docker logs matter-server --tail 30"
echo " 初始化完成后,再添加 Matter 集成: ws://localhost:5580/ws"
echo ""
echo " 手机配网:"
echo " iPhone:直接打开 HA App 扫码配网(不需要 VPN)。"
echo " ① 开 VPN → 打开 Google Play Store 确认能正常刷新"
echo " ② 关 VPN → 打开 HA App 扫码配网"
echo " ③ BLE 连接成功、配网开始后,等一会儿再开 VPN"
echo " ④ 配网界面显示"下发凭证"时,关掉 VPN"
echo ""
脚本执行完成后,终端将提示浏览器访问地址、需添加的集成以及手机配网指引。
4.14 重启后恢复脚本
跑完上述 reset-ha-wifi.sh 脚本后,设备也配网成功了,这个时候重启 Ubuntu 电脑,想再次恢复刚才的 HA fabric,可以跑如下 resume-after-reboot-ha.sh 脚本。与重置脚本不同,恢复脚本不做清理、不删数据、不重建容器、不碰 DBus,仅恢复运行态(启动容器、重建 Thread 网络、恢复 nftables 转发和 SRP 桥):
#!/bin/bash
# ============================================================
# 重启后恢复 HA + OTBR + Matter Server 环境
# 不做清理、不删数据、不重建容器、不碰 DBus
# ============================================================
set -e
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; BLUE='\033[0;34m'; NC='\033[0m'
PASS=<你的sudo密码>; IFACE=wlp0s20f3
ok() { echo -e "${GREEN}✅ $1${NC}"; }
warn() { echo -e "${YELLOW}⚠️ $1${NC}"; }
err() { echo -e "${RED}❌ $1${NC}"; }
info() { echo -e "${BLUE}>>> $1${NC}"; }
echo "=========================================="
echo " 重启后环境恢复"
echo "=========================================="
echo ""
# ---- 1. 清理 chip-tool OTBR(如果存在) ----
if docker ps -a --format '{{.Names}}' | grep -q 'otbr-chip'; then
info "清理 chip-tool OTBR..."
docker stop otbr-chip 2>/dev/null || true
docker rm -f otbr-chip 2>/dev/null || true
ok "chip-tool OTBR 已清理"
fi
# ---- 2. 启动容器 ----
info "启动容器..."
# OTBR 必须重建(docker start 会残留旧 DBus socket,导致 otbr-agent 崩溃)
if [ -x ~/ha-docker/start-otbr.sh ]; then
~/ha-docker/start-otbr.sh
ok "OTBR 已重建"
else
warn "start-otbr.sh 不存在,请先运行 reset-ha-wifi.sh"
fi
# HA 和 Matter Server 用 docker start 即可(无 DBus 残留问题)
for c in home-assistant matter-server; do
if docker ps --format '{{.Names}}' | grep -q "$c"; then
ok "$c 已在运行"
elif docker ps -a --format '{{.Names}}' | grep -q "$c"; then
docker start "$c" 2>/dev/null
ok "$c 已启动"
else
warn "$c 不存在,请先运行 reset-ha-wifi.sh"
fi
done
# ---- 3. OTBR Thread 网络恢复 ----
info "检查 Thread 网络..."
sleep 5
STATE=$(docker exec otbr ot-ctl state 2>/dev/null | tr -d '\r\n' | grep -oE '^(leader|router|child|detached|disabled)' || echo unknown)
echo " 当前状态: $STATE"
if [ "$STATE" = "unknown" ]; then
warn "ot-ctl 无法连接,重建 OTBR..."
~/ha-docker/start-otbr.sh
sleep 12
STATE=$(docker exec otbr ot-ctl state 2>/dev/null | tr -d '\r\n' | grep -oE '^(leader|router|child|detached|disabled)' || echo unknown)
echo " 重建后状态: $STATE"
fi
# 只要不是 leader/router,就从 HA 存储恢复 Thread 凭证
# 覆盖一切异常状态:disabled(RCP 空白)、detached(chip-tool 残留网络)等
if [ "$STATE" != "leader" ] && [ "$STATE" != "router" ]; then
TLV=$(python3 -c "
import json
with open('$HOME/ha-docker/config/.storage/thread.datasets') as f:
data = json.load(f)
for ds in data['data']['datasets']:
if ds.get('source') == 'otbr':
print(ds['tlv'])
break
" 2>/dev/null)
if [ -n "$TLV" ]; then
info "恢复 Thread 网络凭证..."
docker exec otbr ot-ctl dataset set active "$TLV" 2>/dev/null
docker exec otbr bash -c "ot-ctl ifconfig up && ot-ctl thread start" 2>/dev/null
sleep 10
STATE=$(docker exec otbr ot-ctl state 2>/dev/null | tr -d '\r\n' | grep -oE '^(leader|router|child|detached|disabled)' || echo unknown)
ok "Thread 网络已恢复: $STATE"
else
warn "HA 中无 Thread 凭证,需运行 reset-ha-wifi.sh 重建"
fi
else
ok "Thread: $STATE"
fi
# ---- 4. nftables IPv6 转发 ----
info "恢复 nftables IPv6 转发..."
echo $PASS | sudo -S modprobe ip6table_filter 2>/dev/null || true
echo $PASS | sudo -S nft add rule ip6 filter FORWARD iifname $IFACE oifname wpan0 accept 2>/dev/null || true
echo $PASS | sudo -S nft add rule ip6 filter FORWARD iifname wpan0 oifname $IFACE accept 2>/dev/null || true
echo $PASS | sudo -S sysctl -w net.ipv6.conf.all.forwarding=1 > /dev/null 2>&1
ok "nftables + IPv6 转发"
# ---- 5. SRP 桥 ----
info "启动 SRP 桥..."
pkill -f srp-bridge 2>/dev/null || true
sleep 1
nohup ~/srp-bridge.sh > /tmp/srp-bridge.log 2>&1 &
sleep 3
pgrep -f srp-bridge > /dev/null && ok "SRP 桥已启动" || warn "SRP 桥未检测到"
# ---- 6. 重启 HA(stop + start,不用 restart 避免 cgroup 问题)----
info "重启 HA 触发 zeroconf 重新发现..."
docker stop home-assistant 2>/dev/null
sleep 3
docker start home-assistant 2>/dev/null
sleep 8
for i in $(seq 1 15); do
CODE=$(curl -s -o /dev/null -w '%{http_code}' --max-time 2 http://127.0.0.1:8123/ 2>/dev/null)
if [ "$CODE" = "200" ] || [ "$CODE" = "302" ] || [ "$CODE" = "401" ]; then
ok "HA 已就绪 (HTTP $CODE)"
break
fi
echo " 等待 HA 启动... ($i)"
sleep 4
done
# ---- 7. 等待 Thread 网络和设备恢复 ----
info "等待 Thread 网络稳定..."
for i in $(seq 1 15); do
STATE=$(docker exec otbr ot-ctl state 2>/dev/null | tr -d '\r\n' | grep -oE '^(leader|router)' || true)
[ -n "$STATE" ] && break
echo " 等待 OTBR 成为 leader/router... ($i)"
sleep 3
done
if [ -n "$STATE" ]; then
ok "OTBR: $STATE,等待设备重连..."
for i in $(seq 1 15); do
if docker exec otbr ot-ctl neighbor table 2>/dev/null | grep -q '0x'; then
ok "Thread 设备已重连"
break
fi
sleep 2
done
fi
# ---- 8. 清除 chip-tool 缓存 ----
rm -f /tmp/chip_*
# ---- 9. 环境自检 ----
echo ""
echo "=========================================="
echo " 环境自检"
echo "=========================================="
echo ""
docker ps --format "table {{.Names}}\t{{.Status}}"
echo ""
docker exec otbr ot-ctl state 2>/dev/null | grep -qE 'leader|router' && ok "OTBR leader/router" || warn "OTBR 非 leader/router,等待中..."
docker exec otbr ot-ctl neighbor table 2>/dev/null | grep -q '0x' && ok "Thread 设备在线" || warn "设备尚未重连,稍后会自动加入"
docker exec otbr ot-ctl srp server service 2>/dev/null | grep -q '_matter.*deleted: false' && ok "Matter SRP 服务已注册" || warn "SRP 服务尚未注册,设备重连后自动出现"
systemctl is-active --quiet avahi-daemon && ok "avahi 运行" || warn "avahi 未运行"
docker ps --format '{{.Names}}' | grep -q home-assistant && ok "HA 运行" || warn "HA 未检测到"
docker ps --format '{{.Names}}' | grep -q matter-server && ok "Matter Server 运行" || warn "Matter Server 未检测到"
pgrep -f srp-bridge > /dev/null && ok "SRP 桥运行" || warn "SRP 桥未运行"
echo ""
echo "=========================================="
echo " ✅ 环境恢复完成"
echo "=========================================="
echo ""
echo " 浏览器访问: http://$(hostname -I | awk '{print $1}'):8123"
echo " 已配网设备会自动重新连接 Thread 网络"
echo ""
关键差异:恢复脚本中 HA 的重启用的是
docker stop+docker start(而非docker restart),这是为了避免 cgroup 残留导致的问题。OTBR 则必须通过start-otbr.sh重建(docker start会残留旧 DBus socket,导致 otbr-agent 崩溃)。
5. 设备端编译与烧录(light_bulb 例程)
本章以 NCS 的 light_bulb(智能灯泡)标准例程为例,仿照 Nordic 官方 light_bulb README 的结构,按"前置条件 → 编译 → 烧录 → 测试"四步展开。
5.1 前置条件
- 已安装 NCS v3.4.0 工具链(本文在 Windows 端编译烧录)。
- 一块支持的开发板:
- Matter over Thread:
nrf54l15dk/nrf54l15/cpuapp(亦支持 nrf52840dk、nrf5340dk) - Matter over Wi-Fi:
nrf7002dk/nrf5340/cpuapp
- Matter over Thread:
- 一个可用的 Matter 控制器(即第 4 章的 HA + Matter Server;或者第 7 章的 chip-tool)。
- Thread 设备还需要一个已组网的 Thread 边界路由器(即第 4 章搭建的 OTBR)。
5.2 编译
进入例程目录,按设备类型选择开发板编译:
# Matter over Thread(nRF54L15DK)
cd ncs/v3.4.0/nrf/samples/matter/light_bulb
west build -b nrf54l15dk/nrf54l15/cpuapp
# Matter over Wi-Fi(nRF7002DK/nRF5340)
west build -b nrf7002dk/nrf5340/cpuapp
5.3 烧录
编译完成后烧录到开发板:
west flash --recover
--recover 选项会全片擦除,包括之前残留的配网状态(fabric 信息、网络凭证等)。配网失败时常见的 ThreadNetworkSetup: Internal error,根因就是旧配网数据未清除。换例程或重新测试前务必 --recover。
5.4 测试与验证
烧录完成后,按以下步骤验证设备功能:
-
连接串口终端(波特率 115200),观察启动日志。设备启动后会在 UART 打印配网入口(onboarding)信息,包含 Setup PIN Code、Discriminator(识别码)、QR Code 和手动配对码,如下所示:
I: 256 [DL] Serial Number: 11223344556677889900 I: 260 [DL] Vendor Id: 65521 (0xFFF1) I: 263 [DL] Product Id: 32773 (0x8005) I: 267 [DL] Product Name: Matter Light Bulb I: 271 [DL] Hardware Version: 0 I: 274 [DL] Setup Pin Code (0 for UNKNOWN/ERROR): 20202021 I: 279 [DL] Setup Discriminator (0xFFFF for UNKNOWN/ERROR): 3840 (0xF00) I: 286 [DL] Manufacturing Date: 2022-01-01 I: 290 [DL] Device Type: 65535 (0xFFFF) I: 293 [SVR] SetupQRCode: [MT:6FCJ142C00KA0648G00] I: 298 [SVR] Copy/paste the below URL in a browser to see the QR Code: I: 304 [SVR] https://project-chip.github.io/connectedhomeip/qrcode.html?data=MT%3A6FCJ142C00KA0648G00 I: 313 [SVR] Manual pairing code: [34970112332] E: 317 [DL] Long dispatch time: 266 ms, for event type 2 I: 323 [DL] CHIPoBLE advertising started I: 327 [DL] NFC Tag emulation started I: 30327 [DL] CHIPoBLE advertising mode changed to slow I: 30333 [DL] CHIPoBLE advertising started
-
观察状态 LED,判断设备所处阶段:
LED 状态 含义 短闪(亮 50 ms / 灭 950 ms) 未配网,等待配网器(commissioner) 快速对称闪(100 ms / 100 ms) 未配网,低功耗蓝牙已连接 常亮 已配网,网络正常 短灭(亮 950 ms / 灭 50 ms) 已配网,但暂无网络连通 -
确认BLE广播已开启。 设备默认只在上电后一小段时间内广播低功耗蓝牙。若超时后广播停止,可通过短按对应按键重新开启广播(BLE advertising);长按同一按键则触发出厂重置(factory reset)。
-
验证端到端控制。 配网成功后,在控制器中开关这盏"灯",观察开发板上的 LED 随之点亮/熄灭,说明从手机到设备的全链路已打通。
6. 用手机 HA App 测试 Matter 设备
Ubuntu 环境就绪、设备烧录完成,现在进入最终的用户验证环节——用手机 HA App 配网并控制 Matter 设备。过程分两步:先在 HA 中添加所需集成(一次性),再用手机扫码配网(每个设备一次)。
6.1 添加 HA 集成
-
浏览器打开
http://192.168.50.162:8123(替换为宿主机实际 IP),创建 HA 账号。位置页面可跳过,国家选择中国。创建用户界面如下:
-
登录后,依次点击 设置 → 设备与服务 → 添加集成,相关界面如下:


然后依次搜索并添加以下三个集成:
① OpenThread Border Router。搜索
OpenThread Border Router,在弹出界面中输入http://localhost:8081。此操作请在 Ubuntu 电脑的浏览器上进行(localhost 指向宿主机自身),如下所示:

② Thread。搜索
Thread集成并添加 (Thread 集成可能被自动添加)。添加后点击 Thread 集成卡片,进入齿轮图标设置,将自动发现的网络设为 Preferred Network(首选网络):
若自动发现失败("找不到board router"),执行
docker restart home-assistant触发 zeroconf 重新扫描(见第 4.11 节)。设为首选网络后,应验证集成中的网络参数与 OTBR 容器内实际参数一致:
- 点击 Thread 集成旁的感叹号图标,查看网络详情:

- 在终端执行
docker exec -it otbr ot-ctl dataset,查看 OTBR 侧网络参数:

- 确认二者的 Channel 和 PAN ID 一致。
③ Matter。搜索
Matter集成,在弹出界面中输入ws://localhost:5580/ws:
三个集成全部添加完毕、均显示正常状态后,进入下一步手机配网。
- 点击 Thread 集成旁的感叹号图标,查看网络详情:
6.2 手机 HA App 配网和控制
根据设备类型的不同,配网流程在内部有两个分支。下面先展示 Matter over Thread 的完整时序,再说明 Matter over Wi-Fi 的差异。
Matter over Thread 配网时序:
手机 BLE ──PASE──> 设备 (安全握手)
手机 BLE ──> 设备 (下发 Thread 凭证)
设备 ──> OTBR (加入 Thread 网络)
设备 ──> OTBR SRP 服务器 (设备向 OTBR 注册操作服务 _matter._tcp)
SRP 桥 ──> Avahi (发布到宿主机 mDNS)
Matter Server ──> 设备 (CASE 会话 + CommissioningComplete)
Matter over Wi-Fi 配网时序:
手机 BLE ──PASE──> 设备 (安全握手)
手机 BLE ──> 设备 (下发 Wi-Fi 密码)
设备 ──> Wi-Fi 路由器 (设备直接连接 Wi-Fi)
Matter Server ──> 设备 (通过局域网 IP 直连,CASE 会话 + CommissioningComplete)
关键区别:Wi-Fi 设备不经过 OTBR、SRP 和 SRP→Avahi 桥;它通过局域网 IP 直接与 Matter Server 通信,因此配网流程更简单,也不受 VPN 对 mDNS 劫持的影响(见下文 Android 特殊说明)。
手机 HA App 操作流程(以 Android 手机截图为主):
-
打开 HA App,点击右上角 + 图标:

-
选择"添加设备",然后选择"Matter 设备":

-
选择"It's New"(这是一台新设备):

-
用手机扫描第 5.4 节的二维码,或手动输入配对码(安卓手机扫描二维码有时不会自动触发配网流程):


-
等待配网完成。期间可重点关注以下关键节点:
- 下发凭证:

-
设备端 Thread 网络信息确认(应与第 6.1 节的 Thread 集成信息一致):

-
连接 Thread 网络:

-
关联到 HA fabric,并连接成功


-
配网成功后,将设备分配到一个房间:

-
配网完成,现在可以通过 HA App 开/关这盏灯了:

佐证日志——以下日志可以确认配网确实在协议层面成功完成:
-
设备端 UART日志:

-
Matter Server 端日志(宿主机终端):
INFO [matter_server.server.device_controller] Matter commissioning of Node ID 2 successful. INFO [matter_server.server.device_controller] Commissioning of Node ID 2 completed.
Android 配网特殊说明:Android 手机配网有两个额外要求——打开 VPN(激活 Google Play 服务)和授予 HA App 后台运行权限(防止配网中途被系统杀死)。
但 VPN 会劫持 mDNS 组播,导致"操作服务发现"阶段失败。经过长时间测试,下面的 workaround 可以规避:
- 先开 VPN。
- 打开 Google Play Store,确认能正常刷新(说明 Google 服务已激活)。
- 关闭 VPN。
- 打开 HA App,扫码配网。
- BLE 连接成功后,过一小会儿,再打开 VPN。
- 保持 VPN 开着,直到显示"开始下发 Matter 凭证"。

- 这时再关闭 VPN,等待配网完成。
请确认 HA App 获得如下所需权限(缺一不可):
- 允许 HA App 分享位置:配网通过低功耗蓝牙扫描设备,Android 将此归为位置权限,必须允许。
- 选择"最安全"选项:HA App 需要长期在后台运行以维持与 Matter 设备的连接,选择“最安全”可避免被系统限制。

- 不限制后台运行:防止配网过程中 App 被 Android 的后台策略杀死,导致配网中断。
- 始终允许访问位置:即使 App 在后台也需要获取位置信息来与 Matter 设备交互。

7. 用 chip-tool 测试 Matter 设备(与 HA 并行的另一套体系)
首先强调一个关键概念:chip-tool 和 Home Assistant 是两套完全独立、互相平行的 Matter 控制器体系,不能混为一谈。chip-tool 作为 Matter SDK 自带的官方控制器客户端,它支持的功能最全,唯一不足的是不方便给客户演示,但非常适合研发人员测试。前面第 4 章搭的是 HA 测试环境的基座(Ubuntu + Docker + OTBR + RCP),前面第 6 章讲的是 HA 体系的完整配网流程,跟 chip-tool 都无关!本章讲 chip-tool 体系——它是一个独立体系,有自己的控制器、配网流程和 Fabric。
7.1 两套体系的关系
基于Matter over Thread设备测试,两套体系的关系如下。尤其要注意的是,虽然两者都需要调用OTBR[OTBR + nRF52840 RCP],如前所述,他们的配置是不同的,是两个完全不同的OTBR容器。
| 对比项 | chip-tool | Home Assistant |
|---|---|---|
| 形态 | 命令行工具(Matter SDK 自带) | 智能家居平台 + GUI |
| 控制器 | chip-tool 自身 | Matter Server(python-matter-server) |
| Fabric | 独立 Fabric(自有证书 / KVS) | 独立 Fabric |
| 状态存储 | /tmp/chip_* |
Matter Server 的 /data/ |
| 用途 | 快速冒烟、协议一致性测试 | 完整配网 / 控制 / 回归 / Demo |
| 需要手机吗 | 不需要 | 需要 |
7.2 Fabric 隔离警告
一个 Matter 设备被 chip-tool 配网后,就加入了 chip-tool 的 Fabric;此时 HA 看不到也控制不了它,反之亦然。这是因为 Matter 的 Fabric 是隔离的信任域——两个独立的控制器拥有各自的根证书和操作凭证,设备不会跨 Fabric 共享状态。
在 chip-tool 和 HA 之间切换测试时,务必重置设备配网状态(west flash --recover 或长按设备上的 factory reset 按键)。
7.3 OTBR 配置差异:host 模式 vs bridge 模式
chip-tool 和 HA 对 OTBR 的网络模式要求不同,这源于它们使用场景的差异:
| OTBR 配置 | HA 体系 | chip-tool 体系 |
|---|---|---|
| Docker 网络模式 | --network host |
--network otbr(自定义 bridge) |
| mDNS 可达范围 | Wi-Fi 全段(手机需要) | 仅宿主机本机 |
| 端口映射 | 无需(共享 host) | -p 8080:80(Web 管理) |
| 访问路径 | 手机 Wi-Fi → 宿主机 Avahi → OTBR | 宿主机 → Docker bridge → OTBR |
核心原因:HA 需要手机通过 Wi-Fi 发现 OTBR 的 _meshcop._udp 和设备的 _matter._tcp 服务,因此 mDNS 必须能广播到 Wi-Fi 网段,只能使用 host 网络模式。而 chip-tool 在宿主机本机运行,只需要通过 Docker bridge 的 IPv6 路由就能直达 Thread 设备,mDNS 不出本机,bridge 模式反而更干净——不会与宿主机 Avahi 发生端口冲突。
7.4 chip-tool 完整搭建脚本
chip-tool 环境的完整搭建脚本如下(reset-chip-tool.sh)。脚本会自动停掉 HA 的 OTBR 容器、重置 RCP 并创建 Docker bridge 模式的独立 OTBR 容器——注意这会中断 HA 的 Thread 网络,测试完成后需运行 4.14 节的 resume-after-reboot-ha.sh 恢复:
#!/bin/bash
# ============================================================
# chip-tool 测试环境搭建
# 策略:Docker bridge 模式 OTBR,与 HA 环境的 host 模式 OTBR 互斥
# RCP 会被 factoryreset,HA Thread 网络会中断
# 测试完成后运行 resume-after-reboot-ha.sh 恢复 HA
# ============================================================
set -e
GREEN='\033[0;32m'; YELLOW='\033[1;33m'; RED='\033[0;31m'; BLUE='\033[0;34m'; NC='\033[0m'
PASS=<你的sudo密码>
OTBR_IMAGE="nrfconnect/otbr:fbde28a"
OTBR_NET="otbr"
OTBR_SUBNET="fd11:db8:1::/64"
HA_OTBR="otbr"
CHIP_OTBR="otbr-chip"
ok() { echo -e "${GREEN}✅ $1${NC}"; }
warn() { echo -e "${YELLOW}⚠️ $1${NC}"; }
err() { echo -e "${RED}❌ $1${NC}"; }
info() { echo -e "${BLUE}>>> $1${NC}"; }
echo "=========================================="
echo " chip-tool 测试环境"
echo "=========================================="
warn "此操作将停掉 HA 的 OTBR 并重置 RCP,HA Thread 网络会中断"
warn "测试完成后运行 resume-after-reboot-ha.sh 恢复 HA"
read -p "确认继续?(输入 y 确认): " -n 1 -r
echo; [[ $REPLY =~ ^[Yy]$ ]] || { echo "已取消。"; exit 1; }
# ---- 1. 硬件检查 ----
info "检查 nRF52840 Dongle..."
if [ -e /dev/ttyACM0 ]; then
ok "检测到 /dev/ttyACM0"
else
err "未找到 /dev/ttyACM0"
exit 1
fi
# ---- 2. 停掉 HA 的 OTBR(保留容器,不删除)----
info "暂停 HA OTBR..."
docker stop "$HA_OTBR" 2>/dev/null && ok "$HA_OTBR 已暂停" || ok "$HA_OTBR 未在运行"
# ---- 2.5 重置 BLE 适配器(HA OTBR --privileged 可能残留 BlueZ 状态)----
bluetoothctl power off > /dev/null 2>&1 || true
sleep 1
bluetoothctl power on > /dev/null 2>&1 || true
ok "BLE 适配器已重置"
# ---- 3. 清理旧 chip-tool OTBR ----
docker stop "$CHIP_OTBR" 2>/dev/null || true
docker rm -f "$CHIP_OTBR" 2>/dev/null || true
# ---- 4. 内核模块 + Docker 网络 ----
echo $PASS | sudo -S modprobe ip6table_filter 2>/dev/null || true
docker network rm "$OTBR_NET" 2>/dev/null || true
docker network create --ipv6 --subnet "$OTBR_SUBNET" \
-o com.docker.network.bridge.name=otbr0 "$OTBR_NET"
ok "Docker IPv6 网络已创建"
# ---- 5. 启动 OTBR ----
if docker image inspect "$OTBR_IMAGE" > /dev/null 2>&1; then
ok "镜像已本地存在"
else
info "拉取镜像 $OTBR_IMAGE..."
docker pull "$OTBR_IMAGE"
fi
info "启动 chip-tool OTBR..."
docker run -d \
--name "$CHIP_OTBR" \
--privileged \
--network "$OTBR_NET" \
-p 8080:80 \
--sysctl "net.ipv6.conf.all.disable_ipv6=0 net.ipv4.conf.all.forwarding=1 net.ipv6.conf.all.forwarding=1" \
--volume /dev/ttyACM0:/dev/radio \
"$OTBR_IMAGE" \
--radio-url "spinel+hdlc+uart:///dev/radio?uart-baudrate=1000000"
ok "OTBR 容器已启动 ($CHIP_OTBR)"
# ---- 6. 等待 OTBR agent ----
info "等待 OTBR agent..."
sleep 5
for i in $(seq 1 30); do
if docker exec "$CHIP_OTBR" ot-ctl state 2>/dev/null | grep -qE 'disabled|detached'; then
ok "otbr-agent 已就绪"
break
fi
echo " 等待... ($i)"
sleep 3
done
# ---- 7. 工厂重置 + 创建 Thread 网络 ----
info "工厂重置 RCP..."
docker exec "$CHIP_OTBR" ot-ctl factoryreset 2>/dev/null || true
sleep 3
info "创建 Thread 网络..."
docker exec "$CHIP_OTBR" bash -c "
ot-ctl dataset init new && \
ot-ctl dataset commit active && \
ot-ctl ifconfig up && \
ot-ctl thread start
" 2>/dev/null || true
info "等待 Thread 网络 (leader)..."
STATE=""
for i in $(seq 1 20); do
STATE=$(docker exec "$CHIP_OTBR" ot-ctl state 2>/dev/null | tr -d '\r\n' | grep -oE '^(leader|router|child|detached|disabled)' || echo waiting)
case "$STATE" in
leader|router) ok "Thread: $STATE"; break ;;
*) echo " [${i}s] 当前: $STATE"; sleep 3 ;;
esac
done
[[ "$STATE" == "leader" || "$STATE" == "router" ]] || { err "Thread 网络未形成"; exit 1; }
# ---- 8. IPv6 路由 ----
PREFIX=$(docker exec "$CHIP_OTBR" ot-ctl prefix 2>/dev/null | grep 'paos' | awk '{print $1}' | tr -d '\r' | head -n 1)
if [ -z "$PREFIX" ]; then
PREFIX="fd11:22::/64"
docker exec "$CHIP_OTBR" ot-ctl prefix add "$PREFIX" paos
docker exec "$CHIP_OTBR" ot-ctl netdata register
sleep 2
fi
GATEWAY="fd11:db8:1::2"
echo $PASS | sudo -S ip -6 route del "$PREFIX" 2>/dev/null || true
echo $PASS | sudo -S ip -6 route add "$PREFIX" dev otbr0 via "$GATEWAY"
ok "IPv6 路由已添加"
# ---- 9. SRP + 缓存清理 ----
docker exec "$CHIP_OTBR" ot-ctl srp server enable 2>/dev/null || true
rm -f /tmp/chip_*
# ---- 10. 输出配网命令 ----
TLV=$(docker exec "$CHIP_OTBR" ot-ctl dataset active -x 2>/dev/null | \
sed -e 's/Done//g' -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' | head -n 1)
echo ""
echo "=========================================="
echo " ✅ chip-tool 环境就绪"
echo "=========================================="
echo ""
echo " Thread TLV: $TLV"
echo ""
echo " 配网: chip-tool_x64 pairing ble-thread 1 hex:$TLV 20202021 3840"
echo " 控制: chip-tool_x64 onoff on 1 1"
echo " chip-tool_x64 onoff off 1 1"
echo ""
下面展示其核心配网流程以说明与 HA 的差异:
第一步,从 OTBR 获取 Thread 数据集 TLV(这是一串编码了 Channel、PAN ID、Network Key 等参数的十六进制串):
docker exec otbr ot-ctl dataset active -x
第二步,用 chip-tool 通过低功耗蓝牙配网设备,将 TLV 下发给设备(chip-tool 直接与设备低功耗蓝牙通信,不经手机):
# 语法:chip-tool pairing ble-thread <node_id> hex:<TLV> <pin> <discriminator>
chip-tool_x64 pairing ble-thread 1 hex:<上一步的TLV> 20202021 3840
第三步,验证设备受控:
chip-tool_x64 onoff toggle 1 1 # 对 node 1, endpoint 1 执行开关翻转
再次总结一下 chip-tool 和 HA 在配网路径上的根本区别:
- chip-tool:宿主机 chip-tool → 低功耗蓝牙 → 设备(直连),配网完成后 chip-tool ↔ OTBR bridge ↔ 设备 IPv6 直通,不涉及手机、不涉及 Avahi、不涉及 mDNS 广播。
- HA:手机 HA App → 低功耗蓝牙 → 设备(手机是配网代理),配网完成后手机 → Wi-Fi → Avahi → Matter Server → OTBR → 设备,链路长得多,mDNS 广播是必经之路。第 2 章的架构图反映的正是这条链路。
这也是为什么 chip-tool 配网通常比 HA 顺畅——少了很多中间环节。
更详细的 chip-tool 环境搭建可参考 Nordic 官方文档:Testing with chip-tool。
8. 诊断与排查
Matter 配网涉及多层组件,任何一层出问题都表现为笼统的"配网失败"。本章提供分层自检和坑点速查表,出错时按层排查,一层通过再查下一层。
8.1 分层自检
# L1: Thread 状态
docker exec otbr ot-ctl state # 期望: leader/router
# L2: 设备在线
docker exec otbr ot-ctl neighbor table # 期望: 有 R/C 条目
# L3: SRP 注册
docker exec otbr ot-ctl srp server service | grep '_matter.*deleted: false'
# L4: mDNS 解析
avahi-browse -r _matter._tcp -t 2>&1 | grep -c 'Failed' # 期望: 0
avahi-browse -r _meshcop._udp -t 2>&1 | grep -c 'Failed' # 期望: 0
# L5: IPv6 转发
sudo nft list chain ip6 filter FORWARD | head -3 # 期望: wpan0 accept
ping6 -c2 <设备IPv6> # 期望: 通
8.2 常见坑点速查表
| 症状 | 根因 | 修复 |
|---|---|---|
Thread 网络 detached |
RCP 有旧网络状态 | factoryreset + 重建网络 |
Thread 网络 disabled 且重启后 |
RCP 丢凭证 | 从 HA thread.datasets 恢复 TLV:ot-ctl dataset set active <tlv> + ifconfig up + thread start |
Thread 网络 ot-ctl 连不上 |
otbr-agent 崩溃 | 查 docker logs otbr,常见 DBus 错误 |
SRP operation refused for security reasons |
RCP 旧状态 | factoryreset |
avahi-publish 输出 Established 但 avahi-browse 看不到 |
Avahi DBus 激活链路损坏 | apt purge -y avahi-daemon avahi-utils && apt install -y avahi-daemon avahi-utils |
测试 Avahi:timeout 3 avahi-publish -s __test__ _test._tcp 9999 -H test.local TXT=x 2>&1 | grep -q Established |
— | 匹配到 = 正常,否则需 purge |
HA 内 zeroconf 错误 Error with socket (198.18.0.1) |
zeroconf 绑定到 Docker 虚拟接口 | configuration.yaml 加 zeroconf: default_interface: true |
| HA Thread 集成点进去看不到 Border Router | HA 启动时 meshcop 未就绪,且 zeroconf ServiceBrowser 没发初始查询 | HA 必须在 meshcop 就绪后 restart |
| Avahi 5353 被 mdnsd 抢占 | OTBR 启动顺序不对 | 先确认 Avahi 绑定 5353(ss -tulnp | grep ':5353' | grep avahi),再启动 OTBR |
| ping6 不通设备 | Docker nftables ip6 filter FORWARD policy drop |
nft add rule ip6 filter FORWARD iifname <Wi-Fi 接口> oifname wpan0 accept(双向) |
Failed Device Attestation: PAA not found |
Matter Server 从 GitHub 下载测试证书失败(国内网络) | 开 VPN 启动 Matter Server,或见 PAA 兜底方案 |
ThreadNetworkSetup: Internal error |
设备残留配网状态 | west flash --recover |
Incorrect state (Discovery_ImplPlatform.cpp) |
chip-tool 缓存过期 | rm -f /tmp/chip_* |
BLE Connect 报 UnknownObject |
BlueZ 蓝牙守护进程状态被 --privileged 容器残留污染 |
bluetoothctl power off && bluetoothctl power on |
| iPhone 配网始终失败 | iOS 不接受非网关 IPv6 RA | radvd 在 WiFi 接口广播 Thread 前缀 |
设备 UART 有 SRP update succeeded 但 OTBR 看不到 |
mDNS广播找不到 | 搭建SRP和Avahi桥 |
Operational advertising failed: 3 |
设备刚入网,SRP client 尚未完成初始化 | 等待 5-10 秒,设备会自动重试并恢复,无需干预 |
9. 小结
本文从"为什么选择 HA 测试 Matter"出发,手把手搭出了两套完整的测试环境:
- HA 测试环境,一键脚本收敛复杂度——
reset-ha-wifi.sh将 PAA 证书管理、Avahi 恢复、HA + OTBR + Matter Server 三个容器的创建与启动、Thread 组网、nftables IPv6 放行、SRP→Avahi 桥接、radvd 前缀广播、HA 重启触发 zeroconf、环境自检等十余个环节整合为一条可复现的流水线(4.13 节),出问题重跑即可。配网完成后还可通过resume-after-reboot-ha.sh(第 4.14 节)在重启后快速恢复环境,无需重新搭建。 - chip-tool 测试环境,一键脚本生成——
reset-chip-tool.sh自动清理 HA OTBR、重置低功耗蓝牙适配器、创建 Docker bridge 网络、启动独立 OTBR 容器、组建新 Thread 网络、添加 IPv6 路由并输出配网命令,一步到位(第 7.4 节)。
参考资料:
- Nordic《Thread tools — Thread Border Router / 配置射频协处理器》:https://nrfconnectdocs.nordicsemi.com/ncs/latest/nrf/protocols/thread/tools.html#ug-thread-tools-tbr
- Nordic《Matter light_bulb 例程 README》:https://nrfconnectdocs.nordicsemi.com/ncs/latest/nrf/samples/matter/light_bulb/README.html
- Nordic《Testing with chip-tool》:https://nrfconnectdocs.nordicsemi.com/ncs/latest/nrf/samples/matter/light_bulb/README.html#testing-with-chip-tool
- AI对本文做了充分的提高和润色
浙公网安备 33010602011771号