Create a sample cluster

Manufacturer Specific Extensions (MS Extensions) are defined in Section 7.19 of the Matter Specification. Manufacturer-specific clusters are identified by a Manufacturer Extension Identifier (MEI). This guide covers the implementation of the samplemei cluster, available in the Matter SDK.

This guide should be performed in a Linux host environment running a Debian-based distribution.

Get started

  1. Clone the Matter repository in your host workstation:

    $ git clone https://github.com/project-chip/connectedhomeip.git
    $ cd connectedhomeip
    
  2. Install linux prerequisites:

    $ sudo apt-get install git gcc g++ pkg-config libssl-dev libdbus-1-dev \
        libglib2.0-dev libavahi-client-dev ninja-build python3-venv python3-dev \
        python3-pip unzip libgirepository1.0-dev libcairo2-dev libreadline-dev
    
  3. Initialize the Matter repository:

    $ source scripts/bootstrap.sh
    

Define your MS Extension Data Model

The first step to adding a custom feature is to define your data model in an XML file. In our sample, we've created the file sample-mei-cluster.xml. You may use the TestCluster as an exhaustive example of possible extensions, including structs and lists.

In case you want to define your own new cluster, copy sample-mei-cluster.xml and place your data model file to the same folder: src/app/zap-templates/zcl/data-model/chip.

The XML definition file can be broken down in 5 parts:

  1. Header: The file is defined as an XML version 1.0, with the main tag <configurator> and specifying the domain to CHIP. Other domains may be possible, but haven't been tested.

    <?xml version="1.0"?>
    <!--
    Copyright (c) 2023 Project CHIP Authors
    
    Licensed under the Apache License, Version 2.0 (the "License");
    you may not use this file except in compliance with the License.
    You may obtain a copy of the License at
    
        http://www.apache.org/licenses/LICENSE-2.0
    
    Unless required by applicable law or agreed to in writing, software
    distributed under the License is distributed on an "AS IS" BASIS,
    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    See the License for the specific language governing permissions and
    limitations under the License.
    -->
    <configurator>
    <domain name="CHIP"/>
    
  2. General properties:

    a. <name> and <define>: Define a name for your new cluster and apply it consistently. Several scripts generate source code based on the naming convention defined here. Be sure to follow the same naming conventions as in the sample. In different places you're expected to use the same pattern that already exists in the samples, either:

    1. Pascal Case: MyCluster
    2. Space Separated: My Cluster
    3. Screaming Snake Case: MY_CLUSTER

    a. <client|server tick>: The tick mechanism would schedule periodic calls from the scheduler, so your cluster can perform timed or delayed operations. The sample cluster for this guide doesn't use those. In case your cluster needs to use timed calls, it's often preferable to use schedule your own delayed executions using DeviceLayer::SystemLayer().StartTimer().

    a. code: This is a concatenation of your Vendor ID (0xFFF1, test VID in our case) with the Cluster ID (0xFC20 in our case). Remember to change these values to your own production VID and chosen Cluster IDs when developing your product beyond an initial proof-of-concept.

    <cluster>
        <domain>General</domain>
        <name>Sample MEI</name>
        <code>0xFFF1FC20</code>
        <define>SAMPLE_MEI_CLUSTER</define>
        <description>The Sample MEI cluster showcases cluster manufacturer extensions</description>
    
  3. Attributes: Define your custom Matter attributes. Make sure the define XML attributes are in Screaming Snake Case format. The XML content has a matching name in Pascal case.

    <!-- Attributes -->
    <!-- A simple boolean attribute that flips or flops -->
    <attribute side="server" code="0x0000" define="FLIP_FLOP" type="BOOLEAN" writable="true" default="false" optional="false">FlipFlop</attribute>
    
  4. Commands: Define both commands and command responses, such as AddArgumentsResponse. In this case the AddArguments command can be invoked with two arguments, and returns a AddArgumentsResponse with their sum. Ping is a command with no arguments.

    <!-- Command Responses -->
        <command source="server" code="0x01" name="AddArgumentsResponse" optional="false" disableDefaultResponse="true">
        <description>
            Response for AddArguments that returns the sum.
        </description>
        <arg name="returnValue" type="INT8U"/>
        </command>
    
        <!-- Commands -->
        <command source="client" code="0x02" name="AddArguments" response="AddArgumentsResponse" 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="client" code="0x00" name="Ping" optional="false">
        <description>
            Simple command without any parameters and without a response.
        </description>
        </command>
    
  5. Events: No events are defined in this cluster.

    <!-- Events -->
    </cluster>
    </configurator>
    

