connectedhomeip(Matter SDK)集群与设备类型开发完全指南:从 XML 定义、ZAP/Ember 集成到单元测试
【免费下载链接】connectedhomeipMatter (formerly Project CHIP) creates more connections between more objects, simplifying development for manufacturers and increasing compatibility for consumers, guided by the Connectivity Standards Alliance.项目地址: https://gitcode.com/GitHub_Trending/co/connectedhomeip
导读
本文是 Matter 开源 SDK(connectedhomeip)中开发新 Cluster(集群)与 Device Type(设备类型)的端到端技术指南,内容源自仓库 docs/cluster_and_device_type_dev 目录下的系列文档,并辅以当前仓库源码验证。读完本文,你将掌握:如何在src/app/zap-templates/zcl/data-model/chip/下编写集群/设备类型的 XML 定义并接入 ZAP 代码生成链路;如何选择 Ember 层回调与AttributeAccessInterface/CommandHandlerInterface覆盖(Override)两种实现机制;以及如何使用ClusterTester辅助类对 code-driven 集群进行可维护、可认证的单元测试。
集群与设备类型开发的三大目标
开发一个新的 Cluster 或 Device Type,最终要交付三类产出物:
- 集群实现(Cluster implementation)——真正运行在设备上的 C++ 逻辑。
- 支撑 ZAP 代码生成的素材(ZAP glue)——让 ZAP 工具能识别新集群、为 Ember 层生成函数签名与默认实现。
- 测试与认证材料(Tests & certification)——单元测试、YAML/Python 测试计划与自动化脚本,用于证明代码正确性并支撑认证。
单元测试、测试计划与认证测试在 docs/testing 相关章节展开;本文聚焦第 1、2 点,即如何在 SDK 中实现集群与设备类型。
Cluster 定义(XML)
集群定义的载体是 XML 文件,它是对 Matter 规范中集群的直接翻译:
- 描述结构体(struct)、枚举(enum)、属性(attribute)、命令(command)、事件(event)等;
- 存放位置:src/app/zap-templates/zcl/data-model/chip(该目录下现存 100 余个集群定义文件,如
access-control-cluster.xml、air-quality-cluster.xml、audio-control-cluster.xml等,可直接作为新集群的参考模板)。
Cluster 实现
实现分客户端与服务器两端:
- 客户端侧:主要依赖代码生成(codegen),开发者只需编写“胶水层”调用代码。
- 服务器侧:通过 Ember 层生成代码和/或实现两个覆盖接口:
- AttributeAccessInterface——拦截属性读写的接口;
- CommandHandlerInterface——拦截命令处理的接口。
- 实现文件位于
src/app/clusters/<your_cluster_name>,构建配置通过 src/app/chip_data_model.gni 接入——该构建文件利用 codegen 产出的数据自动填充集群列表,在 ZAP 中勾选新集群后即可把对应代码编入固件。
Device Type 定义
设备类型同样以 XML 定义其一致性(conformance)要求,主文件为 src/app/zap-templates/zcl/data-model/chip/matter-devices.xml。从源码可见(matter-devices.xml),每个<deviceType>元素包含设备名称、domain、typeName、profileId、deviceId、revision、class/scope以及强制/可选集群的<include cluster="..." client="..." server="..."/>列表,ZAP 据此决定端点配置中该设备类型可挂载的集群。
注意:定义完成后,还应使用 .matter 解析工具(详见 docs/guides/matter_idl_tooling.md)对照规范验证产出。
ZAP、Ember 与 Overrides:三层协作模型
ZAP(Zigbee Cluster Configurator 的演进版)的核心目标,是让工具链理解新集群,从而在任意设备上被 ZAP 配置所引用(XML 定义 + 胶水代码)。
集群定义与 ZAP 的关系
关于 ZAP 的入门介绍见 docs/zap_and_codegen/zap_intro.md。完成 XML 定义与注册步骤后,可以用 zaptool 打开任意.zap文件来验证集群是否出现在工具中:
./scripts/tools/zap/run_zaptool.sh <filename>验证要点(依次在 ZAP GUI 中检查):
- 设备类型列表:打开端点(Endpoint)配置,确认新设备类型出现在设备类型列表中。
- 集群完整性:XML 中的
domain参数决定集群归属的分组;确认集群包含全部预期属性、命令与事件。 - 属性存储选项:确认每个属性的存储方式(RAM / External)设置符合实现预期。
集群实现:Ember 层与 Overrides 的区别
Ember 层是用于在设备上建立/访问端点、属性、命令等的底层机制:
- 构建时自动生成 Ember 函数签名与默认实现;
- 集群服务器代码实现初始化、命令、属性访问、事件生成等回调;
- 当 Interaction Model(交互模型)收到针对该集群的入站交互时,会调用对应的 Ember 函数。
**Overrides(覆盖)**与 Ember 层最大的差异在于生命周期:
- Ember 层在编译时生成,而 Overrides 在运行时安装;
- 对属性访问或命令处理而言,Overrides 会先于Ember 层被调用;
- 通过 AttributeAccessInterface 与 CommandHandlerInterface 实现,提供远高于 Ember 层的控制粒度,但复杂度也更高;
- 关键优势:可单元测试(UNIT TESTABLE);
- 若 ZAP 中保留了相应配置,两种机制都允许“回落(fall through)”到 Ember 层。
集群服务器初始化流程
入站消息从 Matter 核心流入集群初始化代码的完整流程如下:
EmberAfInitializeAttributes——针对 ZAP 中标记为RAM存储的全部属性,在 Ember 属性存储中写入默认值;Matter<Cluster>PluginServerCallback——.h是生成文件,.cpp实现在集群服务器代码中编写。利用它完成集群初始化,并通过chip::app::AttributeAccessInterfaceRegistry::Instance().Register注册覆盖实例,以在外部处理属性读写。
集群服务器属性:两种机制的选择
| 存储方式(ZAP 中设置) | Ember 层行为 | Override 要求 |
|---|---|---|
| RAM | 自动分配存储,Ember 处理读写;生成文件 Accessors.h 为每个属性生成 Get/Set 函数 | 可选。若覆盖函数中不对该属性编码,则回落至存储层。若总在覆盖函数里编码,则纯属浪费 RAM |
| External | 不分配任何存储,不回落 Ember 存储 | 必须注册访问覆盖(access override),否则属性无法工作 |
通过 Override 读属性
属性读取在 AttributeAccessInterface::Read() 中实现。源码注释明确了三种处理方式:返回失败(转换为 StatusIB 返回给客户端);成功并编码数据(数据返回客户端);成功但不编码(回落 Ember 属性存储/外部回调)。典型实现:
CHIP_ERROR Read(const ConcreteReadAttributePath & aPath, AttributeValueEncoder & aEncoder) { // 解析 aPath 确定被请求的属性 switch (aPath.mAttributeId) { case SomeAttribute::Id: // 直接编码值 aEncoder.Encode(mSomeValue); break; } // 小心 List 属性 —— 必须使用 EncodeList 才能正确处理分块(chunking) return CHIP_NO_ERROR; }通过 Override 写属性
写入与读取走同一条路径,但落在 AttributeAccessInterface::Write() 中。源码注释同样给出三种行为:返回失败(转换为 StatusIB);成功并解码(视为成功写入);成功但不解码(回落 Ember 存储)。属性处理器(attribute handler)需要自行负责约束检查(constraint checking)与属性持久化(attribute persistence)。
属性持久化
- 使用
AttributeAccessInterface时,需要自行管理需要持久化的属性; - 可通过
AttributePersistence(基于AttributePersistenceProvider)完成,它为读写任意类型的值到默认 PersistenceProvider 提供了统一 API,见 src/app/AttributePersistence.h; - 单元测试时,可将
TestPersistentStorageDelegate与DefaultAttributePersistenceProvider组合使用。
Ember 层读写
在 Ember 层函数中,编解码由 Ember 层自己处理。这对简单属性可行,但复杂属性交互会很有挑战,且Ember 层极难单元测试。Ember 层提供属性变更回调供应用处理:
void MatterPostAttributeChangeCallback(const chip::app::ConcreteAttributePath & attributePath, uint8_t type, uint16_t size, uint8_t * value)注意:使用该回调时要确保实现方式能够跨多个示例(examples)复用。
集群服务器命令
命令处理与属性一样存在 Ember 与 Override 两条路:
- Override:运行时注册(
InteractionModelEngine::RegisterCommandHandler),实现CommandHandlerInterface; - Ember:静态的
emberAf<ClusterName><CommandName>Callback。
命令分发流程如下图所示:
命令处理器编写要点
- CommandHandlerInterface(见 CommandHandlerInterface.h):
- 可使用
HandleCommand便捷函数(自动标记为已处理); - 否则需显式设置命令是否已处理——若未处理,默认回落 Ember 层;
- 若命令完全由该接口处理,在 src/app/common/templates/config-data.yaml 的
CommandHandlerInterfaceOnlyClusters列表中登记,关闭 Ember 命令回调生成。
- 可使用
- Ember 接口:返回
true表示命令已处理;返回false会收到无效命令响应。 - 两者通用:
- 必须在命令处理器中通过
AddResponse或AddStatus处理对调用方的返回; - 必须按规范进行约束检查并返回正确的状态码或响应体。
- 必须在命令处理器中通过
事件与属性订阅
- 属性变更上报(Attribute change reporting):
- 走 Ember 存储层(生成的属性 Get/Set 函数)时自动处理;
- 使用
AttributeAccessInterface时,需要主动通知上报引擎属性已变更——调用MatterReportingAttributeChangeCallback。
- 事件(Events):
- Ember 层没有直接支持;
- 调用 EventLogging.h 中的
LogEvent函数。调用方需持有 Matter 栈锁(lock),或将事件排入 Matter 事件队列(queue)后再调用LogEvent。
动态端点的补充说明
- ZAP 配置在编译期是静态的;
- 也可以在运行时注册动态端点(常见于桥接设备 bridge):
- 使用
emberAfSetDynamicEndpoint; - 如果为属性等数据自建了存储,必须同时考虑动态端点与静态端点两种情况。
- 使用
向 SDK 添加新集群与设备类型:完整操作清单
以下步骤均建立在 Matter 规范已评审通过的前提下。
第一步:添加集群定义到 SDK
- 在目录 src/app/zap-templates/zcl/data-model/chip 中,以恰当的命名新建集群定义 XML 文件。
- 将新集群定义引用登记到以下文件中:
.github/workflows/tests.yaml
scripts/rules.matterlint
src/app/zap-templates/zcl/zcl-with-test-extensions.json
src/app/zap-templates/zcl/zcl.json
若新集群是派生集群(derived cluster),需在基类集群定义中加入引用(例如在
mode-base-cluster.xml中加入集群代码,否则运行regen_all.py时会抛出难以理解的异常):<struct name="ModeOptionStruct"> <cluster code="0x0051"/> <!-- Laundry Washer Mode --> <cluster code="YOUR NEW CLUSTER ID"/> </struct>src/controller/python/matter/clusters/init.py
- 在 src/controller/data_model/controller-clusters.zap 中启用新集群(作用于 Python 与 Android 客户端),需要借助 ZAP 工具编辑:
若本机未安装 ZAP,可运行 zaptool 直接打开目标文件:
./scripts/tools/zap/run_zaptool.sh src/controller/data_model/controller-clusters.zapGUI 操作步骤:
- 在左侧面板选择
Endpoint-1; - 打开集群分组(例如
Appliances); - 找到要启用的集群(例如
Dishwasher Control); - 在 Enable 列的下拉框中为该集群选择
Client; File -> Save保存配置,然后关闭 GUI。
- 在左侧面板选择
- 在 src/app/zap_cluster_list.json 的
ClientDirectories一节添加条目。 - 更新 chip-tool:
重新生成全部 ZAP 生成代码:
./scripts/tools/zap_regen_all.py(该脚本位于 scripts/tools/zap_regen_all.py);重新构建 chip-tool,即可获得新集群支持:
./scripts/examples/gn_build_example.sh examples/chip-tool SOME-PATH/
第二步:添加设备类型定义到 SDK
将设备 XML 定义加入 matter-devices.xml;
实现所有应用通用的入站命令行为:TLV 载荷到 C++ 结构体的解析由 XML 自动生成的代码完成(生成产物见 zzz_generated/app-common/app-common/zap-generated),其余功能需手动实现。重新生成自动代码的方法:
全部重新生成:
./scripts/tools/zap_regen_all.py仅重新生成 app-common 部分:
./scripts/tools/zap/generate.py -t src/app/common/templates/templates.json -o zzz_generated/app-common/app-common/zap-generated src/controller/data_model/controller-clusters.zap
为所有非全局属性(如
CommandList、AttributeList等全局属性无需实现)实现读写与存储操作。非 list/struct 类型属性的处理代码是通用生成的,通常无需额外工作;实现所有应用通用的属性规范要求,例如特定范围(range)的强制约束、属性间交互的处理等。
第三步:实现代码与测试
- 新集群实现放在 src/app/clusters,编写规范见 docs/guides/writing_clusters.md;
- 测试放在 src/app/tests/suites;
- 实现示例集群服务器应用:
- YAML 测试会针对该服务器运行;
- 两个可选方案:
- 在all-clusters-app中启用新集群并作为示例服务器(可另附一个仅含相关集群的精简示例应用,属锦上添花);
- 若集群有复杂的全局应用需求,考虑独立示例应用(可参考 door lock、bridge、TV、OTA 集群的做法)。
mode-base 派生集群接入 all-clusters-app 的完整清单
假设新集群名为XYZMode(派生自mode-base):
- 在 src/app/zap-templates/zcl/data-model/chip 中创建精简的
xyz-mode-cluster.xml(派生集群定义远小于普通集群,参考 dishwasher-mode-cluster.xml)。依据规范审视是否需要StartUpMode、OnMode属性; - 确认已在 mode-base-cluster.xml 中登记集群代码:
<struct name="ModeTagStruct"> <cluster code="0xXXXX"><struct name="ModeOptionStruct"> <cluster code="0xXXXX"><bitmap name="Feature" type="bitmap32"> <cluster code="0xXXXX">
- 在
xyz-mode.h中定义模式/标签/tag,参考 dishwasher-mode.h; - 添加实例化模式的 stub,参考 dishwasher-mode.cpp;
- 修改 examples/all-clusters-app/linux/main-common.cpp:
- 添加
#include "xyz-mode.h" - 在
ApplicationShutdown()中调用Clusters::XYZMode::Shutdown();
- 添加
- 在 examples/all-clusters-app/linux/BUILD.gn 的
sources中加入xyz-mode.cpp; - 修改 src/app/common/templates/config-data.yaml:
- 在
EnumsNotUsedAsTypeInXML一节添加<XYZMode>::ModeTag(该节的作用是避免代码误以为可安全使用kUnknownEnumValue,派生集群尤其关键); - 在
CommandHandlerInterfaceOnlyClusters一节添加XYZ Mode条目(该节用于声明完全由CommandHandlerInterface实现、无需生成命令分发的集群);
- 在
- 在 src/app/util/util.cpp 的
// Cluster Init Functions...区域添加空实现void MatterXYZModePluginServerInitCallback() {}; - 修改 src/app/zap-templates/zcl/zcl-with-test-extensions.json:
- 将
xyz-mode-cluster.xml加入xmlFile列表; - 在
attributeAccessInterfaceAttributes中添加"XYZ Mode": [ "SupportedModes", "CurrentMode", "FeatureMap" ]——这样 ZAP 就不会为这些属性生成处理代码;
- 将
- 修改 src/app/zap_cluster_list.json:
- 在
ClientDirectories对象中添加XYZ_MODE_CLUSTER: []; - 在
ServerDirectories对象中添加"XYZ_MODE_CLUSTER": ["mode-base-server"]。
- 在
第四步:测试计划与 CI 集成
- 添加测试计划,使用模板:cluster_test_plan_template.adoc 所述流程对应的官方测试计划模板(位于 CHIP-Specifications/chip-test-plans 仓库);
- 注意:CHIP-Tool 参考客户端是从 XML 生成的(无需手写客户端代码);
- 按需添加测试:
- 相对简单的测试:在 src/app/tests/suites/certification 添加 YAML 测试,并更新 src/app/tests/suites/certification/PICS.yaml;
- 较复杂的测试:在 src/python_testing 添加 Python 测试;
- 接入 CI:
- Python 测试加入 .github/workflows/tests.yaml,需提供每个 Python 脚本的全部参数(如端点 PIXIT);
- YAML 测试编辑 src/app/tests/suites/ciTests.json:创建
"MyDevice"章节列出该设备的所有 YAML 测试,并将章节名加入"collection"列表,然后执行./scripts/tools/zap_regen_all.py重新生成 ZAP 代码;
- 将设备类型规范加入测试计划工具(device_type_requirements,供 src/app/tests/suites/certification/Test_TC_DESC_2_1.yaml 使用);
- 将设备类型加入 Chef:examples/chef/devices。
常见问题(Q&A)
- Q1:测试活动可以用什么设备?一种实现可以是测试框架 + all-clusters 示例应用 + Raspberry Pi;两个独立实现需运行在目标硬件上(可以是 mock-up、原型机等)。
- Q2:Chef 工具如何用于上述交付物?官方答复为 TBD(待定)。
- Q3:如何使用 ZAP 自动生成代码并提交结果?搜索本文提到的
zap_regen,运行后把变更文件全部加入 git 提交。 - Q4:旧的集群定义在哪里?src/app/zap-templates/zcl/data-model/silabs/general.xml。
- Q5:ZAP 文档在哪?ZAP 官方仓库的 README(外部链接,按需访问)。
面向可测试性与可移植性的集群设计
推荐的组合式实现(Combined Implementation)
新集群设计推荐组合式实现模式,以在保持完全可单元测试的同时优化 flash 与 RAM 占用:
- 组合式实现(推荐):集群的逻辑、数据存储与
ServerClusterInterface实现集中在一个类中,通常继承DefaultServerCluster。最小化样板代码与虚函数开销。 - 模块化实现 /
ClusterLogic(不推荐):把逻辑拆到独立的ClusterLogic类会带来明显的 flash 与 RAM 开销,新集群不建议采用。 - 平台相关代码分离(按需):若集群需要触发平台/硬件特定动作,通过构造函数注入Delegate(或 Driver)接口。这既保证集群可移植,也便于在测试中 mock delegate。简单的上报型集群(如传感器读数)往往完全不需要 delegate——应用直接调用 setter 即可。
整体架构
Interaction Model --(Read/Write/Invoke)--> MyCluster --(可选 Hardware/Platform actions)--> DelegateMyCluster实现ServerClusterInterface;Delegate为可选虚线连接,仅当需要平台动作时存在;- 组合式实现中
DefaultServerCluster提供:- 数据版本(Data version)管理;
- 路径管理(单端点/集群对);
- 未处理属性/命令的默认状态响应;
- 与
ServerClusterContext的集成。
组合式集群实现示例
DataModel::ActionReturnStatus DiscoBallCluster::ReadAttribute(const DataModel::ReadAttributeRequest & request, AttributeValueEncoder & encoder) { switch (request.path.mAttributeId) { case Attributes::ClusterRevision::Id: return encoder.Encode(kRevision); case Attributes::Run::Id: return encoder.Encode(mRun); // ... } return Protocols::InteractionModel::Status::UnsupportedAttribute; }Delegate / Driver 接口
Delegate 是可选组件,仅在集群需要触发平台/硬件特定动作时使用(例如驱动电机旋转、调节真实设备的亮度)。简单的上报集群(如传感器测量值)可完全跳过 delegate,由应用通过 setter 直接推送状态。
使用 delegate 时,通过构造函数注入以便在单元测试中 mock。集群在调用 delegate 之前,仍需完成所有规范定义的校验(如范围检查)。
class DiscoBallDelegate { public: virtual ~DiscoBallDelegate() = default; virtual void OnRunChanged(bool run) = 0; };用 ClusterTester 为 Code-Driven 集群编写单元测试
单元测试应聚焦行为正确性与规范一致性。对于 code-driven 集群,ClusterTester辅助类是强制要求(mandatory)的测试组件。
ClusterTester 是什么
ClusterTester是位于 src/app/server-cluster/testing/ClusterTester.h 的 C++ 辅助类,为实现了ServerClusterInterface的 Matter 集群简化单元测试。其价值体现在:
- 自动 TLV 处理:写入/命令时自动把 C++ 结构编码为 TLV,读取/响应时自动把 TLV 解码为 C++ 结构;
- 内存安全:对于返回视图(view)的属性(如
CharSpan、List),ClusterTester持有底层 TLV 数据的所有权,测试作用域内可安全检视数据而无悬垂指针之忧; - 类型安全:利用生成的数据模型类型(如
Attributes::MyAttr::TypeInfo)确保参数与规范一致。
从源码看(ClusterTester.h),其核心 API 包括ReadAttribute、WriteAttribute(含 list 专用重载,支持指定写入模式)与Invoke。
基础用法:无 Fabric 上下文的集群
对于不需要 fabric 上下文(fabric context)的简单集群(如Boolean State),直接包装集群实例即可。
#include <app/server-cluster/testing/ClusterTester.h> #include <app/clusters/boolean-state-server/BooleanStateCluster.h> class TestBooleanStateCluster : public ::testing::Test { // ... (Setup memory and context) ... BooleanStateCluster booleanState{kRootEndpointId}; // 用集群实例初始化 tester chip::Testing::ClusterTester tester{booleanState}; void SetUp() override { booleanState.Startup(testContext.Get()); } };读取属性:
TEST_F(TestBooleanStateCluster, ReadAttributeTest) { // 1. 读取简单整型属性 uint16_t revision{}; ASSERT_EQ(tester.ReadAttribute(Globals::Attributes::ClusterRevision::Id, revision), CHIP_NO_ERROR); // 2. 读取 bitmap uint32_t features{}; ASSERT_EQ(tester.ReadAttribute(FeatureMap::Id, features), CHIP_NO_ERROR); // 3. 读取布尔值 bool stateValue{}; ASSERT_EQ(tester.ReadAttribute(StateValue::Id, stateValue), CHIP_NO_ERROR); }高级用法:Fabric 作用域集群
涉及访问控制(Access Control)或 fabric 作用域的复杂集群(如Group Key Management),ClusterTester与FabricTestFixture集成。不必把 fixture 传入 tester 构造器,但在执行操作前必须在 tester 上设置 active fabric index。
class TestGroupKeyManagementCluster : public ::testing::Test { TestServerClusterContext mTestContext; FabricTestFixture fabricHelper{ &mTestContext.StorageDelegate() }; GroupKeyManagementCluster mCluster{ fabricHelper.GetFabricTable() }; // 仅用集群初始化 tester ClusterTester tester{ mCluster}; void SetUp() override { // ... (Startup cluster) ... // 初始化测试 fabric fabricHelper.SetUpTestFabric(kTestFabricIndex); // 重要:为 tester 设置 fabric index。 // 之后所有的写入/命令都会被当作来自该 fabric。 tester.SetFabricIndex(kTestFabricIndex); } };写入复杂类型属性(Struct)
Tester 自动处理 Struct 等复杂类型(List 除外):
TEST_F(TestCameraAVStreamManagementCluster, TestReadWriteViewport) { ... // 写入一个合法的新值 Attributes::Viewport::TypeInfo::Type newViewport = { 0, 0, 1280, 720 }; EXPECT_EQ(mClusterTester.WriteAttribute(Attributes::Viewport::Id, newViewport), CHIP_NO_ERROR); }写入 List 属性
list 属性存在两种主流写入模式,集群服务器通常都支持,因此测试应通过ListWritingPattern遍历两种模式以面向未来兼容:
TEST_F(TestUserLabelCluster, WriteValidLabelListTest) { // 每种 List 写入模式各跑一遍 for (ListWritingPattern listWritingPattern : { ListWritingPattern::ReplaceAll, ListWritingPattern::ClearAllThenAppendItems }) { ClusterTester tester(userLabel); Structs::LabelStruct::Type labels[] = { { .label = "room"_span, .value = "bedroom 2"_span }, { .label = "orientation"_span, .value = "North"_span }, }; // 包进 DataModel::List auto listToWrite = DataModel::List(labels); // 写入属性。 // 对 fabric 作用域属性,tester 自动处理 EncodeForWrite ASSERT_EQ(tester.WriteAttribute(LabelList::Id, listToWrite, listWritingPattern), CHIP_NO_ERROR); } }调用命令
使用Invoke方法与生成的 Command Request 结构体交互:
TEST_F(TestGroupKeyManagementCluster, TestKeySetWriteCommand) { // 1. 构造 Request 结构体 GroupKeyManagement::Commands::KeySetWrite::Type requestData; requestData.groupKeySet.groupKeySetID = kTestKeySetId; requestData.groupKeySet.groupKeySecurityPolicy = GroupKeyManagement::GroupKeySecurityPolicyEnum::kTrustFirst; requestData.groupKeySet.epochStartTime0 = kStartTime; // ... 填充其余字段 ... // 2. 调用命令 // CommandId 由请求类型自动推导, // 也可以显式传入: tester.Invoke(CommandId, request) auto result = tester.Invoke(requestData); // 3. 校验结果 EXPECT_TRUE(result.IsSuccess()); }关键行为与测试场景
完整类定义见 ClusterTester.h 头文件,以下为简化测试的若干内部行为:
- 读操作的内存管理:
ReadAttribute自动管理底层 TLV 数据。含视图/列表的类型(CharSpan、ByteSpan、DecodableList)会被 tester 在内部缓冲;该数据在ClusterTester实例生命周期内持续有效,可安全迭代列表或检视 span。 - 写入的 Fabric 作用域处理:
WriteAttribute通过 C++ 内省(introspection)检测结构体是否为 fabric 作用域——若是则自动使用EncodeForWrite,否则使用标准Encode;同时自动应用与SetFabricIndex()对应 fabric 索引的访问控制主体描述符(subject descriptor)。 - Invoke 的结果统一:返回
InvokeResult<ResponseType>,同时携带状态与解码后的响应载荷(若命令返回数据)。IsSuccess()在状态为成功且(对返回数据的命令)响应存在时返回 true;GetStatusCode()返回std::optional<ClusterStatusCode>,可省去ASSERT_TRUE(status.has_value())直接以单个EXPECT_EQ()校验状态码;该 helper 面向同步执行的单元测试设计。
验证副作用(事件与上报)
- 验证事件生成:
// 1. 执行动作(如写属性) tester.WriteAttribute(MyAttribute::Id, newValue); // 2. 从队列弹出下一个事件 auto event = tester.GetNextGeneratedEvent(); ASSERT_TRUE(event.has_value()); // 3. 用规范生成的 decodable 类型解码事件载荷 MyCluster::Events::MyEvent::DecodableType eventData; ASSERT_EQ(event->GetEventData(eventData), CHIP_NO_ERROR); // ... 校验 eventData 字段 ...- 验证脏属性(Dirty Attributes):
// 1. 执行动作 tester.Invoke(myCommand); // 2. 检查特定属性是否被标记为 dirty EXPECT_TRUE(tester.IsAttributeDirty(MyAttribute::Id)); // 或者按需检视完整 dirty path 列表 auto & dirtyList = tester.GetDirtyList();推荐测试清单
针对 code-driven 集群,至少覆盖以下测试类别:
- 初始化:
Startup后初始属性值正确; - 范围检查:setter 与命令处理器对越界值返回错误(如
ConstraintError); - 边界情况:最小值、最大值、可空(nullable)属性的 null 值行为;
- 规范一致性:所有规范定义的错误条件均正确处理;
- 副作用:状态变更时的事件生成与脏属性标记;
- Delegate 交互:正确调用 delegate、正确处理 delegate 上报的错误;
- Fabric 隔离:fabric 作用域属性与命令的正确行为。
测试既有集群的两个选项
- 选项 1:重构(Refactor,推荐):将集群重构为使用
DefaultServerCluster与ClusterTester,长期可维护性最佳; - 选项 2:接口边界测试(Test at Interface Boundary):实例化集群,通过
ServerClusterInterface边界用ClusterTester交互。适合暂时无法完整重构的既有实现。
结语
至此,一条完整的新集群/设备类型开发流水线已经打通:从src/app/zap-templates/zcl/data-model/chip/下的 XML 规范翻译,到 ZAP/Ember/codegen 三层的注册与接线,再到AttributeAccessInterface/CommandHandlerInterface覆盖机制的精细控制,最后以DefaultServerCluster+ClusterTester的组合交付可单元测试、可移植、可认证的服务器实现。开发者可按 docs/cluster_and_device_type_dev/how_to_add_new_dts_and_clusters.md 的清单逐项落地,并以 docs/cluster_and_device_type_dev/unit_testing_clusters.md 与 docs/cluster_and_device_type_dev/cluster_tester.md 作为测试环节的规范依据,最终将新功能安全地带入 Matter 生态。
【免费下载链接】connectedhomeipMatter (formerly Project CHIP) creates more connections between more objects, simplifying development for manufacturers and increasing compatibility for consumers, guided by the Connectivity Standards Alliance.项目地址: https://gitcode.com/GitHub_Trending/co/connectedhomeip
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考