Adding support for a new Matter device type

The Matter Bridge application supports bridging only a few Matter device types due to practical reasons. However, you can select any of the available Matter device types and add support to it in the application.

You will need to implement the Matter Bridged Device and Bridged Device Data Provider roles based on the Matter Bridge architecture for the newly added Matter device type. The Matter Bridge application supports simulated and Bluetooth LE bridged device configurations. In this guide, the simulated provider example is presented, but the process is similar for the Bluetooth LE provider as well.

The following steps show how to add support for a new Matter device type, using the Pressure Sensor device type as an example.

  1. Enable the Pressure Measurement cluster for the endpoint 2 in the src/bridge.zap file and re-generate the files located in the src/zap-generated directory.

    To learn how to modify the .zap file and re-generate the zap-generated directory, see the Edit clusters using the ZAP tool section in the Adding clusters to Matter application user guide.

  2. Implement the Matter Bridged Device role.

    1. Create the pressure_sensor.cpp and pressure_sensor.h files in the src/bridged_device_types directory.

    2. Open the nrf/samples/matter/common/src/bridge/matter_bridged_device.h header file and find the MatterBridgedDevice class constructor. Note the constructor signature, it will be used in the child class implemented in the next steps.

    3. Add a new PressureSensorDevice class inheriting MatterBridgedDevice, and implement its constructor in the pressure_sensor.cpp and pressure_sensor.h files.

      • pressure_sensor.h

        #pragma once
        
        #include "matter_bridged_device.h"
        
        class PressureSensorDevice : public Nrf::MatterBridgedDevice {
        public:
        
        PressureSensorDevice(const char *nodeLabel);
        static constexpr uint16_t kPressureSensorDeviceTypeId = 0x0305;
        
        };
        
      • pressure_sensor.cpp

        #include "pressure_sensor.h"
        
        PressureSensorDevice::PressureSensorDevice(const char *nodeLabel) : MatterBridgedDevice(nodeLabel) {}
        
    4. Declare all clusters that are mandatory for the Pressure Sensor device type, according to the Matter device library specification, and fill the appropriate MatterBridgedDevice class fields in the PressureSensorDevice class constructor.

      The Pressure Sensor device requires the Descriptor, Bridged Device Basic Information and Identify clusters, which can be declared using helper macros from the nrf/samples/matter/common/src/bridge/matter_bridged_device.h header file, and the Pressure Measurement cluster, which has to be defined in the application. Edit the pressure_sensor.cpp file as follows:

      • Add:

        namespace
        {
        DESCRIPTOR_CLUSTER_ATTRIBUTES(descriptorAttrs);
        BRIDGED_DEVICE_BASIC_INFORMATION_CLUSTER_ATTRIBUTES(bridgedDeviceBasicAttrs);
        IDENTIFY_CLUSTER_ATTRIBUTES(identifyAttrs);
        }; /* namespace */
        using namespace ::chip;
        using namespace ::chip::app;
        using namespace Nrf;
        
        DECLARE_DYNAMIC_ATTRIBUTE_LIST_BEGIN(pressureSensorAttrs)
        DECLARE_DYNAMIC_ATTRIBUTE(Clusters::PressureMeasurement::Attributes::MeasuredValue::Id, INT16S, 2, 0),
           DECLARE_DYNAMIC_ATTRIBUTE(Clusters::PressureMeasurement::Attributes::MinMeasuredValue::Id, INT16S, 2,
                       0),
           DECLARE_DYNAMIC_ATTRIBUTE(Clusters::PressureMeasurement::Attributes::MaxMeasuredValue::Id, INT16S, 2,
                       0),
           DECLARE_DYNAMIC_ATTRIBUTE(Clusters::PressureMeasurement::Attributes::FeatureMap::Id, BITMAP32, 4, 0),
           DECLARE_DYNAMIC_ATTRIBUTE_LIST_END();
        
        DECLARE_DYNAMIC_CLUSTER_LIST_BEGIN(bridgedPressureClusters)
        DECLARE_DYNAMIC_CLUSTER(Clusters::PressureMeasurement::Id, pressureSensorAttrs, ZAP_CLUSTER_MASK(SERVER), nullptr, nullptr),
           DECLARE_DYNAMIC_CLUSTER(Clusters::Descriptor::Id, descriptorAttrs, ZAP_CLUSTER_MASK(SERVER), nullptr, nullptr),
           DECLARE_DYNAMIC_CLUSTER(Clusters::BridgedDeviceBasicInformation::Id, bridgedDeviceBasicAttrs, ZAP_CLUSTER_MASK(SERVER), nullptr, nullptr),
           DECLARE_DYNAMIC_CLUSTER(Clusters::Identify::Id, identifyAttrs, ZAP_CLUSTER_MASK(SERVER), sIdentifyIncomingCommands,
                       nullptr) DECLARE_DYNAMIC_CLUSTER_LIST_END;
        
        DECLARE_DYNAMIC_ENDPOINT(bridgedPressureEndpoint, bridgedPressureClusters);
        
        static constexpr uint8_t kBridgedPressureEndpointVersion = 2;
        
        static constexpr EmberAfDeviceType kBridgedPressureDeviceTypes[] = {
           { static_cast<chip::DeviceTypeId>(PressureSensorDevice::kPressureSensorDeviceTypeId),
           kBridgedPressureEndpointVersion },
           { static_cast<chip::DeviceTypeId>(MatterBridgedDevice::DeviceType::BridgedNode),
           MatterBridgedDevice::kDefaultDynamicEndpointVersion }
        };
        
        static constexpr uint8_t kPressureDataVersionSize = ArraySize(bridgedPressureClusters);
        
      • Modify the constructor:

        PressureSensorDevice::PressureSensorDevice(const char *nodeLabel) : MatterBridgedDevice(nodeLabel)
        {
              mDataVersionSize = kPressureDataVersionSize;
              mEp = &bridgedPressureEndpoint;
              mDeviceTypeList = kBridgedPressureDeviceTypes;
              mDeviceTypeListSize = ARRAY_SIZE(kBridgedPressureDeviceTypes);
              mDataVersion = static_cast<DataVersion *>(chip::Platform::MemoryAlloc(sizeof(DataVersion) * mDataVersionSize));
        }
        
    5. Open the nrf/samples/matter/common/src/bridge/matter_bridged_device.h header file again to see which methods of the MatterBridgedDevice class are purely virtual (assigned with =0) and have to be overridden by the PressureSensorDevice class.

    6. Edit the PressureSensorDevice class in the pressure_sensor.h header file to declare the required methods as follows:

      uint16_t GetDeviceType() const override;
      
      CHIP_ERROR HandleRead(chip::ClusterId clusterId, chip::AttributeId attributeId, uint8_t *buffer,
                  uint16_t maxReadLength) override;
      CHIP_ERROR HandleWrite(chip::ClusterId clusterId, chip::AttributeId attributeId, uint8_t *buffer) override;
      CHIP_ERROR HandleAttributeChange(chip::ClusterId clusterId, chip::AttributeId attributeId, void *data,
                      size_t dataSize) override;
      
    7. Implement the body of the GetDeviceType() method so that it can return the device type ID for the Pressure Sensor device type, which is equal to 0x0305. To check the device type ID for specific type of device, see Matter Device Library Specification.

      Edit the pressure_sensor.cpp file as follows:

      uint16_t PressureSensorDevice::GetDeviceType() const {
         return PressureSensorDevice::kPressureSensorDeviceTypeId;
      }
      
    8. Implement the body of the HandleRead() method to handle reading data operations for all supported attributes.

      The read operations for the Descriptor, Bridged Device Basic Information and Identify clusters, which are common to all devices, are handled in a common bridge module. The read operations for the Pressure Measurement cluster are the only ones to that need to be handled in the application.

      To provide support for reading attributes for the Pressure Sensor device, edit the pressure_sensor.h and pressure_sensor.cpp files as follows:

      • pressure_sensor.h, PressureSensorDevice class

        int16_t GetMeasuredValue() { return mMeasuredValue; }
        int16_t GetMinMeasuredValue() { return 95; }
        int16_t GetMaxMeasuredValue() { return 101; }
        uint16_t GetPressureMeasurementClusterRevision() { return 3; }
        uint32_t GetPressureMeasurementFeatureMap() { return 0; }
        
        CHIP_ERROR HandleReadPressureMeasurement(chip::AttributeId attributeId, uint8_t *buffer, uint16_t maxReadLength);
        uint16_t mMeasuredValue = 0;
        
      • pressure_sensor.cpp

        CHIP_ERROR PressureSensorDevice::HandleRead(ClusterId clusterId, AttributeId attributeId, uint8_t *buffer, uint16_t maxReadLength) {
           switch (clusterId) {
           case Clusters::PressureMeasurement::Id:
              return HandleReadPressureMeasurement(attributeId, buffer, maxReadLength);
           default:
              return CHIP_ERROR_INVALID_ARGUMENT;
           }
        }
        CHIP_ERROR PressureSensorDevice::HandleReadPressureMeasurement(AttributeId attributeId, uint8_t *buffer, uint16_t maxReadLength) {
           switch (attributeId) {
           case Clusters::PressureMeasurement::Attributes::MeasuredValue::Id: {
              int16_t value = GetMeasuredValue();
              return CopyAttribute(&value, sizeof(value), buffer, maxReadLength);
           }
           case Clusters::PressureMeasurement::Attributes::MinMeasuredValue::Id: {
              int16_t value = GetMinMeasuredValue();
              return CopyAttribute(&value, sizeof(value), buffer, maxReadLength);
           }
           case Clusters::PressureMeasurement::Attributes::MaxMeasuredValue::Id: {
              int16_t value = GetMaxMeasuredValue();
              return CopyAttribute(&value, sizeof(value), buffer, maxReadLength);
           }
           case Clusters::PressureMeasurement::Attributes::ClusterRevision::Id: {
              uint16_t clusterRevision = GetPressureMeasurementClusterRevision();
              return CopyAttribute(&clusterRevision, sizeof(clusterRevision), buffer, maxReadLength);
           }
           case Clusters::PressureMeasurement::Attributes::FeatureMap::Id: {
              uint32_t featureMap = GetPressureMeasurementFeatureMap();
              return CopyAttribute(&featureMap, sizeof(featureMap), buffer, maxReadLength);
           }
           default:
              return CHIP_ERROR_INVALID_ARGUMENT;
           }
        }
        
    9. Implement the body of the HandleWrite() method, which handles write data operations for all supported attributes. In this case, there is no attribute supporting write operations, so edit the pressure_sensor.cpp file as follows:

      CHIP_ERROR PressureSensorDevice::HandleWrite(chip::ClusterId clusterId, chip::AttributeId attributeId, uint8_t *buffer) {
         return CHIP_ERROR_UNSUPPORTED_CHIP_FEATURE;
      }
      
    10. Implement the body of the HandleAttributeChange() method. This will be called by the Bridge Manager to notify that data was changed by the Bridged Device Data Provider and the local state should be updated.

      Edit the pressure_sensor.h and pressure_sensor.cpp files as follows:

      • pressure_sensor.h

        void SetMeasuredValue(int16_t value) { mMeasuredValue = value; }
        
      • pressure_sensor.cpp

        CHIP_ERROR PressureSensorDevice::HandleAttributeChange(chip::ClusterId clusterId, chip::AttributeId attributeId, void *data, size_t dataSize)
        {
           CHIP_ERROR err = CHIP_NO_ERROR;
           if (!data) {
              return CHIP_ERROR_INVALID_ARGUMENT;
           }
           switch (clusterId) {
           case Clusters::BridgedDeviceBasicInformation::Id:
              return HandleWriteDeviceBasicInformation(clusterId, attributeId, data, dataSize);
           case Clusters::Identify::Id:
              return HandleWriteIdentify(attributeId, data, dataSize);
           case Clusters::PressureMeasurement::Id: {
              switch (attributeId) {
              case Clusters::PressureMeasurement::Attributes::MeasuredValue::Id: {
                 int16_t value;
        
                 err = CopyAttribute(data, dataSize, &value, sizeof(value));
        
                 if (err != CHIP_NO_ERROR) {
                    return err;
                 }
        
                 SetMeasuredValue(value);
        
                 break;
              }
              default:
                 return CHIP_ERROR_INVALID_ARGUMENT;
              }
              break;
           }
           default:
              return CHIP_ERROR_INVALID_ARGUMENT;
           }
        
           return err;
        }
        
  3. Implement the Bridged Device Data Provider role.

    1. Create the simulated_pressure_sensor_data_provider.cpp and simulated_pressure_sensor_data_provider.h files in the src/simulated_providers directory.

    2. Open the nrf/samples/matter/common/src/bridge/bridged_device_data_provider.h header file and find the BridgedDeviceDataProvider class constructor. Note the constructor signature, it will be used in the child class implemented in the next steps.

    3. Add a new SimulatedPressureSensorDataProvider class inheriting BridgedDeviceDataProvider, and implement its constructor in the simulated_pressure_sensor_data_provider.h header file.

      #pragma once
      
      #include "bridged_device_data_provider.h"
      
      #include <zephyr/kernel.h>
      
      class SimulatedPressureSensorDataProvider : public Nrf::BridgedDeviceDataProvider {
      public:
         SimulatedPressureSensorDataProvider(UpdateAttributeCallback updateCallback, InvokeCommandCallback commandCallback) : Nrf::BridgedDeviceDataProvider(updateCallback, commandCallback) {}
         ~SimulatedPressureSensorDataProvider() {}
      };
      
    4. Open the nrf/samples/matter/common/src/bridge/bridged_device_data_provider.h header file again to see which methods of the BridgedDeviceDataProvider class are purely virtual (assigned with =0) and have to be overridden by the SimulatedPressureSensorDataProvider class.

    5. Edit the SimulatedPressureSensorDataProvider class in the simulated_pressure_sensor_data_provider.h header file to declare the required methods as follows:

      void Init() override;
      void NotifyUpdateState(chip::ClusterId clusterId, chip::AttributeId attributeId, void *data, size_t dataSize) override;
      CHIP_ERROR UpdateState(chip::ClusterId clusterId, chip::AttributeId attributeId, uint8_t *buffer) override;
      
    6. Implement the body of the Init() method so that it can prepare the data provider for further operation. In this case, the pressure measurements will be simulated by changing data in a random manner and updating it at fixed time intervals.

      To initialize the timer and perform measurement updates, edit the simulated_pressure_sensor_data_provider.h and simulated_pressure_sensor_data_provider.cpp files as follows:

      • simulated_pressure_sensor_data_provider.h, SimulatedPressureSensorDataProvider class

        static void NotifyAttributeChange(intptr_t context);
        
        static constexpr uint16_t kMeasurementsIntervalMs = 10000;
        static constexpr int16_t kMinRandomPressure = 95;
        static constexpr int16_t kMaxRandomPressure = 101;
        
        static void TimerTimeoutCallback(k_timer *timer);
        k_timer mTimer;
        int16_t mPressure = 0;
        
      • simulated_pressure_sensor_data_provider.cpp

        #include "simulated_pressure_sensor_data_provider.h"
        
        using namespace ::chip;
        using namespace ::chip::app;
        using namespace Nrf;
        
        void SimulatedPressureSensorDataProvider::Init()
        {
           k_timer_init(&mTimer, SimulatedPressureSensorDataProvider::TimerTimeoutCallback, nullptr);
           k_timer_user_data_set(&mTimer, this);
           k_timer_start(&mTimer, K_MSEC(kMeasurementsIntervalMs), K_MSEC(kMeasurementsIntervalMs));
        }
        
        void SimulatedPressureSensorDataProvider::TimerTimeoutCallback(k_timer *timer)
        {
           if (!timer || !timer->user_data) {
              return;
           }
           SimulatedPressureSensorDataProvider *provider = reinterpret_cast<SimulatedPressureSensorDataProvider *>(timer->user_data);
           /* Get some random data to emulate sensor measurements. */
           provider->mPressure = chip::Crypto::GetRandU16() % (kMaxRandomPressure - kMinRandomPressure) + kMinRandomPressure;
           DeviceLayer::PlatformMgr().ScheduleWork(NotifyAttributeChange, reinterpret_cast<intptr_t>(provider));
        }
        
        void SimulatedPressureSensorDataProvider::NotifyAttributeChange(intptr_t context)
        {
           SimulatedPressureSensorDataProvider *provider = reinterpret_cast<SimulatedPressureSensorDataProvider *>(context);
           provider->NotifyUpdateState(Clusters::PressureMeasurement::Id,
                       Clusters::PressureMeasurement::Attributes::MeasuredValue::Id,
                       &provider->mPressure, sizeof(provider->mPressure));
        }
        
    7. Implement the body of the NotifyUpdateState() method that shall be called after every data change related to the Pressure Sensor device. It is used to inform the Bridge Manager and Matter Data Model that an attribute value should be updated.

      To make the method invoke the appropriate callback, edit the simulated_pressure_sensor_data_provider.cpp file as follows:

      void SimulatedPressureSensorDataProvider::NotifyUpdateState(chip::ClusterId clusterId, chip::AttributeId attributeId,
                                 void *data, size_t dataSize)
      {
         if (mUpdateAttributeCallback) {
            mUpdateAttributeCallback(*this, Clusters::PressureMeasurement::Id,
                     Clusters::PressureMeasurement::Attributes::MeasuredValue::Id, data,
                     dataSize);
         }
      }
      
    8. Implement the body of the UpdateState() method. This will be called by the Bridge Manager to inform that data in Matter Data Model was changed and request propagating this information to the end device.

      In this case, there is no attribute supporting write operations and sending data to end device is not required, so edit the simulated_pressure_sensor_data_provider.cpp file as follows:

      CHIP_ERROR SimulatedPressureSensorDataProvider::UpdateState(chip::ClusterId clusterId, chip::AttributeId attributeId,
                                 uint8_t *buffer)
      {
         return CHIP_ERROR_UNSUPPORTED_CHIP_FEATURE;
      }
      
  4. Add the PressureSensorDevice and SimulatedPressureSensorDataProvider implementations created in previous steps to the compilation process. To do that, edit the CMakeLists.txt file as follows:

    target_sources(app PRIVATE
      src/bridged_device_types/pressure_sensor.cpp
      src/simulated_providers/simulated_pressure_sensor_data_provider.cpp
    )
    
  5. Provide allocators for PressureSensorDevice and SimulatedPressureSensorDataProvider object creation. The Matter Bridge application uses a SimulatedBridgedDeviceFactory factory module that creates paired Matter Bridged Device and Bridged Device Data Provider objects matching a specific Matter device type ID.

    To add support for creating the PressureSensorDevice and SimulatedPressureSensorDataProvider objects when the Pressure Sensor device type ID is used, edit the src/simulated_providers/simulated_bridged_device_factory.h and src/simulated_providers/simulated_bridged_device_factory.cpp files as follows:

    • src/simulated_providers/simulated_bridged_device_factory.h

      #include "pressure_sensor.h"
      #include "simulated_pressure_sensor_data_provider.h"
      
    • src/simulated_providers/simulated_bridged_device_factory.cpp, GetBridgedDeviceFactory() method

      { PressureSensorDevice::kPressureSensorDeviceTypeId,
      [checkLabel](const char *nodeLabel) -> Nrf::MatterBridgedDevice * {
         if (!checkLabel(nodeLabel)) {
            return nullptr;
         }
         return chip::Platform::New<PressureSensorDevice>(nodeLabel);
      } },
      
    • src/simulated_providers/simulated_bridged_device_factory.cpp, GetDataProviderFactory() method

      { PressureSensorDevice::kPressureSensorDeviceTypeId,
      [](UpdateAttributeCallback updateClb, InvokeCommandCallback commandClb) {
         return chip::Platform::New<SimulatedPressureSensorDataProvider>(updateClb, commandClb);
      } },
      
  1. Compile the target and test it following the steps from the Matter Bridge application testing section.