blob: d93fba0bfc0f7a57d4271a75eca40e285a09e98f [file] [view]
# Writing and Updating Clusters
This guide provides a comprehensive walkthrough for creating a new Matter
cluster implementation, referred to as a "code-driven" cluster.
## Overview of the Process
Writing a new cluster involves the following key stages:
1. **Define the Cluster:** Generate or update the cluster definition XML based
on the Matter specification.
2. **Implement the Cluster:** Write the C++ implementation for the cluster's
logic and data management.
3. **Integrate with Build System:** Add the necessary files to integrate the new
cluster into the build process.
4. **Integrate with Application:** Connect the cluster to an application's code
generation configuration.
5. **Test:** Add unit and integration tests to verify the cluster's
functionality.
---
## Part 1: Cluster Definition (XML)
Clusters are defined based on the Matter specification. The C++ code for them is
generated from XML definitions located in
`src/app/zap-templates/zcl/data-model/chip`.
- **Generate XML:** To create or update a cluster XML, use
[Alchemy](https://github.com/project-chip/alchemy) to parse the
specification's `asciidoc`. Manual editing of XML is discouraged, as it is
error-prone.
- **Run Code Generation:** Once the XML is ready, run the code generation
script. It's often sufficient to run:
```bash
./scripts/run_in_build_env.sh 'scripts/tools/zap_regen_all.py'
```
For more details, see the
[code generation guide](../zap_and_codegen/code_generation.md).
---
## Part 2: C++ Implementation
### File Structure
Create a new directory for your cluster at
`src/app/clusters/<cluster-directory>/`. This directory will house the cluster
implementation and its unit tests.
For zap-based support, the directory mapping is defined in
[src/app/zap_cluster_list.json](https://github.com/project-chip/connectedhomeip/blob/master/src/app/zap_cluster_list.json)
under the `ServerDirectories` key. This maps the `UPPER_SNAKE_CASE` define of
the cluster to the directory name under `src/app/clusters`.
#### Naming conventions
Names vary, however to be consistent with most of the existing code use:
- `cluster-name-server` for the cluster directory name
- `ClusterNameSnakeCluster.h/cpp` for the `ServerClusterInterface`
implementation
### Recommended Implementation Pattern
For optimal flash and RAM usage on resource-constrained devices, we strongly
recommend a **combined implementation** pattern. You should avoid splitting the
implementation into separate logic and translation layers, as this introduces
unnecessary overhead.
- **Combined Implementation (Recommended):**
- The cluster's logic, data storage, and `ServerClusterInterface`
implementation are all contained within a single class (often by
deriving from `DefaultServerCluster`).
- This minimizes boilerplate and virtual function translation layers,
resulting in a significantly smaller flash footprint.
- **Example:** The
[Basic Information](https://github.com/project-chip/connectedhomeip/tree/master/src/app/clusters/basic-information)
cluster is a good example of a combined implementation.
- **Modular Implementation / `ClusterLogic` (Not Recommended):**
- Historically, some clusters separated core business logic into a
type-safe `ClusterLogic` class, with a `ClusterImplementation` class
acting as a translation layer.
- While this isolated the logic for testing, it adds noticeable flash and
RAM overhead and is **discouraged** for new clusters.
- **Example:** The
[Administrator Commissioning](https://github.com/project-chip/connectedhomeip/tree/master/src/app/clusters/administrator-commissioning-server)
cluster demonstrates this legacy modular implementation.
- **`ClusterDriver` (or `Delegate`):**
- An optional interface providing callbacks to the application for cluster
interactions. We recommend the term `Driver` to avoid confusion with the
overloaded term `Delegate`.
### Design Principles
When designing and implementing a cluster, adhere to the following principles to
ensure a high-quality and developer-friendly experience:
#### Prioritize Easy Application Development
Clusters should aim to do as much work as possible autonomously, reducing the
burden on the application developer.
- **Handle Common Logic Internally:** Implement persistence (NVM), timers, and
complex state machines within the cluster itself. The application should
only be notified of significant events or changes it needs to act upon. _For
example, a state machine managing a multi-step process like a firmware
update, door lock/unlock sequence with retries, or a calibration procedure
should typically reside within the cluster, rather than requiring the
application to manage the intermediate steps and timeouts._
- **Provide Helper Abstractions:** If a cluster requires the application to
implement complex logic, consider providing helper classes or default
implementations that simplify the task.
- **Encapsulate Complexity:** Avoid deferring low-level details (like raw
storage keys or individual timer management) to the application.
#### Delegate/Driver Pattern for Validation
When an application needs to be involved in a cluster operation (especially
writes or commands), use a delegate (or driver) interface that acts as a
"pre-check."
- **Pre-Write Validation:** For writable attributes, provide a callback that
allows the application to accept or reject the new value _before_ it is
applied to the cluster's internal state or persisted.
- **Delegate Veto:** Callbacks must return a
`Protocols::InteractionModel::Status`. Returning any status other than
Success allows the application to reject the proposed change. The cluster
MUST honor this by failing the operation and propagating the delegate's
status code to the initiator.
- **Perform Cluster-Level Checks First:** The cluster remains responsible for
all spec-defined validations (e.g., range checks, constraint validations, or
state-based restrictions) before involving the application delegate.
- **Avoid Redundant Notifications:** Ensure that no-op operations (e.g.,
writing the same value that is already present) are handled early and do not
trigger delegate callbacks or change notifications.
### BUILD file layout
The description below will describe build files under
`src/app/clusters/<cluster-directory>/`. You are expected to have the following
items:
#### `BUILD.gn`
This file will contain a target that is named `<cluster-directory>`, usually a
`source_set`. This file gets referenced from
[src/app/chip_data_model.gni](https://github.com/project-chip/connectedhomeip/blob/master/src/app/chip_data_model.gni)
by adding a dependency as `deps += [ "${_app_root}/clusters/${cluster}" ]`, so
the default target name is important.
#### `app_config_dependent_sources`
There are two code generation integration support files: one for `GN` and one
for `CMake`. The way these work is that
`chip_data_model.gni`/`chip_data_model.cmake` will include these files and
bundle _ALL_ referenced sources into _ONE SINGLE SOURCE SET_, together with
ember code-generated settings (e.g. `endpoint_config.h` and similar files that
are application-specific)
As a result, there will be a difference between `.gni` and `.cmake`:
- `app_config_dependent_sources.gni` will typically just contain
`CodegenIntegration.cpp` and any other helper/compatibility layers (e.g.
`CodegenIntegration.h` if applicable)
- `app_config_dependent_sources.cmake` will contain all the files that the
`.gni` file contains PLUS any dependencies that the `BUILD.gn` would pull in
but cmake would not (i.e. dependencies not in the `libCHIP` builds). These
extra files are often the `*.h/*.cpp` files that were in the `BUILD.gn`
source set.
**EXAMPLE** taken from
([src/app/clusters/basic-information](https://github.com/project-chip/connectedhomeip/tree/master/src/app/clusters/basic-information)):
```
# BUILD.gn
import("//build_overrides/build.gni")
import("//build_overrides/chip.gni")
source_set("basic-information") {
sources = [ ... ]
public_deps = [ ... ]
}
```
```
# app_config_dependent_sources.gni
app_config_dependent_sources = [ "CodegenIntegration.cpp" ]
```
```
# app_config_dependent_sources.cmake
# This block adds the codegen integration sources, similar to app_config_dependent_sources.gni
TARGET_SOURCES(
${APP_TARGET}
PRIVATE
"${CLUSTER_DIR}/CodegenIntegration.cpp"
)
# These are the things that BUILD.gn dependencies would pull
TARGET_SOURCES(
${APP_TARGET}
PRIVATE
"${CLUSTER_DIR}/BasicInformationCluster.cpp"
"${CLUSTER_DIR}/BasicInformationCluster.h"
)
```
### Implementation Details
#### Attribute and Feature Handling
Your implementation must correctly report which attributes and commands are
available based on the enabled features and optional items.
- Use a feature map to control elements dependent on features.
- Use boolean flags or `BitFlags` for purely optional elements.
- Ensure your unit tests cover different combinations of enabled features and
optional attributes/commands.
#### Attribute Accessors
Your cluster implementation must provide public getter and setter APIs for each
attribute to allow applications to interact with cluster state.
- **Getter Methods:** Provide a getter method for every attribute (e.g.,
`GetCurrentSensitivityLevel()`, `GetAlarmsActive()`). Applications need
these to read the current cluster state.
- **Return by value (preferred):** Getters should return copies of data
whenever practical. This avoids lifetime and ownership concerns.
- **Avoid returning pointers or references:** Returning pointers or
references to internal cluster data create lifetime risks—if the
underlying memory is deallocated while the caller still holds the
pointer, use-after-free bugs can occur. If you must return a pointer or
reference, clearly document that the returned value is only valid for
immediate use and must not be stored.
- **Setter Methods:** Provide methods to modify all non-fixed (mutable)
attributes in spec-compliant ways. For simple attributes, this may be a
straightforward setter (e.g., `SetCurrentSensitivityLevel()`). However, spec
compliance may require updating multiple attributes together atomically—in
such cases, provide a higher-level API that encapsulates the required
behavior rather than individual setters. When the application's driver state
changes, these methods can be used to update the cluster's state
accordingly. Setters are also responsible for triggering attribute change
notifications (see
[Attribute Change Notifications](#attribute-change-notifications)).
- **Example:** The
[Boolean State Configuration](https://github.com/project-chip/connectedhomeip/blob/master/src/app/clusters/boolean-state-configuration-server/BooleanStateConfigurationCluster.h)
cluster demonstrates this pattern.
#### Attribute Change Notifications
For subscriptions to work correctly, you must notify the system whenever an
attribute's value changes.
- The `Startup` method of your cluster receives a `ServerClusterContext`.
- Use the context to call
`interactionContext->dataModelChangeListener->MarkDirty(path)`. A
`NotifyAttributeChanged` helper exists for paths managed by this cluster.
- For write implementations, you can use `NotifyAttributeChangedIfSuccess`
together with a separate `WriteImpl` such that any successful attribute
write will notify.
Canonical example code would look like:
```cpp
DataModel::ActionReturnStatus SomeCluster::WriteAttribute(const DataModel::WriteAttributeRequest & request,
AttributeValueDecoder & decoder)
{
// Delegate everything to WriteImpl. If write succeeds, notify that the attribute changed.
return NotifyAttributeChangedIfSuccess(request.path.mAttributeId, WriteImpl(request, decoder));
}
```
- For the `NotifyAttributeChangedIfSuccess` ensure that WriteImpl is
returning
[ActionReturnStatus::FixedStatus::kWriteSuccessNoOp](https://github.com/project-chip/connectedhomeip/blob/master/src/app/data-model-provider/ActionReturnStatus.h)
when no notification should be sent.
**Crucial:** No-op writes (where the value remains unchanged) MUST NOT
trigger:
- Network attribute change notifications.
- Application-level delegate/driver callbacks.
Canonical example is:
```cpp
VerifyOrReturnValue(mValue != newValue, ActionReturnStatus::FixedStatus::kWriteSuccessNoOp);
```
- **Per-Attribute Change Callbacks:** As a concrete realization of the
[Delegate/Driver Pattern for Validation](#delegate-driver-pattern-for-validation),
each mutable attribute should have a corresponding
`On<AttributeName>Changed` callback in the delegate interface. These are
_pre-write_ hooks invoked after spec-level validation and the no-op guard,
but _before_ the value is committed. The callback must always receive the
**proposed new value**, not the current (stale) value. Returning `true`
accepts the change; returning `false` vetoes it. The cluster must then fail
the operation with `Protocols::InteractionModel::Status::Failure` (for APIs
returning `Status`) or `CHIP_ERROR_INCORRECT_STATE` (for APIs returning
`CHIP_ERROR`). Default implementations should return `true` so applications
only override the callbacks they need.
**Example:** The
[Boolean State Configuration delegate](https://github.com/project-chip/connectedhomeip/blob/master/src/app/clusters/boolean-state-configuration-server/boolean-state-configuration-delegate.h)
declares:
```cpp
virtual bool OnCurrentSensitivityLevelChanged(uint8_t newValue) { return true; }
virtual bool OnAlarmsActiveChanged(chip::BitMask<AlarmModeBitmap> newValue) { return true; }
virtual bool OnAlarmsSuppressedChanged(chip::BitMask<AlarmModeBitmap> newValue) { return true; }
virtual bool OnAlarmsEnabledChanged(chip::BitMask<AlarmModeBitmap> newValue) { return true; }
virtual bool OnSensorFaultChanged(chip::BitMask<SensorFaultBitmap> newValue) { return true; }
```
And the cluster invokes them in the standard order—validate, guard no-op,
call delegate, then commit and notify:
```cpp
VerifyOrReturnError(level < mSupportedSensitivityLevels, CHIP_IM_GLOBAL_STATUS(ConstraintError));
VerifyOrReturnError(mCurrentSensitivityLevel != level, CHIP_NO_ERROR);
if (mDelegate != nullptr)
{
VerifyOrReturnError(mDelegate->OnCurrentSensitivityLevelChanged(level), CHIP_ERROR_INCORRECT_STATE);
}
mCurrentSensitivityLevel = level;
NotifyAttributeChanged(CurrentSensitivityLevel::Id);
```
#### Persistent Storage
- **Attributes:** For scalar attribute values, use `AttributePersistence` from
`src/app/persistence/AttributePersistence.h`. The `ServerClusterContext`
provides an `AttributePersistenceProvider`.
- **General Storage:** For non-attribute data, the context provides a
`PersistentStorageDelegate`.
#### Optimizing for Flash/RAM
For common or large clusters, you may need to optimize for resource usage.
Consider using `C++` templates to compile-time select features and attributes,
which can significantly reduce flash and RAM footprint.
### Advanced `ServerClusterInterface` Details
While `ReadAttribute`, `WriteAttribute`, and `InvokeCommand` are the most
commonly implemented methods, the `ServerClusterInterface` has other methods for
more advanced use cases.
#### List Attribute Writes (`ListAttributeWriteNotification`)
This method is an advanced callback for handling large list attributes that may
require special handling, such as persisting them to storage in chunks. A
typical example of a cluster that might use this is the **Binding cluster**. For
most clusters, the default implementation is sufficient.
#### Event Permissions (`EventInfo`)
You must implement the `EventInfo` method if your cluster emits any events that
require non-default permissions to be read. For example, an event might require
`Administrator` privileges. While not common, this should be verified for every
new cluster implementation and checked during code reviews to ensure event
access is correctly restricted.
#### Accepted vs. Generated Commands
The distinction between `AcceptedCommands` and `GeneratedCommands` can be
understood using a REST API analogy:
- **`AcceptedCommands`**: These are the "requests" that the server cluster can
process. In the Matter specification, these are commands sent from the
client to the server (`client => server`).
- **`GeneratedCommands`**: These are the "responses" that the server cluster
can generate after processing an accepted command. In the spec, these are
commands sent from the server back to the client (`server => client`).
These lists are built based on the cluster's definition in the Matter
specification.
### Unit Testing
Unit tests should reside in `src/app/clusters/<cluster-name>/tests/`.
Use the `chip::Testing::ClusterTester` utility to write your unit tests. This
modern API removes the need to manually mock encoders, handlers, or raw TLV
buffers. More on [ClusterTester Helper Class Guide](cluster_tester.md).
- **Test Setup:** Create a mock delegate to inject fake data into your cluster
instance.
- **Menu Verification:** Ensure `Attributes()` and `AcceptedCommands()` return
the correct metadata, or `ClusterTester` will reject your reads/invocations.
- **Reads:** Test `ReadAttribute` via `tester.ReadAttribute()` and verify data
matches your mock.
- **Commands:** Test commands via `tester.Invoke()` and ensure specific
`Protocols::InteractionModel::Status` codes are returned accurately based on
delegate responses.
- **Reporting:** Verify reporting logic by reading `tester.GetDirtyList()` to
ensure state changes properly mark attributes as dirty, while No-Op writes
do not.
---
## Part 3: Build and Application Integration
### Build System Integration
The build system maps cluster names to their source directories. Add your new
cluster to this mapping:
- Edit `src/app/zap_cluster_list.json` and add an entry for your cluster,
pointing to the directory you created.
### Application Integration (`CodegenIntegration.cpp`)
To integrate your cluster with an application's `.zap` file configuration, you
need to bridge the gap between the statically generated code and your C++
implementation.
1. **Create `CodegenIntegration.cpp`:** This file will contain the integration
logic.
2. **Create Build Files:** Add `app_config_dependent_sources.gni` and
`app_config_dependent_sources.cmake` to your cluster directory. These files
should list `CodegenIntegration.cpp` and its dependencies. See existing
clusters for examples.
3. **Use Generated Configuration:** The code generator creates a header file at
`<app/static-cluster-config/<cluster-name>.h` that provides static,
application-specific configuration. Use this to initialize your cluster
correctly for each endpoint.
4. **Implement Callbacks:** Implement
`Matter<Cluster>ClusterInitCallback(EndpointId)` and
`Matter<Cluster>ClusterShutdownCallback(EndpointId)` in your
`CodegenIntegration.cpp`.
5. **Update `config-data.yaml`:** To enable these callbacks, add your cluster to
the `CodeDrivenClusters` array in
`src/app/common/templates/config-data.yaml`.
6. **Update ZAP Configuration:** To prevent the Ember framework from allocating
memory for your cluster's attributes, you must:
- In `src/app/zap-templates/zcl/zcl.json` and
`zcl-with-test-extensions.json`, add all non-list attributes of your
cluster to `attributeAccessInterfaceAttributes`. This marks them as
externally handled.
7. **Regenerate ZAP:** Once `config-data.yaml` and
`zcl.json/zcl-with-test-extensions.json` are updated, run the ZAP
regeneration command, like
```bash
./scripts/run_in_build_env.sh 'scripts/tools/zap_regen_all.py'
```
---
## Part 4: Example Application and Integration Testing
- Write unit tests to ensure cluster test coverage
- **Integrate into an Example:** Add your cluster to an example application,
such as the `all-clusters-app`, to test it in a real-world scenario.
- use tools such as `chip-tool` or `matter-repl` to manually validate the
cluster
- **Add Integration Tests:** Write integration tests to validate the
end-to-end functionality of your cluster against the example application.