Create the MS Source Code

The source code of your cluster should be placed in src/app/clusters/<cluster-name>. It builds whenever your device's .zap/.matter files include the cluster in any of its endpoints.

src/app/clusters/sample-mei-server/sample-mei-server.h is the header of MEI's cluster's classes. You may implement your cluster in any way you prefer. samplemei shows an example of how that may be done. We have declared a class within it that holds the same Attributes previously defined in the XML file. samplemei has a single Attribute: bool flipflop.

(...)
*****************************************************************************
// SampleMeiContent has the stateful attributes of the cluster: its endpoint
// and attributes

class SampleMeiContent
{
public:
    EndpointId endpoint;

    // Attribute List
    bool flipflop; /* Attributes::FlipFlop::Id */

    SampleMeiContent(EndpointId endpoint);
    SampleMeiContent();
};

Meanwhile, your main class for the server cluster should inherit from AttributeAccessInterface and CommandHandlerInterface and override the Read(), Write() and InvokeCommand() methods to perform Attributes R/W, as well as be called when commands are received.

// *****************************************************************************
// SampleMeiServer implements both Attributes and Commands

class SampleMeiServer : public AttributeAccessInterface, public CommandHandlerInterface
{
public:
    (...)
    // Attributes
    CHIP_ERROR Read(const ConcreteReadAttributePath & aPath, AttributeValueEncoder & aEncoder) override;
    CHIP_ERROR Write(const ConcreteDataAttributePath & aPath, AttributeValueDecoder & aDecoder) override;

    // Commands
    void InvokeCommand(HandlerContext & ctx) override;

    // Attribute storage
#if SAMPLE_MEI_NUM_SUPPORTED_ENDPOINTS > 0
    SampleMeiContent content[kNumSupportedEndpoints];
#else
    SampleMeiContent* content = nullptr;
#endif

    (...)
};

src/app/clusters/sample-mei-server/sample-mei-server.cpp implements the samplemei cluster class. In your cluster, each supported command should be included in the cases handled by the switch statement of InvokeCommand().

void SampleMeiServer::InvokeCommand(HandlerContext & ctxt)
{
    (...)

    switch (ctxt.mRequestPath.mCommandId)
    {
    case Commands::Ping::Id:
        HandleCommand<Commands::Ping::DecodableType>(ctxt, [endpoint](HandlerContext & ctx, const auto & req) {
            ChipLogProgress(Zcl,"Ping Command on endpoint %d", endpoint);
            ctx.mCommandHandler.AddStatus(ctx.mRequestPath, Protocols::InteractionModel::Status::Success);
        });
        return;

        (...)

    }
}

The Read(), Write() and InvokeCommand() methods must be defined, along with any other class methods that have been declared.

CHIP_ERROR SampleMeiServer::Read(const ConcreteReadAttributePath & aPath, AttributeValueEncoder & aEncoder)
{
     (...)

    switch (aPath.mAttributeId)
    {
        case Attributes::FlipFlop::Id:
            ChipLogProgress(Zcl,"Read Attribute flip-flop from endpoint %d index %zu value %d", endpoint, endpointIndex, content[endpointIndex].flipflop);
            err = aEncoder.Encode(content[endpointIndex].flipflop);
            break;
        default:
        break;
    }

    return err;
}

CHIP_ERROR SampleMeiServer::Write(const ConcreteDataAttributePath & aPath, AttributeValueDecoder & aDecoder)
{
    (...)

    switch (aPath.mAttributeId)
    {
        case Attributes::FlipFlop::Id:
            {
                auto oldValue = content[endpointIndex].flipflop;
                ReturnErrorOnFailure(aDecoder.Decode(content[endpointIndex].flipflop));
                ChipLogProgress(Zcl,"Write Attribute flip-flop on endpoint %d index %zu newValue %d oldValue %d", endpoint, endpointIndex, content[endpointIndex].flipflop, oldValue);
            }
            break;
        default:
        break;
    }

    return err;
}

References to your cluster from other files

Other files need to be changed whenever a new cluster is defined. You don't need to change these files if you are just making changes to samplemei cluster.

.github/workflows/tests.yaml implements XML parsing tests for the .xml cluster definitions:

            ./scripts/run_in_build_env.sh \
               "./scripts/py_matter_idl/matter_idl/xml_parser.py \
                 --no-print \
                 --log-level info \
            src/app/zap-templates/zcl/data-model/chip/global-attributes.xml \
            (...)
