Creating manufacturer-specific clusters in Matter application

This guide describes how you can create a manufacturer-specific cluster for the Matter: Template sample. The Matter: Manufacturer-specific sample already contains a custom NordicDevkit cluster that you can use as a reference.

Overview

A manufacturer-specific cluster is a cluster that is not defined in the Matter Device Type Library Specification. The cluster description is written in XML format and is used to generate the C++ source files that provide the cluster implementation. You can add the cluster to the Matter data model definition file and use it in the ZAP tool to generate the source files.

Requirements

To take advantage of this guide, you need to be familiar with the Matter architecture and configuration, and have built and tested at least one of the available Matter samples.

Copy Matter template sample

Use the Matter: Template sample as the base for building a manufacturer-specific device as follows:

  1. Make sure that you meet the requirements for building the sample.

  2. Copy the contents of the samples/matter/template directory to a new directory meant for your custom application. For example, samples/matter/sensor.

  3. Build and test the sample as described on its documentation page.

Create a new cluster description file in XML format

You can create a new cluster description file in the following ways:

  • Using the Matter Cluster Editor app. The related tab provides the steps to create a new cluster description file.

  • Manually by writing an XML file. The related tab explains each element of the XML file to help you create the file manually. You can also use the example of the XML file provided at the end of this section.

Before using the tool, you need to download and install it. See the Matter Cluster Editor app documentation for installation instructions.

Once you have the tool installed, you can create a new cluster description file. Complete the following steps:

  1. Edit the CLUSTER tab contents.

    1. Open the CLUSTER tab.

    2. Fill in the domain, name, code, define, and description of the cluster as follows:

      Cluster tab

      CLUSTER tab

  2. Add a new command in the COMMANDS tab.

    1. Open the COMMANDS tab.

    2. Click Add command to open edit box.

    3. In the edit box, set the following values:

      • Name as MyCommand

      • Code as 0xFFF10000

      • Source as client

      • Response as MyCommandResponse

      • Description as Command that takes two uint8 arguments and returns their sum

    4. Click Arguments.

    5. In the new edit box, click the plus icon to create a new argument.

    6. Fill in Name as arg1, Type as int8u.

    7. Click the plus icon again to create second argument.

    8. Fill in Name as arg2, Type as int8u.

      The following figure shows the filled in edit box dialog with two arguments added:

      Arguments tab

      Arguments tab

    9. Click Save to save the arguments.

      The following figure shows the filled in edit box dialog with the new command added:

      Commands tab

      Commands tab

    10. Click Save to save the command.

  3. Add a new argument in the ATTRIBUTES tab.

    1. Open the ATTRIBUTES tab.

    2. Click Add attribute to open edit box dialog.

    3. Set the following values:

      • Name as MyAttribute

      • Side as server

      • Code as 0xFFF10000

      • Define as MY_ATTRIBUTE

      • Type as boolean

      • Writable as true

      The following figure shows the filled in edit box dialog with the new attribute added:

      Attributes tab

      Attributes tab

    4. Click Save to save the attribute.

  4. Add a new event in the EVENTS tab.

    1. Open the EVENTS tab.

    2. Click Add event to open edit box dialog.

    3. In the edit box, set the following values:

      • Code as 0xFFF10000

      • Name as MyEvent

      • Side as server

      • Priority as info

      • Description as Event that is generated by the server

    4. Click Fields.

    5. In the new edit box, click the plus icon to add a new field.

    6. Fill in the following values:

      • Field Id as 0x1

      • Name as arg1

      • Type as int8u

      The following figure shows the filled in edit box dialog with the new field added:

      Fields tab

      Fields tab

    7. Click Save to save the field.

      The following figure shows the filled in edit box dialog with the new event added:

      Event page

      Events tab

    8. Click Save to save the event.

  5. Add a new structure in the STRUCTURES tab.

    1. Open the STRUCTURES tab.

    2. Click Add structure to open edit box dialog.

    3. In the edit box, set the following values:

      • Name as MyStruct

      • Is Fabric Scoped as true

    4. Click Items.

    5. In the new edit box, click the plus icon to create a new item.

    6. Fill in the following values:

      • Field Id as 0x1

      • Name as value1

      • Type as int8u

      • Is Fabric Sensitive as true

      The following figure shows the filled in edit box dialog with the new item added:

      Structure items tab

      Structure items tab

    7. Click Save to save the item.

    8. Click Assigned clusters to open edit box dialog.

    9. In the new edit box, click the plus icon to create a new cluster assignment.

    10. Fill in Code with the value of the cluster code defined in first step as 0xFFF1FC01.

      The following figure shows the filled in edit box dialog with the new cluster added:

      Assigned clusters tab

      Assigned clusters tab

    11. Click Save to save the cluster.

      The following figure shows the filled in edit box dialog with the new structure added:

      Structures tab

      Structures tab

    12. Click Save to save the structure.

  6. Add a new enum in the ENUMS tab.

    1. Open the ENUMS tab.

    2. Click Add enum to open edit box dialog.

    3. Set the following values:

      • Name as MyEnum

      • Type as int8u

    4. Click Items.

    5. In the new edit box, click the plus icon to create a new item.

    6. Fill in the following values:

      • Name as EnumValue1

      • Value as 0

    7. Click the plus icon to create a new item.

    8. Fill in the following values:

      • Name as EnumValue2

      • Value as 1

      The following figure shows the filled in edit box dialog with the new items added:

      Items tab

      Items tab

    9. Click Save to save the item.

    10. Click Assigned clusters to open edit box dialog.

    11. In the new edit box, click the plus icon to create a new cluster assignment.

    12. Fill in Code with the value of the cluster code defined in first step as 0xFFF1FC01.

      The following figure shows the filled in edit box dialog with the new cluster assignment added:

      Assigned clusters tab

      Assigned clusters tab

    13. Click Save to save the cluster.

      The following figure shows the filled in edit box dialog with the new enum added:

      Enums tab

      Enums tab

    14. Click Save to save the enum.

  7. Add a new device type in the DEVICE TYPE tab.

    1. Open the DEVICE TYPE tab.

    2. Fill the fields as follows:

      Device type tab

      Device type tab

    3. Click Add cluster assignment to device type to open edit box dialog.

    4. Fill the Cluster fields as follows:

      Device type cluster assignment tab

      Device type cluster assignment tab

    5. Click Save to save the cluster assignment.

  8. Click the Save cluster to file button to save the cluster description file to the sample directory and name it as MyCluster.xml.

