blob: 945edb868cb9c350007388492f9fd4e61443c724 [file] [view]
# 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.,
`LoggingMySensorDevice`).
- 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 `devices/` 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.
---
### The Header (`MySensorDevice.h`)
Derive your class from `SingleEndpointDevice` (or `EndpointDevice` if managing
sub-endpoints). 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 <devices/interface/SingleEndpointDevice.h>
#include <lib/support/TimerDelegate.h>
namespace chip::app {
class MySensorDevice : public SingleEndpointDevice
{
public:
MySensorDevice(TimerDelegate & timerDelegate, Clusters::IdentifyDelegate & identifyDelegate);
~MySensorDevice() 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(); }
private:
TimerDelegate & mTimerDelegate;
Clusters::IdentifyDelegate & mIdentifyDelegate;
LazyRegisteredServerCluster<Clusters::IdentifyCluster> mIdentifyCluster;
LazyRegisteredServerCluster<Clusters::MySensorCluster> mMySensorCluster;
};
} // namespace chip::app
```
### The Source (`MySensorDevice.cpp`)
In `Register()`, wire up your mandatory delegates using the `.WithDelegate()`
helper when creating the cluster instances:
```cpp
#include "MySensorDevice.h"
#include <devices/Types.h>
#include <lib/support/logging/CHIPLogging.h>
using namespace chip::app::Clusters;
namespace chip::app {
MySensorDevice::MySensorDevice(TimerDelegate & timerDelegate, Clusters::IdentifyDelegate & identifyDelegate) :
SingleEndpointDevice(Span<const DataModel::DeviceTypeEntry>(&Device::Type::kMySensor, 1)),
mTimerDelegate(timerDelegate), mIdentifyDelegate(identifyDelegate)
{}
CHIP_ERROR MySensorDevice::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()));
ReturnErrorOnFailure(provider.AddEndpoint(mEndpointRegistration));
transaction.Commit();
return CHIP_NO_ERROR;
}
void MySensorDevice::Unregister(CodeDrivenDataModelProvider & provider)
{
UnregisterDescriptor(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/LoggingMySensorDevice.h` & `.cpp`)
To provide a self-contained simulator variant ready for `DeviceFactory`,
implement the self-delegate logging mock under the `impl/` subfolder:
#### Header (`impl/LoggingMySensorDevice.h`)
```cpp
#pragma once
#include <devices/my-sensor/MySensorDevice.h>
#include <lib/support/logging/CHIPLogging.h>
namespace chip::app {
class LoggingMySensorDevice : public MySensorDevice, public Clusters::IdentifyDelegate
{
public:
explicit LoggingMySensorDevice(TimerDelegate & timerDelegate) :
MySensorDevice(timerDelegate, *this)
{}
~LoggingMySensorDevice() 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/devices/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 = [
"MySensorDevice.cpp",
"MySensorDevice.h",
]
public_deps = [
"${chip_root}/examples/all-devices-app/all-devices-common/devices/interface: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 <devices/my-sensor/impl/LoggingMySensorDevice.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 std::make_unique<LoggingMySensorDevice>(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._
---
## 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}/devices/my-sensor/MySensorDevice.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
)
```
### 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/devices/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/devices/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.