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-toolchip-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 烧写,如下所示:

    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-urluart-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.shreset-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 需要一定时间。若长时间停留在 detacheddisabled,检查 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 Threadnrf54l15dk/nrf54l15/cpuapp(亦支持 nrf52840dk、nrf5340dk)
    • Matter over Wi-Finrf7002dk/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 网络参数
    • 确认二者的 ChannelPAN 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 输出 Establishedavahi-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.yamlzeroconf: 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 ConnectUnknownObject 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  阅读(109)  评论(0)    收藏  举报

导航