For an example, you can use the following template for the MyCluster.xml file:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<configurator>
<cluster>
   <domain>General</domain>
   <name>MyNewCluster</name>
   <code>0xfff1fc01</code>
   <define>MY_NEW_CLUSTER</define>
   <description>The MyNewCluster cluster showcases cluster manufacturer extensions</description>
   <attribute side="server" code="0xfff10000" define="MY_ATTRIBUTE" type="boolean" writable="true" default="false" optional="false" name="MyAttribute">
   </attribute>
   <command source="client" code="0xfff10000" name="MyCommand" optional="false">
      <description>Command that takes two uint8 arguments and returns their sum.</description>
      <arg name="arg1" type="int8u"/>
      <arg name="arg2" type="int8u"/>
   </command>
   <event side="server" code="0xfff10000" name="MyEvent" priority="info" optional="false">
      <description>Event that is generated by the server.</description>
      <arg name="arg1" type="int8u"/>
   </event>
</cluster>
<enum name="MyNewEnum" type="int8u">
   <cluster code="0xfff1fc01"/>
   <item name="EnumValue1" value="0"/>
   <item name="EnumValue2" value="1"/>
</enum>
<struct name="MyStruct" isFabricScoped="true">
   <cluster code="0xfff1fc01"/>
   <item fieldId="1" name="Data" type="octet_string" length="128" isFabricSensitive="true"/>
</struct>
<deviceType>
   <name>my-new-device</name>
   <domain>CHIP</domain>
   <typeName>My new device</typeName>
   <profileId editable="false">0x104</profileId>
   <deviceId editable="false">0xfff10001</deviceId>
   <class>Simple</class>
   <scope>Endpoint</scope>
   <clusters lockOthers="true">
      <include cluster="MyNewCluster" client="true" server="true" clientLocked="false" serverLocked="false"/>
   </clusters>
</deviceType>
<clusterExtension code="0x28">
   <attribute side="server" code="0x17" define="EXTENDED_ATTRIBUTE" type="boolean" writable="true" default="false" optional="false">ExtendedAttribute</attribute>
   <command source="client" code="0x0" name="ExtendedCommand" response="ExtendedCommandResponse" optional="false">
      <description>Command that takes two uint8 arguments and returns their sum.</description>
      <arg name="arg1" type="int8u"/>
      <arg name="arg2" type="int8u"/>
   </command>
   <command source="server" code="0x1" name="ExtendedCommandResponse" optional="false" disableDefaultResponse="true">
      <description>Response to ExtendedCommand.</description>
      <arg name="arg1" type="int8u"/>
   </command>
   <event side="server" code="0x4" name="ExtendedEvent" priority="info" optional="false">
      <description>Event that is generated by the server.</description>
      <arg name="arg1" type="int8u"/>
   </event>
