| # How to Add a New Simulated Device | |
| This guide explains how to implement a simulated Matter device in the | |
| `all-devices-app`. | |
| We will walk through creating a device named `MySensor` (`--device my-sensor`), | |
| implementing its data model, registering it in the device factory, expanding | |
| build targets, and executing certification tests. | |
| --- | |
| ## Step 0: Research Specification Requirements | |
| 1. **Matter Device Library Specification (Primary)**: Reference the latest CSA | |
| Matter spec for authoritative next-version requirements. | |
| 2. **Local XML Reference (Secondary)**: Cross-reference | |
| `data_model/{version}/device_types/{DeviceType}.xml` for quick lookup. Note: | |
| XMLs are static snapshots of a certifiable spec version; they may need | |
| augmentation for next-version features. | |
| 3. **Endpoint Support Constraint**: Endpoints must contain at least one | |
| functional, domain-specific server cluster. 0-cluster endpoints are | |
| disallowed. | |
| 4. **Identify Key Requirements**: | |
| - **Device Type ID**: Hex identifier (e.g., `0x0302` for Temperature | |
| Sensor). | |
| - **Mandatory & Optional Clusters**: Server clusters required to comply. | |
| - **Endpoint Constraints / Composition Rules**: Tree composition rules. | |
| 5. **AI Agent Assistant**: If using an AI coding agent, it can use the | |
| [spec access skill](../../../.agents/skills/matter-specification-access/SKILL.md) | |
| to retrieve relevant sections of the specification. | |
| --- | |
| ## Step 0.5: Test Plan & Test Discovery | |
| 1. **Locate Test Plans**: Reference the private `chip-test-plans` repository | |
| (accessible to CSA members) to understand cluster verification steps. | |
| 2. **Locate Integration Tests**: Search `src/python_testing/` for | |
| `TC_{CLUSTER}_*.py` or `src/app/tests/suites/` for YAML tests corresponding | |
| to the implemented clusters. | |
| 3. **Plan CI Runs**: Plan to enable a subset of cluster tests in CI. This is | |
| done by adding run configurations to the `=== BEGIN CI TEST ARGUMENTS ===` | |
| block of the python script or to the YAML test suites, passing | |
| `--device {your-device}`. | |
| --- | |
| ## Architectural Best Practices | |
| When implementing your device, keep these key architectural patterns in mind to | |
| make your code compile-time safe, highly reusable across different platforms, | |
| and easy to test: | |
| 1. **Abstract Hardware Interactions**: | |
| - Build your core device class so it doesn't depend on specific RTOS or | |
| platform-specific libraries. | |
| - Abstract hardware-specific actions (like toggling LED pins or playing | |
| sound) behind pure virtual delegate interfaces. This allows contributors | |
| to reuse your exact device behavior on their specific target boards. | |
| 2. **Prefer References (`Delegate &`) for Mandatory Delegates**: | |
| - If a delegate is mandatory for a cluster's core functionality (e.g., | |
| `OnOffDelegate` for a light, or `IdentifyDelegate` for a device), always | |
| require it as a **C++ reference (`&`)** in the constructor. | |
| - This enforces at compile-time that a valid, functional handler must be | |
| provided, completely eliminating the risk of runtime null-pointer crashes | |
| or devices that silently ignore commands. | |
| - Use optional pointers (`Delegate *` defaulting to `nullptr`) only for | |
| clusters that are optional based on feature flags or configuration. | |
| 3. **Expose Public Cluster Getters**: | |
| - Always provide public C++ getter methods (e.g., `OnOffCluster()`, | |
| `IdentifyCluster()`) to expose the underlying cluster instances. | |
| - This allows local application code (such as physical button drivers, | |
| platform leaf classes, shell commands, or Out-of-Band accessors) to | |
| programmatically read, write, or modify the device state. | |
| 4. **Symmetrical Logging Mocks (`impl/` subfolder)**: | |
| - For every base device class, implement a corresponding | |
| "logging-by-default" mock subclass in a dedicated `impl/` subfolder (e.g., | |
| `LoggingMySensor`). | |
| - Use the **self-delegate pattern** where the mock subclass inherits from | |
| both the base device class and the delegate interfaces, implements the | |
| callbacks to log to the console, and passes `*this` to the base | |
| constructor. | |
| - This makes the mock completely self-contained and ready to be instantiated | |
| by `DeviceFactory` or unit tests without requiring external wiring. | |
| 5. **Spec-Pure Directory Layout & Extracted Capabilities**: | |
| - Keep the root of the `device/types/` directory pure: it should contain | |
| _only_ real, spec-defined Matter Device Types (like `dimmable-light`, | |
| `fan`, `air-purifier`). | |
| - **Avoid non-spec inheritance** (such as making a plug-in unit inherit from | |
| a light) just to reuse code. | |
| - If multiple device types share a common capability (like dimming or air | |
| circulation), extract it into an abstract capability base class under | |
| `device/capabilities/<capability-name>/` (e.g., | |
| `device/capabilities/dimmable-load/`). Concrete leaf devices then inherit | |
| publicly from this capability base. | |
| 6. **Registration & Teardown Ordering**: | |
| - **Registration order**: Call `RegisterDescriptor()` first (to construct | |
| the descriptor cluster and initialize endpoint metadata), create and add | |
| all domain and optional clusters via `provider.AddCluster()`, and finally | |
| register the endpoint via `provider.AddEndpoint(mEndpointRegistration)`. | |
| Once started, `CodeDrivenDataModelProvider` rejects adding clusters to an | |
| already-registered endpoint (`CHIP_ERROR_INCORRECT_STATE`). | |
| - **Teardown order**: Call `UnregisterDescriptor(provider)` **first**, | |
| before removing or destroying any domain or optional clusters. | |
| `UnregisterDescriptor()` removes the endpoint from the provider via | |
| `provider.RemoveEndpoint()`. Once started, `CodeDrivenDataModelProvider` | |
| prevents non-atomic endpoint modifications and returns | |
| `CHIP_ERROR_INCORRECT_STATE` if `provider.RemoveCluster()` is called while | |
| the endpoint is still registered. | |
| --- | |
| ### The Header (`MySensor.h`) | |
| Derive your class from `SingleEndpoint` (or `DeviceInterface` if managing | |
| composite / multi-endpoint devices). Require all mandatory delegates as | |
| references in the constructor and declare public getters to expose the | |
| underlying clusters: | |
| ```cpp | |
| #pragma once | |
| #include <app/clusters/identify-server/IdentifyCluster.h> | |
| #include <app/clusters/my-sensor-server/MySensorServerCluster.h> // Example code-driven cluster | |
| #include <data-model-providers/codedriven/CodeDrivenDataModelProvider.h> | |
| #include <device/api/SingleEndpoint.h> | |
| #include <lib/support/TimerDelegate.h> | |
| namespace chip::app { | |
| class MySensor : public SingleEndpoint | |
| { | |
| public: | |
| MySensor(TimerDelegate & timerDelegate, Clusters::IdentifyDelegate & identifyDelegate); | |
| ~MySensor() override = default; | |
| // DeviceInterface pure virtual lifecycle hooks | |
| CHIP_ERROR Register(chip::EndpointId endpoint, CodeDrivenDataModelProvider & provider, | |
| EndpointComposition composition = {}) override; | |
| void Unregister(CodeDrivenDataModelProvider & provider) override; | |
| // Public cluster getters for programmatic control | |
| Clusters::IdentifyCluster & IdentifyCluster() { return mIdentifyCluster.Cluster(); } | |
| Clusters::MySensorCluster & MySensorCluster() { return mMySensorCluster.Cluster(); } | |
| protected: | |
| // Optional cluster extension hooks for subclasses (impl/ or hardware targets) | |
| virtual CHIP_ERROR RegisterAdditionalClusters(chip::EndpointId endpoint, CodeDrivenDataModelProvider & provider) | |
| { | |
| return CHIP_NO_ERROR; | |
| } | |
| virtual void UnregisterAdditionalClusters(CodeDrivenDataModelProvider & provider) {} | |
| private: | |
| TimerDelegate & mTimerDelegate; | |
| Clusters::IdentifyDelegate & mIdentifyDelegate; | |
| LazyRegisteredServerCluster<Clusters::IdentifyCluster> mIdentifyCluster; | |
| LazyRegisteredServerCluster<Clusters::MySensorCluster> mMySensorCluster; | |
| }; | |
| } // namespace chip::app | |
| ``` | |
| ### The Source (`MySensor.cpp`) | |
| In `Register()`, wire up mandatory clusters and invoke | |
| `RegisterAdditionalClusters()` **before** calling `provider.AddEndpoint()` so | |
| subclasses can attach optional clusters within the atomic registration | |
| transaction: | |
| ```cpp | |
| #include "MySensor.h" | |
| #include <devices/Types.h> | |
| #include <lib/support/logging/CHIPLogging.h> | |
| using namespace chip::app::Clusters; | |
| namespace chip::app { | |
| MySensor::MySensor(TimerDelegate & timerDelegate, Clusters::IdentifyDelegate & identifyDelegate) : | |
| SingleEndpoint(Span<const DataModel::DeviceTypeEntry>(&Device::Type::kMySensor, 1)), | |
| mTimerDelegate(timerDelegate), mIdentifyDelegate(identifyDelegate) | |
| {} | |
| CHIP_ERROR MySensor::Register(chip::EndpointId endpoint, CodeDrivenDataModelProvider & provider, EndpointComposition composition) | |
| { | |
| VerifyOrReturnError(mEndpointId == kInvalidEndpointId, CHIP_ERROR_INCORRECT_STATE); | |
| DeviceRegistrationTransaction transaction(*this, provider); | |
| ReturnErrorOnFailure(RegisterDescriptor(endpoint, provider, composition)); | |
| // Wire up the mandatory identify delegate | |
| mIdentifyCluster.Create(IdentifyCluster::Config(endpoint, mTimerDelegate).WithDelegate(&mIdentifyDelegate)); | |
| ReturnErrorOnFailure(provider.AddCluster(mIdentifyCluster.Registration())); | |
| mMySensorCluster.Create(endpoint); | |
| ReturnErrorOnFailure(provider.AddCluster(mMySensorCluster.Registration())); | |
| // Allow subclasses to register optional clusters before committing the endpoint | |
| ReturnErrorOnFailure(RegisterAdditionalClusters(endpoint, provider)); | |
| ReturnErrorOnFailure(provider.AddEndpoint(mEndpointRegistration)); | |
| transaction.Commit(); | |
| return CHIP_NO_ERROR; | |
| } | |
| void MySensor::Unregister(CodeDrivenDataModelProvider & provider) | |
| { | |
| // UnregisterDescriptor MUST be called first to remove the endpoint from the provider. | |
| // Once started, calling provider.RemoveCluster while the endpoint is still registered returns CHIP_ERROR_INCORRECT_STATE. | |
| UnregisterDescriptor(provider); | |
| UnregisterAdditionalClusters(provider); | |
| if (mMySensorCluster.IsConstructed()) | |
| { | |
| LogErrorOnFailure(provider.RemoveCluster(&mMySensorCluster.Cluster())); | |
| mMySensorCluster.Destroy(); | |
| } | |
| if (mIdentifyCluster.IsConstructed()) | |
| { | |
| LogErrorOnFailure(provider.RemoveCluster(&mIdentifyCluster.Cluster())); | |
| mIdentifyCluster.Destroy(); | |
| } | |
| } | |
| } // namespace chip::app | |
| ``` | |
| ### The Symmetrical Logging Mock (`impl/LoggingMySensor.h` & `.cpp`) | |
| To provide a self-contained simulator variant ready for `DeviceFactory`, | |
| implement the logging subclass under `impl/`. | |
| Note the inheritance order: delegate base classes | |
| (`private Clusters::IdentifyDelegate`) are listed **before** `public MySensor` | |
| so the delegate subobject is constructed before being passed as `*this` to | |
| `MySensor`: | |
| #### Header (`impl/LoggingMySensor.h`) | |
| ```cpp | |
| #pragma once | |
| #include <device/types/my-sensor/MySensor.h> | |
| #include <lib/support/logging/CHIPLogging.h> | |
| namespace chip::app { | |
| class LoggingMySensor : private Clusters::IdentifyDelegate, public MySensor | |
| { | |
| public: | |
| explicit LoggingMySensor(TimerDelegate & timerDelegate) : | |
| MySensor(timerDelegate, *this) | |
| {} | |
| ~LoggingMySensor() override = default; | |
| // IdentifyDelegate implementation | |
| void OnIdentifyStart(Clusters::IdentifyCluster & cluster) override | |
| { | |
| ChipLogProgress(DeviceLayer, "MySensor: OnIdentifyStart"); | |
| } | |
| void OnIdentifyStop(Clusters::IdentifyCluster & cluster) override | |
| { | |
| ChipLogProgress(DeviceLayer, "MySensor: OnIdentifyStop"); | |
| } | |
| void OnTriggerEffect(Clusters::IdentifyCluster & cluster) override | |
| { | |
| ChipLogProgress(DeviceLayer, "MySensor: OnTriggerEffect"); | |
| } | |
| bool IsTriggerEffectEnabled() const override { return true; } | |
| }; | |
| } // namespace chip::app | |
| ``` | |
| ### The GN Build (`BUILD.gn`) | |
| Create `all-devices-common/device/types/my-sensor/BUILD.gn` to define your | |
| standalone source set: | |
| ```text | |
| # Copyright (c) 2026 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. | |
| import("//build_overrides/chip.gni") | |
| source_set("my-sensor") { | |
| sources = [ | |
| "MySensor.cpp", | |
| "MySensor.h", | |
| ] | |
| public_deps = [ | |
| "${chip_root}/examples/all-devices-app/all-devices-common/device/api:single-endpoint-device", | |
| "${chip_root}/src/app/clusters/identify-server", | |
| # Add public_deps for your specific cluster servers here | |
| "${chip_root}/src/data-model-providers/codedriven", | |
| "${chip_root}/src/lib/core", | |
| ] | |
| } | |
| ``` | |
| --- | |
| ## Step 2: Register the Device Type in `DeviceFactory` | |
| To enable runtime initialization via the `--device my-sensor` CLI flag, register | |
| your self-contained **logging mock** in | |
| `all-devices-common/device-factory/DeviceFactory.h`. | |
| 1. **Include your Logging Mock Header** (keep the include list sorted | |
| alphabetically): | |
| ```cpp | |
| #include <device/types/my-sensor/impl/LoggingMySensor.h> | |
| ``` | |
| 2. **Register the Creator** inside the `DeviceFactory` constructor: | |
| ```cpp | |
| if constexpr (ALL_DEVICES_ENABLE_MY_SENSOR) | |
| { | |
| RegisterCreator("my-sensor", [this]() { | |
| VerifyOrDie(mContext.has_value()); | |
| return MakeDevice<LoggingMySensor>(mContext->timerDelegate); | |
| }); | |
| } | |
| ``` | |
| _Note: We instantiate the Logging mock version in the factory so it runs | |
| completely out-of-the-box with self-contained console logging, keeping the | |
| platform main clean._ | |
| 3. **(Optional) Register Out-of-Band & Named Pipe Support**: | |
| - For simulated sensor triggers or external control, implement | |
| `OOBAccessors.h/.cpp` (platform-neutral) and `NamedPipeTranslators.h/.cpp` | |
| (POSIX-only). | |
| - Include `<device/types/<name>/OOBAccessors.h>` in | |
| `all-devices-common/oob-accessors/OOBAccessorHook.h` and | |
| `<device/types/<name>/NamedPipeTranslators.h>` in | |
| `posix/named_pipe/Hook.h` so the SFINAE detection hooks | |
| (`HasOOBAccessors`, `HasNamedPipeTranslators`) discover the registration | |
| overloads at compile-time. | |
| - See [Out-of-Band Control Architecture](design/out_of_band_control.md) for | |
| full details and examples. | |
| --- | |
| ## Step 3: Register Files in Build Configurations | |
| Next, register your implementation files across our core build scripts. | |
| > [!NOTE] To keep project lists clean, our build systems enforce alphabetical | |
| > sorting. Please insert your new device entries in alphabetical order to ensure | |
| > automated CI formatting checks pass successfully. | |
| ### 1. Macro Template (`all-devices-common/device-factory/enabled_devices_config.h.in`) | |
| Add the compile-time CMake hook: | |
| ```text | |
| #cmakedefine01 ALL_DEVICES_ENABLE_MY_SENSOR | |
| ``` | |
| ### 2. GN Configuration (`all-devices-common/device-factory/enabled_devices.gni`) | |
| Add the device entry to `_available_devices` (keep sorted): | |
| ```text | |
| _available_devices = [ | |
| ... | |
| [ | |
| "my-sensor", | |
| "MY_SENSOR", | |
| ], | |
| ... | |
| ] | |
| ``` | |
| ### 3. CMake Configuration (`all-devices-common/device-factory/enabled_devices.cmake`) | |
| Add your `.cpp` source file to `ALL_DEVICES_DEVICE_SOURCES` (keep sorted): | |
| ```cmake | |
| set(ALL_DEVICES_DEVICE_SOURCES | |
| # keep-sorted: start | |
| ... | |
| "${ALL_DEVICES_COMMON_DIR}/device/types/my-sensor/MySensor.cpp" | |
| ... | |
| # keep-sorted: end | |
| ) | |
| ``` | |
| Also add your key to the activation loop in the same file (keep sorted): | |
| ```cmake | |
| foreach(_key | |
| # keep-sorted: start | |
| ... | |
| my-sensor | |
| ... | |
| # keep-sorted: end | |
| ) | |
| ``` | |
| Also add a `bool` entry to `examples/all-devices-app/zephyr/Kconfig.devices` | |
| inside the `menu "Matter device types"` block (keep sorted): | |
| ```kconfig | |
| config ALL_DEVICES_DEVICE_MY_SENSOR | |
| bool "My sensor" | |
| ``` | |
| ### 4. Device Factory Dependency (`all-devices-common/device-factory/BUILD.gn`) | |
| Add your new device target to the `public_deps` of `device-factory` (keep | |
| sorted): | |
| ```text | |
| public_deps = [ | |
| ... | |
| "${chip_root}/examples/all-devices-app/all-devices-common/device/types/my-sensor", | |
| ... | |
| ] | |
| ``` | |
| ### 5. Platform Executable Dependencies (`BUILD.gn` files) | |
| Add the target dependency to the relevant platform executable targets where | |
| `all-devices-app` is built (e.g., `examples/all-devices-app/posix/BUILD.gn`, and | |
| embedded platform builds such as Silicon Labs or ESP32): | |
| ```text | |
| "${chip_root}/examples/all-devices-app/all-devices-common/device/types/my-sensor", | |
| ``` | |
| --- | |
| ## Step 4: Expand Build Targets & Update Snapshots | |
| To support building automated variants of the application with your device | |
| enabled by default, expand the Python build targets. | |
| ### 1. `targets.py` | |
| Open `scripts/build/build/targets.py` and append your device string to | |
| `_ALL_DEVICES_APP_DEVICES` (keep sorted): | |
| ```python | |
| _ALL_DEVICES_APP_DEVICES = [ | |
| # keep-sorted: start | |
| ... | |
| 'my-sensor', | |
| ... | |
| ] | |
| ``` | |
| ### 2. Update Build Test Golden Snapshots | |
| Our automated PR workflows verify that build variants match a golden snapshot. | |
| Updating `targets.py` will temporarily cause `scripts/build/test.py` to fail | |
| until you regenerate this golden file: | |
| 1. Run the build script tests within the activated build environment (this run | |
| will fail but generate an actual output file): | |
| ```bash | |
| scripts/run_in_build_env.sh "python3 scripts/build/test.py" | |
| ``` | |
| 2. Overwrite the golden snapshot file with the newly generated actual file: | |
| ```bash | |
| cp all_targets_linux_x64.txt.actual scripts/build/testdata/all_targets_linux_x64.txt | |
| rm all_targets_linux_x64.txt.actual | |
| ``` | |
| 3. Re-run the tests to confirm they now pass successfully (`OK`): | |
| ```bash | |
| scripts/run_in_build_env.sh "python3 scripts/build/test.py" | |
| ``` | |
| --- | |
| ## Step 5: End-to-End Verification & Certification Testing | |
| Fully validate your new device integration using end-to-end testing tools. | |
| ### 1. Compile the Application | |
| Build the executable using the standard Python build scripts: | |
| ```bash | |
| source scripts/activate.sh | |
| ./scripts/build/build_examples.py --target linux-x64-all-devices-clang build | |
| ``` | |
| ### 2. Execute Basic Composition Verification | |
| Activate the automated test virtual environment and run the core device basic | |
| composition certification test. This instructs the binary to dynamically spawn | |
| your new device on a child endpoint: | |
| ```bash | |
| source out/venv/bin/activate | |
| ./scripts/tests/run_python_test.py \ | |
| --factory-reset \ | |
| --app ./out/linux-x64-all-devices-clang/all-devices-app \ | |
| --app-args "--device my-sensor:2 --discriminator 1234 --KVS kvs1" \ | |
| --script src/python_testing/TC_DeviceBasicComposition.py \ | |
| --script-args "--storage-path admin_storage.json --commissioning-method on-network --discriminator 1234 --passcode 20202021" | |
| ``` | |
| Confirm that all test cases execute and pass successfully. | |
| ### 3. Interactive Testing with `chip-tool` | |
| For manual interactive validation, execute `chip-tool` (refer to the | |
| `chip-tool-testing` skill for detailed commissioning setups): | |
| 1. Compile `chip-tool` from source: | |
| ```bash | |
| source scripts/activate.sh | |
| ./scripts/build/build_examples.py --target linux-x64-chip-tool-clang build | |
| ``` | |
| 2. Start your application configured with your new device: | |
| ```bash | |
| ./out/linux-x64-all-devices-clang/all-devices-app --device my-sensor | |
| ``` | |
| 3. Commission the app and execute interaction model read/write commands or | |
| custom cluster commands against your new device endpoint to verify functional | |
| data model operations. | |
| ### 4. Run Dedicated Cluster Certification Suites | |
| - Identify existing Python integration tests in `src/python_testing/` or YAML | |
| test cases in `src/app/tests/suites/` that match the specific clusters | |
| implemented by your device. | |
| - Execute those dedicated test scripts via `run_python_test.py` to guarantee | |
| full compliance with the Matter Specification. | |
| --- | |
| ## Step 6: Update Implementation Status Documentation | |
| After implementing and registering your device, manually update the tracking | |
| documentation: | |
| 1. **`supported_device_types.md`**: Move your device type entry from the | |
| **Unimplemented** table to the **Implemented** table, and increment the total | |
| count. | |
| 2. **`supported_clusters.md`**: If your device introduced support for a new | |
| cluster, update its **Used in All-Devices** status to `Yes` and list your | |
| device type in the **Notes/Devices** column. | |
| 3. **`README.md`**: Update the **Supported Devices** list and the `--device` CLI | |
| help options example block under the **Running the Application** section. |