Matter Actions 集群(0x0025)服务端实现指南:ActionsCluster 架构、Delegate 接口与实战接入
【免费下载链接】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
本篇技术指南以 connectedhomeip 仓库中 src/app/clusters/actions-server/README.md 为骨架,深入讲解 Matter **Actions 集群(Cluster ID0x0025)**服务端的完整实现。你将掌握:为何该集群必须遵循 "Scope: Node" 约束、ActionsCluster与旧版ActionsServer两套接入方式的区别、Actions::Delegate全部接口的语义,以及如何通过GenerateEvent异步上报StateChanged/ActionFailed事件,最终能在桥接设备(如 bridge-app)或任意聚合端点(aggregator endpoint)上正确落地一个可用的 Actions 集群。
集群概述与"Node 单例"作用域约束
Actions 集群用于在 Matter 设备上描述并控制"动作"(Action)——例如场景(Scene)、序列(Sequence)、自动化(Automation)、通知(Notification)等。它的数据中心模型定义可参考仓库中的规范快照 data_model/1.7/clusters/ActionsCluster.xml,其中明确写有:
<classification hierarchy="base" role="application" picsCode="ACT" scope="Node"/>scope="Node"是理解本集群实现的关键前提,它带来三条硬性约束:
- 只能出现在一个端点上:集群在整个节点上必须且只能存在于一个端点,通常是被桥接设备的聚合端点(aggregator endpoint),例如 bridge 类应用中的 endpoint 1;
- 同一时刻仅允许一个
ActionsServer实例:构造函数会对实例计数并记录诊断日志(详见下文源码分析); - Delegate 接口不带
EndpointId参数:因为设计上就只面向单一集群实例,无需在回调里区分端点。
正是基于这一约束,服务端实现才形成了"应用提供数据、集群层负责协议"的分层结构。
两层架构:新实现与向后兼容包装
实现分为两层,源码目录 src/app/clusters/actions-server/ 下同时包含新老两套代码:
ActionsCluster (一个实例,位于聚合端点) └── Actions::Delegate (由应用提供)ActionsCluster:新代码的规范实现(推荐)
ActionsCluster继承自DefaultServerCluster,是规范实现,代码见 ActionsCluster.h 与 ActionsCluster.cpp。它在聚合端点上实例化一次,并通过RegisteredServerCluster<ActionsCluster>注册到数据模型。其核心职责包括:
- 属性读取(
ReadAttribute):支持ActionList(ID0x0000)、EndpointLists(ID0x0001)、SetupURL(ID0x0002,可选)以及ClusterRevision、FeatureMap(当前编码为 0,即不使用 feature map),见 ActionsCluster.cpp; - 命令分发(
InvokeCommand):对 12 条命令统一执行"解码 → 校验 action 存在 → 校验命令位掩码支持"三步检查后,再转发给 Delegate 的Handle*回调(见下文命令表格); - 属性变更通知:
ActionListModified()与EndpointListsModified()分别对ActionList、EndpointLists触发NotifyAttributeChanged,见 ActionsCluster.cpp; - 事件生成:
GenerateEvent将StateChanged/ActionFailed事件投递到 fabric,见 ActionsCluster.cpp。
ActionsServer/CodegenIntegration:旧应用兼容包装
ActionsServer是包裹在ActionsCluster外层的一层薄壳,面向基于旧版 Ember/ZAP 生成 API 编写的应用,实现见 CodegenIntegration.h 与 CodegenIntegration.cpp。它:
- 为聚合端点持有一个
RegisteredServerCluster<ActionsCluster>; - 构造时从 Ember RAM 读取初始属性状态(如
SetupURL,通过Attributes::SetupURL::GetDefault读取并转成CharSpan,见 CodegenIntegration.cpp); - 暴露旧版
ActionListModified(EndpointId)/EndpointListModified(EndpointId)回调,对错误端点的调用会被静默忽略(VerifyOrReturn(aEndpoint == ...),见 CodegenIntegration.cpp); - 使用静态计数
sInstanceCount追踪活跃实例:超过 1 个实例时打印诊断日志"ActionsServer: %u instances active (multiple aggregator endpoints in use)."(CodegenIntegration.cpp)——注意:多聚合端点设备(例如 EP1 上是 Zigbee 桥、EP2 上是 Z-Wave 桥)各自托管一个 Actions 集群是合法的,因此计数仅用于诊断而非报错; - 析构函数会在未调用
Shutdown()的情况下自动补调用,避免数据模型注册表中残留悬垂指针(CodegenIntegration.cpp)。
值得留意的是 actions-server.h 头文件本身只做了一件事——#include <app/clusters/actions-server/CodegenIntegration.h>并在注释中声明"仅用于向后兼容,新代码应直接使用 ActionsCluster.h"。
ZAP 生成回调:刻意留空的桩
ZAP 生成的插件回调(MatterActionsClusterInitCallback等)在 CodegenIntegration.cpp 中被实现为空桩。应用直接实例化ActionsServer并通过Init()注册到 codegen 数据模型提供者——这正是标准的 code-driven 集群模式:集群生命周期由应用拥有,而非 ZAP 生成的脚手架。
用法:新老两种接入方式
新代码(code-driven 数据模型)
auto cluster = std::make_unique<ActionsCluster>(aggregatorEndpointId, myDelegate); // 通过 RegisteredServerCluster<ActionsCluster> 注册,并调用 Init()。ActionsCluster的构造签名(见 ActionsCluster.h)还支持两个可选参数:
ActionsCluster(EndpointId endpointId, Actions::Delegate & delegate, OptionalAttributesSet optionalAttributes = {}, std::optional<CharSpan> setupURL = std::nullopt);其中OptionalAttributesSet是OptionalAttributeSet<Actions::Attributes::SetupURL::Id>——即只有SetupURL是可声明为可选的属性;setupURL直接给出该属性的初始值。
旧代码(向后兼容)
// 通常在 emberAfActionsClusterInitCallback 中调用,并加保护确保只执行一次: sActionsDelegateImpl = std::make_unique<MyDelegate>(); sActionsServer = std::make_unique<ActionsServer>(aggregatorEndpointId, *sActionsDelegateImpl); sActionsServer->Init(); // 当动作列表发生变化时: sActionsServer->ActionListModified(aggregatorEndpointId); // 析构函数会自动完成 Shutdown。Init()与Shutdown()都是幂等的:Init()内部用VerifyOrReturnError(!mRegistered, ...)防止重复注册,Shutdown()用VerifyOrReturn(mRegistered)防止重复注销(见 CodegenIntegration.cpp)。
这一模式在仓库中有真实落地案例:
- examples/bridge-app/linux/bridged-actions-stub.cpp:在
emberAfActionsClusterInitCallback中创建LinuxActionsDelegateImpl与ActionsServer并调用Init(); - examples/all-clusters-app/all-clusters-common/src/bridged-actions-stub.cpp:同样在回调中创建,并额外校验
endpoint == 1、emberAfContainsServer(endpoint, Actions::Id)以及"仅初始化一次"(VerifyOrReturn(!sActionsDelegateImpl && !sActionsServer)),关闭时在emberAfActionsClusterShutdownCallback中调用Shutdown()。
Delegate 接口:应用侧数据源与命令处理
应用必须提供一个具体的Actions::Delegate实现,完整接口定义见 ActionsDelegate.h。按职责可分为三类:
1. 集合遍历(属性数据源)
| 接口 | 语义 | 终止条件 |
|---|---|---|
ReadActionAtIndex(uint16_t index, ActionStructStorage & action) | 按索引返回第 N 个动作;索引假定从 0 开始且无空洞 | 返回CHIP_ERROR_PROVIDER_LIST_EXHAUSTED表示已到列表末尾 |
ReadEndpointListAtIndex(uint16_t index, EndpointListStorage & epList) | 按索引返回第 N 个端点列表,同样假定连续索引 | 同上 |
HaveActionWithId(uint16_t aActionId, uint16_t & aActionIndex) | 按 action ID 快速查找(O(n)),并通过出参返回命中索引 | 返回true/false |
ActionStructStorage与EndpointListStorage定义在 ActionsStructs.h:前者内部缓冲区kActionNameMaxSize = 128,后者名称缓冲区同为 128、端点数组上限kEndpointListMaxSize = 256,均通过Set()完成带截断的拷贝,保证在受限内存(嵌入式)环境下安全。这些容量常量与数据模型 XML 中的约束一致(ActionStruct.Name最大 128 字节、EndpointListStruct.Endpoints最大 256 项)。
2. 命令处理(每个命令一个回调)
Handle*系列方法每个对应一条 Actions 命令,返回Status::Success表示接受,返回其他状态码表示拒绝(如NotFound、InvalidCommand)。以下是规范快照 ActionsCluster.xml 与 ActionsDelegate.h 对应的完整 12 条命令及签名:
| 命令(ID) | Delegate 方法 | 额外参数含义 |
|---|---|---|
InstantAction(0x00) | HandleInstantAction(actionId, invokeId) | — |
InstantActionWithTransition(0x01) | HandleInstantActionWithTransition(actionId, transitionTime, invokeId) | transitionTime:从当前状态过渡到新状态的时间 |
StartAction(0x02) | HandleStartAction(actionId, invokeId) | — |
StartActionWithDuration(0x03) | HandleStartActionWithDuration(actionId, duration, invokeId) | duration:保持 start 状态的时长 |
StopAction(0x04) | HandleStopAction(actionId, invokeId) | — |
PauseAction(0x05) | HandlePauseAction(actionId, invokeId) | — |
PauseActionWithDuration(0x06) | HandlePauseActionWithDuration(actionId, duration, invokeId) | duration:保持 pause 状态的时长 |
ResumeAction(0x07) | HandleResumeAction(actionId, invokeId) | — |
EnableAction(0x08) | HandleEnableAction(actionId, invokeId) | — |
EnableActionWithDuration(0x09) | HandleEnableActionWithDuration(actionId, duration, invokeId) | duration:保持 active 状态的时长 |
DisableAction(0x0A) | HandleDisableAction(actionId, invokeId) | — |
DisableActionWithDuration(0x0B) | HandleDisableActionWithDuration(actionId, duration, invokeId) | duration:保持 disable 状态的时长 |
所有命令的invokeId均为可选参数(Optional<uint32_t>),对应数据模型 XML 中InvokeID字段的optionalConform声明,用于把异步事件与特定命令调用关联起来。
3. 命令分发前的统一校验
ActionsCluster::InvokeCommand对每条命令先执行统一的DECODE_AND_VALIDATE流程(见 ActionsCluster.cpp):
- 用
DataModel::Decode解码 TLV 入参,失败返回Status::InvalidCommand; - 调用
ValidateActionExists,通过mDelegate.HaveActionWithId查找 action,找不到返回Status::NotFound; - 调用
ValidateCommandSupported,校验该 action 的supportedCommands位掩码是否包含当前命令(action.supportedCommands.Raw() & (1 << commandId)),不支持返回Status::InvalidCommand,见 ActionsCluster.cpp。
另外,ActionsCluster::AcceptedCommands在 ActionsCluster.cpp 中一次性声明全部 12 条命令为 accepted;Attributes通过AttributeListBuilder合并强制属性(kMandatoryMetadata)与可选的SetupURL。这意味着应用只需在 Delegate 中通过supportedCommands位掩码声明每条 action 支持哪些命令,服务端会自动拒绝未声明的调用——这一机制与数据模型 XML 中CommandBits位图(bit 0 到 bit 11)一一对应。
异步事件生成:状态变更上报
当命令被接受后,action 的实际状态发生变化时,应用需要异步调用ActionsCluster::GenerateEvent向 fabric 发送对应 Matter 事件(示例见 ActionsCluster.h):
cluster.GenerateEvent(Events::StateChanged::Type{ actionId, invokeId, newState }); cluster.GenerateEvent(Events::ActionFailed::Type{ actionId, invokeId, state, error });底层实现(ActionsCluster.cpp)会先检查集群上下文mContext是否为空(未注册时生成事件会打印错误日志),随后通过mContext->interactionContext.eventsGenerator.GenerateEvent(event, mPath.mEndpointId)以集群所在端点发出事件。
两个事件的数据结构与优先级来自数据模型规范:
StateChanged(事件 ID0x00,优先级info):字段ActionID(uint16)、InvokeID(uint32)、NewState(ActionStateEnum);ActionFailed(事件 ID0x01,优先级info):字段ActionID、InvokeID、NewState以及Error(ActionErrorEnum)。
配套的枚举取值(定义于 ActionsCluster.xml):
ActionStateEnum:Inactive(0)、Active(1)、Paused(2)、Disabled(3);ActionErrorEnum:Unknown(0,其他未列出的原因)、Interrupted(1,被另一条命令或交互打断);ActionTypeEnum:Other(0)、Scene(1)、Sequence(2)、Automation(3)、Exception(4)、Notification(5)、Alarm(6);EndpointListTypeEnum:Other(0)、Room(1)、Zone(2)。
正如 ActionsDelegate.h 接口注释所强调的:"Handle*命令回调的实现需要按需调用OnStateChanged或OnActionFailed来生成规范要求的事件"——即事件生成是 Delegate 侧在状态真正变化后主动触发的,服务端只在命令被接受时返回Status::Success,并不会替应用伪造状态变迁。
测试覆盖与验证入口
仓库为 actions-server 提供了两组单元测试,均基于pw_unit_test框架(ClusterTester辅助类):
- tests/TestActionsCluster.cpp:验证
ActionsCluster核心逻辑。测试中定义了MockActionsDelegate(内部持有ActionStructStorage mActions[kMaxActions]与EndpointListStorage mEndpointLists[kMaxEndpointLists],并跟踪HandleInstantAction是否被调用、记录mLastActionId/mLastInvokeId/mReturnStatus),覆盖属性列表编码、命令分发的校验路径以及事件生成等场景; - tests/TestActionsClusterBackwardCompatability.cpp:验证
ActionsServer兼容包装层的注册/注销(Init/Shutdown)与旧回调行为。
构建入口见 BUILD.gn(该目录同时提供 app_config_dependent_sources.gni 与 app_config_dependent_sources.cmake,供 GN 与 CMake 两套构建体系按配置裁剪源码)。
端到端示例:桥接应用中的 Actions 集群
以 examples/bridge-app/linux/bridged-actions-stub.cpp 为例,可看到完整的落地方案:
LinuxActionsDelegateImpl的ReadActionAtIndex从内部std::vector<Action*>中取出可见(getIsVisible())的 action 并填充ActionStructStorage,找不到目标索引时返回CHIP_ERROR_PROVIDER_LIST_EXHAUSTED;ReadEndpointListAtIndex从GetEndpointListInfo返回的EndpointListInfo构造DataModel::List<const EndpointId>后调用epList.Set(...);HaveActionWithId线性遍历查找 action ID 并回填索引;- 各
Handle*命令在示例中暂返回Status::NotFound(未实现),而 all-clusters-app 的 bridged-actions-stub.cpp 中的emberAfActionsClusterInitCallback则展示了"仅 endpoint 1、仅一次、先校验再构造"的标准防御式初始化。
在 ZAP 配置侧,all-clusters-app.zap 中 Actions 集群以"code": 37(0x0025)、"side": "server"、"enabled": 1声明,并逐个启用了 12 条传入命令——这与ActionsCluster::AcceptedCommands的声明保持一致。
小结
本集群的落地要点可归纳为四条:
- 位置唯一:Actions 集群是 "Scope: Node" 集群,只放在聚合端点上,一个节点通常一个实例(多聚合端点时每端点一个);
- 新代码用
ActionsCluster:code-driven 数据模型下直接构造并RegisteredServerCluster注册;旧 Ember/ZAP 应用继续用ActionsServer包装层,享受幂等的Init()/Shutdown()与自动清理; - Delegate 是数据与行为的唯一来源:属性靠
ReadActionAtIndex/ReadEndpointListAtIndex遍历,命令靠 12 个Handle*回调执行,位掩码supportedCommands控制命令可见性; - 事件手动上报:状态真正变化后调用
GenerateEvent发送StateChanged/ActionFailed,让 fabric 上的订阅者感知动作生命周期。
如需从零实践,建议从 tests/TestActionsCluster.cpp 中的MockActionsDelegate入手理解接口契约,再对照 bridge-app 的集成方式将集群挂到自己的聚合端点上。
【免费下载链接】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),仅供参考