| # 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. |