+           src/app/zap-templates/zcl/data-model/chip/sample-mei-cluster.xml \
            "

scripts/rules.matterlint also performs lint checks:

+load "../src/app/zap-templates/zcl/data-model/chip/sample-mei-cluster.xml";

src/app/common/templates/config-data.yaml controls some of the code generation flags. CommandHandlerInterfaceOnlyClusters ensures some C-style callbacks for command handlers are not automatically called, and thus would need to be defined by your code.

CommandHandlerInterfaceOnlyClusters:
    # List of clusters that are implemented entirely with
    # CommandHandlerInterface and hence do not need generated command dispatch.
    # This uses asUpperCamelCase versions of the cluster name.
    - NetworkCommissioning
    - Scenes
(...)
+   - SampleMei

(...)

src/app/zap-templates/zcl/data-model/all.xml

<?xml version="1.0"?>
<all xmlns:xi="http://www.w3.org/2001/XInclude">
   <xi:include href="chip/access-control-cluster.xml" />
   (...)
+  <xi:include href="chip/sample-mei-cluster.xml" />
</all>

src/app/zap-templates/zcl/zcl-with-test-extensions.json is used by the ZAP GUI. It draws from this list the set of available clusters when a device is being created. This list includes all the MS clusters.

    "xmlFile": [
        "access-control-definitions.xml",
        (...)
+       "sample-mei-cluster.xml",
        (...)
    ],

src/app/zap-templates/zcl/zcl.json is also used by the ZAP GUI. It alternatively draws from this list the set of available clusters when a device is being created. This list shouldn't include the MS clusters, but temporarily it does. Make sure you also define your MS Cluster here.

    "xmlFile": [
        "access-control-definitions.xml",
        (...)
+       "sample-mei-cluster.xml",
        (...)
    ],

src/app/zap_cluster_list.json flags to the build system to include your server cluster source directory (src/app/clusters/sample-mei-server/) to the list of source code to be selectively compiled whenever they exist in your device's data model: the .zap and .matter files. That is also true for client clusters, but those are not implemented in samplemei. If your cluster also implements client logic, make sure to also add it to the ClientDirectorieslist.

{
    "ClientDirectories": {
        "ACCESS_CONTROL_CLUSTER": [],
        (...)
        "SAMPLE_MEI_CLUSTER": [],
        (...)
    },
    "ServerDirectories": {
        "ACCESS_CONTROL_CLUSTER": ["access-control-server"],
        (...)
        "SAMPLE_MEI_CLUSTER": ["sample-mei-server"],
        (...)
    }
}

src/controller/data_model/BUILD.gn needs to include Android JNI generation for the new cluster:

if (current_os == "android" || matter_enable_java_compilation) {
  config("java-build-config") {
    if (matter_enable_java_compilation) {
      include_dirs = java_matter_controller_dependent_paths
    }
  }
  chip_codegen("java-jni-generate") {
    input = "controller-clusters.matter"
    generator = "java-jni"
    outputs = [
      "jni/AccessControlClient-InvokeSubscribeImpl.cpp",
      (...)
+     "jni/SampleMeiClient-InvokeSubscribeImpl.cpp",
+     "jni/SampleMeiClient-ReadImpl.cpp",
      (...)
    ]

    deps = [
      ":data_model",
      "${chip_root}/src/platform:platform_buildconfig",
    ]
    public_configs = [ ":java-build-config" ]
  }

(...)

Regenerate all code

Once all changes have been made, regenerate all code by running:

$ scripts/tools/zap_regen_all.py

Reformat your changes to adhere to the Matter project coding style by running:

$ scripts/helpers/restyle-diff.sh

Add the MS Cluster to controller-clusters.zap

From the Matter root folder to open the ZAP GUI run:

$ cd <chip_root>
$ scripts/tools/zap/run_zaptool.sh src/controller/data_model/controller-clusters.zap

Once the UI is loaded, click Endpoint - 0:

image

Search for your cluster by entering its name in the Search Clusters field. Once located, select Not Enabled and change it to Client.

image

Click the to configure the cluster:

image

In the Attributes tab, ensure all attributes are enabled:

image

In the Commands tab ensure all commands are enabled:

image

Leave the cluster configuration window by clicking on the button.

Save your the controller configuration by selecting File > Save or use Ctrl+S.

Exit ZAP by selecting File > Exit or use Ctrl+Q.

Regenerate for consistency the ZAP and codegen files with:

$ scripts/tools/zap_regen_all.py