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 的总览界面:

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 设备的数据路径不同:

graph TB Phone[手机 HA App] -->|Wi-Fi| HA[Home Assistant<br/>Docker: host 网络] HA -->|WebSocket| MS[Matter Server<br/>Docker: host 网络] MS -->|IPv6| OTBR[OTBR Docker<br/>network: host] OTBR -->|UART| RCP[nRF52840 Dongle<br/>Radio Co-Processor] RCP -->|Thread 15.4| ThreadDev[nRF54L15<br/>Matter over Thread] MS -->|mDNS 发现<br/>Wi-Fi 设备| WiFiDev[nRF7002<br/>Matter over Wi-Fi] ThreadDev -->|SRP 注册| OTBR OTBR -->|SRP→Avahi 桥| Avahi[Avahi<br/>宿主机 mDNS] Avahi -->|mDNS 广播| Phone WiFiDev -->|mDNS 广播| Phone

要点:

  • 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

不想自己编译的,可以直接下载这个hex版本烧录:RCP固件hex版本

注:本文所用版本的 UART 波特率为 1 Mbps。

第二步:烧录固件到 dongle。dongle 必须先进入 DFU(Device Firmware Update,设备固件升级)模式——按住Dongle上的 RESET 键并插入电脑的USB口,红色 LED 开始呼吸闪烁即表示已进入。选择下列两种方式之一烧录:

  • nRF Connect Programmer(图形界面):将 build/coprocessor/zephyr/zephyr.hex 或者上面解压缩后的hex文件 拖入 Programmer 烧写,如下所示:

    nRF Connect Programmer 烧录 RCP 固件

  • 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 测试环境搭建

整个环境的搭建遵循如下顺序,每一步的输出是下一步的前提:

graph TD A[硬件检查] --> B[清理残留] B --> C[恢复 Avahi / 创建目录 / PAA 证书] C --> D[启动 OTBR] C --> E[启动 HA] D --> F[RCP 出厂重置] E --> F F --> G[创建 Thread 网络] G --> H[nftables IPv6 转发] H --> I[启动 Matter Server] I --> J[SRP → Avahi 桥] J --> K[radvd 广播前缀] K --> L[重启 HA 触发 zeroconf] L --> M[环境自检]

以下各节逐一展开。为方便重复搭建,全部操作已整合为一键脚本 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。我们采取两道防线:

  1. 备份/恢复机制:重置脚本在删除旧数据前会备份 ~/ha-docker/matter-server/paa/ 到 /tmp/paa-backup/,建好新目录后恢复。避免每次重置都重新下载。
  2. 本地兜底证书:将 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 控制器(即第 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 测试与验证

烧录完成后,按以下步骤验证设备功能:

  1. 连接串口终端(波特率 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
    

    设备 UART 打印的配网入口信息

  2. 观察状态 LED,判断设备所处阶段:

    LED 状态 含义
    短闪(亮 50 ms / 灭 950 ms) 未配网,等待配网器(commissioner)
    快速对称闪(100 ms / 100 ms) 未配网,低功耗蓝牙已连接
    常亮 已配网,网络正常
    短灭(亮 950 ms / 灭 50 ms) 已配网,但暂无网络连通
  3. 确认BLE广播已开启。 设备默认只在上电后一小段时间内广播低功耗蓝牙。若超时后广播停止,可通过短按对应按键重新开启广播(BLE advertising);长按同一按键则触发出厂重置(factory reset)。

  4. 用控制器配网。 使用 HA App(第 6 章)或 chip-tool(第 7 章)扫描第 1 步的二维码完成配网。

  5. 验证端到端控制。 配网成功后,在控制器中开关这盏"灯",观察开发板上的 LED 随之点亮/熄灭,说明从手机到设备的全链路已打通。

6. 用手机 HA App 测试 Matter 设备

Ubuntu 环境就绪、设备烧录完成,现在进入最终的用户验证环节——用手机 HA App 配网并控制 Matter 设备。过程分两步:先在 HA 中添加所需集成(一次性),再用手机扫码配网(每个设备一次)。

6.1 添加 HA 集成

  1. 浏览器打开 http://192.168.50.162:8123(替换为宿主机实际 IP),创建 HA 账号。位置页面可跳过,国家选择中国。创建用户界面如下:

    创建 HA 用户

  2. 登录后,依次点击 设置 → 设备与服务 → 添加集成,相关界面如下:

    设备与服务

    添加集成

    然后依次搜索并添加以下三个集成:

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

    搜索集成

    添加 OpenThread Border Router

    ② Thread。搜索 Thread 集成并添加 (Thread 集成可能被自动添加)。添加后点击 Thread 集成卡片,进入齿轮图标设置,将自动发现的网络设为 Preferred Network(首选网络):

    HA 将 Thread 网络设为 Preferred Network

    若自动发现失败("找不到board router"),执行 docker restart home-assistant 触发 zeroconf 重新扫描(见第 4.11 节)。

    设为首选网络后,应验证集成中的网络参数与 OTBR 容器内实际参数一致:

    • 点击 Thread 集成旁的感叹号图标,查看网络详情:
      Thread 集成网络详情
    • 在终端执行 docker exec -it otbr ot-ctl dataset,查看 OTBR 侧网络参数:
      OTBR 网络参数
    • 确认二者的 Channel 和 PAN ID 一致。

    ③ Matter。搜索 Matter 集成,在弹出界面中输入 ws://localhost:5580/ws:

    添加 Matter 集成

    三个集成全部添加完毕、均显示正常状态后,进入下一步手机配网。

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 手机截图为主):

  1. 打开 HA App,点击右上角 + 图标:

    添加设备至 HA

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

    选择 Matter 设备

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

    新设备

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

    配网界面

    扫描二维码

  5. 等待配网完成。期间可重点关注以下关键节点:

    • 下发凭证:

    下发凭证

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

      设备端 Thread 网络信息

    • 连接 Thread 网络:

      连接 Border Router

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

      关联HA
      连接成功

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

    分配房间

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

    控制设备

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

  • 设备端 UART日志:
    设备端配网成功 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 可以规避:

  1. 先开 VPN。
  2. 打开 Google Play Store,确认能正常刷新(说明 Google 服务已激活)。
  3. 关闭 VPN。
  4. 打开 HA App,扫码配网。
  5. BLE 连接成功后,过一小会儿,再打开 VPN。
  6. 保持 VPN 开着,直到显示"开始下发 Matter 凭证"。
    下发凭证
  7. 这时再关闭 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容器。

graph TB subgraph HAsys[HA 测试体系] Phone[手机 HA App] --> HAsrv[Home Assistant] HAsrv -->|WebSocket| MSrv[Matter Server<br/>控制器 · Fabric A] end subgraph CLIsys[chip-tool 测试体系] CLI[chip-tool 命令行<br/>控制器 · Fabric B] end MSrv --> OTBR[OTBR + nRF52840 RCP] CLI --> OTBR OTBR -->|Thread| Dev[light_bulb 设备]
对比项 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 节)。

参考资料:

posted on 2026-07-16 10:25  iini  阅读(378)  评论(0)    收藏  举报

导航