</clusterExtension>
</configurator>

For further guidance, save this file as MyCluster.xml in the sample directory.

Add the cluster description file to the data model definition file and run the ZAP tool

The data model definition file contains all the cluster XML locations and manufacturers list. To work with the new custom cluster, you need to append it to the list in the existing data model definition file.

You can use the zap-gui command to add the cluster and run the ZAP tool, or zap-append command to add the cluster only without starting the ZAP tool. This guide focuses on the zap-gui command.

  1. Run the following command:

    west zap-gui -j src/default_zap/zcl.json --clusters ./MyCluster.xml
    

    This example command copies the original <default Matter SDK location>/src/app/zap-templates/zcl/zcl.json file, adds the MyCluster.xml cluster, and saves the new zcl.json file in the sample directory. The newly generated zcl.json file is used as an input to the ZAP tool.

    Note

    Execute the command from your application’s directory as the ZAP tool searches recursively for the .zap files in the current directory.

  2. Add an endpoint with the new device type in the ZAP tool.

    Endpoint with My new device in ZAP tool

    Endpoint with My new device in ZAP tool

  3. Locate the new cluster in the ZAP tool.

    New custom cluster in ZAP tool

    New custom cluster in ZAP tool

  4. Choose whether the cluster should be enabled for the Client and Server sides.

  5. Click the gear icon to open the cluster configuration and enable the attributes, commands, and events.

    1. In the Attributes tab, ensure that you have the required attributes enabled.

      Attributes of the new custom cluster in ZAP tool

      Attributes of the new custom cluster in ZAP tool

    2. In the Commands tab, ensure that you have the required commands enabled.

      Commands of the new custom cluster in ZAP tool

      Commands of the new custom cluster in ZAP tool

    3. In the Events tab, ensure that you have the required events enabled.

      Events of the new custom cluster in ZAP tool

      Events of the new custom cluster in ZAP tool

  6. Save the file and exit.

Generate the C++ code that contains the selected clusters

Run the following command to use the modified ZAP file to generate the C++ code that contains the selected clusters:

west zap-generate --full

Add the -j or --zcl-json argument to the command to specify the path to the zcl.json file if the file is not stored in the sample_directory/src/default_zap/ subdirectory.

For example:

west zap-generate --full -j ./zcl.json

Important

In the nRF Connect SDK versions older than 3.2.0, the zcl.json had to be stored in the sample_directory/src/default_zap/ subdirectory.

After completing these steps, the following changes will be visible within your sample directory:

  • The new cluster description file MyCluster.xml.

  • The updated data model definition file zcl.json with the new cluster and relative paths to the Matter data model directory.

  • The generated C++ source files for the new cluster.

  • The updated .zap file with the new cluster configuration and relative path to the zcl.json file.

Once the new cluster is added to the Matter application, you can call the zap-gui command without the additional --clusters argument. However, you still need to provide the path to the zcl.json file if you created a new one in a location different from the default one.

Align CMake configuration with the new cluster

Generating the .zap files with the --full option creates new source files under zap-generated/app-common. They need to override the default files located in the Matter SDK in the zzz_generated/app-common directory. To override the path, you need to set the CHIP_APP_ZAP_DIR variable in the CMakeLists.txt file, pointing to the parent of the generated app-common directory before initializing the Matter Data Model.

As custom clusters are not part of the default Matter SDK, you need to additionally pass a list of all new cluster names in an EXTERNAL_CLUSTERS argument when calling ncs_configure_data_model.

The following code snippet shows how to modify the Matter template CMakeLists.txt file with the new cluster:

project(matter-template)

# Override zap-generated directory.
include(${ZEPHYR_NRF_MODULE_DIR}/samples/matter/common/cmake/zap_helpers.cmake)
ncs_get_zap_parent_dir(ZAP_PARENT_DIR)

get_filename_component(CHIP_APP_ZAP_DIR ${ZAP_PARENT_DIR}/zap-generated REALPATH CACHE)

# Existing code in CMakeList.txt

ncs_configure_data_model(
   EXTERNAL_CLUSTERS "MY_NEW_CLUSTER" # Add EXTERNAL_CLUSTERS flag
)

# NORDIC SDK APP END

If you want to add more than one cluster custom cluster, you need to add all of them to the EXTERNAL_CLUSTERS argument.

For example:

ncs_configure_data_model(
   EXTERNAL_CLUSTERS "MY_NEW_CLUSTER" "MY_NEW_CLUSTER_2"
)

Implement all the required commands in the application code

You must implement the newly defined commands as a dedicated function in the application code to reflect the cluster functionality. Name the function by combining the emberAf prefix, cluster name, command name, and Callback suffix. The function must return a boolean value, and it takes the following parameters:

  • CommandHandler *commandObj - The command handler.

  • const ConcreteCommandPath &commandPath - The command path.

  • const <command name>::DecodableType &commandData - The command arguments, where <command name> is the name of the command.

For example, if you define the following command in the MyCluster.xml file:

<command source="client" code="0xFFF10000" name="MyCommand" optional="false">
   <description>Command that takes two uint8 arguments and returns their sum.</description>
   <arg name="arg1" type="int8u"/>
   <arg name="arg2" type="int8u"/>
</command>

Then, you need to implement the following command in the application code:

#include <app-common/zap-generated/callback.h>

bool emberAfMyNewClusterClusterMyCommandCallback(chip::app::CommandHandler *commandObj, const chip::app::ConcreteCommandPath &commandPath,
                                                 const chip::app::Clusters::MyNewCluster::Commands::MyCommand::DecodableType &commandData)
{
   // TODO: Implement the command.
}

void MatterMyNewClusterPluginServerInitCallback()
{
   // TODO: Implement the plugin server init callback.
}

Implement the extension handling in the application code

The way you handle cluster extensions depends on whether the cluster that you want to extend is implemented using the code-driven approach or with ZAP-generated code.

If the cluster is implemented using the code-driven approach, you must inherit from this cluster delegate class and implement the methods to handle the customized part. Then, you must unregister the original cluster delegate and register the customized one. For example, if you want to extend the BasicInformation cluster, you need to implement it in the application code as follows:

  • Inherit from the BasicInformationCluster class and override the methods to handle the customized part.

    #include <app/clusters/basic-information/BasicInformationCluster.h>
    #include <app/server-cluster/ServerClusterContext.h>
    #include <app/server-cluster/ServerClusterInterface.h>
    
    class BasicInformationExtension : public chip::app::Clusters::BasicInformationCluster {
    public:
       BasicInformationExtension() {}
    
       /* Overrides the default BasicInformationCluster implementation. */
       chip::app::DataModel::ActionReturnStatus
       ReadAttribute(const chip::app::DataModel::ReadAttributeRequest &request,
                     chip::app::AttributeValueEncoder &encoder) override;
    
            CHIP_ERROR Attributes(const chip::app::ConcreteClusterPath &path,
                             chip::ReadOnlyBufferBuilder<chip::app::DataModel::AttributeEntry> &builder) override;
    
                 CHIP_ERROR AcceptedCommands(const chip::app::ConcreteClusterPath &path,
                             chip::ReadOnlyBufferBuilder<chip::app::DataModel::AcceptedCommandEntry> &builder) override;
    
                 CHIP_ERROR GeneratedCommands(const chip::app::ConcreteClusterPath &path,
                             chip::ReadOnlyBufferBuilder<chip::app::DataModel::GeneratedCommandEntry> &builder) override;
    
                 CHIP_ERROR Attributes(const chip::app::ConcreteClusterPath &path,
                             chip::ReadOnlyBufferBuilder<chip::app::DataModel::AttributeEntry> &builder) override;
    
       chip::app::DataModel::ActionReturnStatus SetExtendedAttribute(bool newExtendedAttribute);
    
         private:
                 bool mExtendedAttribute;
         };
    
  • Implement the body of overridden methods to handle the custom attributes, commands and events.

    #include <app/util/attribute-storage.h>
    #include <clusters/BasicInformation/Events.h>
    #include <clusters/BasicInformation/Metadata.h>
    
    using namespace chip;
    using namespace chip::app;
    
    constexpr AttributeId kExtendedAttributeId = 0x17;
    
    constexpr DataModel::AttributeEntry kExtraAttributeMetadata[] = {
       { kExtendedAttributeId,
       {} /* qualities */,
       Access::Privilege::kView /* readPriv */,
       std::nullopt /* writePriv */ },
    };
    
    DataModel::ActionReturnStatus BasicInformationExtension::SetExtendedAttribute(bool newExtendedAttribute)
    {
       mExtendedAttribute = newExtendedAttribute;
       return CHIP_NO_ERROR;
    }
    
    DataModel::ActionReturnStatus BasicInformationExtension::ReadAttribute(const DataModel::ReadAttributeRequest &request,
                                   AttributeValueEncoder &encoder)
    {
       switch (request.path.mAttributeId) {
       case kExtendedAttributeId:
          return encoder.Encode(mExtendedAttribute);
       default:
          return chip::app::Clusters::BasicInformationCluster::ReadAttribute(request, encoder);
       }
    }
    
    DataModel::ActionReturnStatus BasicInformationExtension::WriteAttribute(const DataModel::WriteAttributeRequest &request,
                                AttributeValueDecoder &decoder)
    {
       switch (request.path.mAttributeId) {
       case kExtendedAttributeId:
          bool newExtendedAttribute;
          ReturnErrorOnFailure(decoder.Decode(newExtendedAttribute));
          return NotifyAttributeChangedIfSuccess(request.path.mAttributeId, SetExtendedAttribute(newExtendedAttribute));
       default:
          return chip::app::Clusters::BasicInformationCluster::WriteAttribute(request, decoder);
       }
    }
    
    CHIP_ERROR BasicInformationExtension::Attributes(const ConcreteClusterPath &path,
                      ReadOnlyBufferBuilder<DataModel::AttributeEntry> &builder)
    {
       ReturnErrorOnFailure(builder.ReferenceExisting(kExtraAttributeMetadata));
    
       return chip::app::Clusters::BasicInformationCluster::Attributes(path, builder);
    }
    
    CHIP_ERROR BasicInformationExtension::AcceptedCommands(const ConcreteClusterPath &path,
                             ReadOnlyBufferBuilder<DataModel::AcceptedCommandEntry> &builder)
    {
       /* The BasicInformationCluster does not have any commands, so it is not necessary to call the implementation of the base class. */
       static constexpr DataModel::AcceptedCommandEntry kAcceptedCommands[] = {
          Clusters::BasicInformation::Commands::ExtendedCommand::kMetadataEntry
       };
       return builder.ReferenceExisting(kAcceptedCommands);
    }
    
    std::optional<DataModel::ActionReturnStatus>
    BasicInformationExtension::InvokeCommand(const DataModel::InvokeRequest &request, chip::TLV::TLVReader &input_arguments,
                    CommandHandler *handler)
    {
       switch (request.path.mCommandId) {
       case Clusters::BasicInformation::Commands::ExtendedCommand::Id: {
          /* Implement the command handling logic here */
       }
       default:
          /* The BasicInformationCluster does not have any commands, so it is not necessary to call the implementation of the base class. */
          return Protocols::InteractionModel::Status::UnsupportedCommand;
       }
    }
    
  • Unregister the original cluster delegate and register the customized one.

    #include <data-model-providers/codegen/CodegenDataModelProvider.h>
    
    /* Replaces the registered BasicInformation cluster with a customized one that adds random number handling. */
         auto &registry = chip::app::CodegenDataModelProvider::Instance().Registry();
    
         ServerClusterInterface *interface =
                 registry.Get({ kRootEndpointId, chip::app::Clusters::BasicInformation::Id });
    
         VerifyOrDie(interface != nullptr);
    
         registry.Unregister(interface);
    static RegisteredServerCluster<BasicInformationExtension> sBasicInformationExtension;
    
         VerifyOrDie(registry.Register(sBasicInformationExtension.Registration()) == CHIP_NO_ERROR);
    

If the cluster is implemented with ZAP-generated code, you must implement the required extension callbacks by defining the appropriate emberAf...Callback functions, as described in the code examples and in the cluster XML. For example, if you want to extend the LevelControl cluster with the ExtendedCommand command, you need to implement it in the application code as follows:

#include <app-common/zap-generated/callback.h>

bool emberAfLevelControlClusterExtendedCommandCallback(chip::app::CommandHandler *commandObj, const chip::app::ConcreteCommandPath &commandPath,
                                                                           const chip::app::Clusters::BasicInformation::Commands::ExtendedCommand::DecodableType &commandData)
{
   // TODO: Implement the command.
}

Synchronizing the ZAP files with the new Matter SDK

If you want to update the Matter SDK revision in your project and you have custom clusters or device types in your project, you need to call the zap-sync command with the additional -j and --clusters arguments. This command updates the .zap file with all required changes from the new Matter SDK Device Type Library Specification and the zcl.json file with the new cluster and relative paths to the Matter data model directory.

This is needed especially when you notice an issue with opening the ZAP tool GUI.