Skip to main content

code-driven-cluster-development

Guidelines for implementing or migrating Matter server clusters (code that resides in `src/app/clusters`) using the DefaultServerCluster base class (code-driven data model approach), as opposed to the legacy ZAP/Ember codegen approach.

Quellinformationen

Repository
project-chip/connectedhomeip
Letzte Quellaktivität
28. April 2026 um 15:13
Erkannte Sprache von SKILL.md
Englisch
Sterne
8.960
Forks
2.486

Installationsoptionen

Standardmäßig ist der Prompt ausgewählt, der zuerst die Quelle prüft. Sie können zu einem direkten Befehl wechseln oder eine lokale Kopie herunterladen.

Quelldateien prüfen

Lesen Sie SKILL.md und alle von SkillsMP angezeigten Begleitdateien, bevor Sie sich für eine Installation entscheiden.

SKILL.md wird angezeigt

SKILL.md
Quellanweisungen · Schreibgeschützte Vorschau
name
code-driven-cluster-development
description
Guidelines for implementing or migrating Matter server clusters (code that resides in `src/app/clusters`) using the DefaultServerCluster base class (code-driven data model approach), as opposed to the legacy ZAP/Ember codegen approach.
# Code-Driven Cluster Development ## What Is a Code-Driven Cluster? A _code-driven_ cluster is a `ServerClusterInterface` implementation that lives in `src/app/clusters/<cluster-folder>/` and extends `DefaultServerCluster`. It stores its own attribute state in C++ member variables instead of relying on the Ember attribute RAM store. The framework calls the cluster's virtual methods (`ReadAttribute`, `WriteAttribute`, `InvokeCommand`, …) directly; ZAP-generated attribute accessors (`emberAfReadAttribute` etc.) must not be used inside the cluster class itself. Note that the `<cluster-folder>` naming is not standardized, but often starts with the cluster name. It is a mapping defined in `src/app/zap_cluster_list.json` and it is often (but not always) `<cluster-name>-server`. --- ## Directory / File Layout A common pattern for code-driven cluster directory layout is: ``` src/app/clusters/<cluster-name>-server/ ├── <ClusterName>Cluster.h # Core class (extends DefaultServerCluster) ├── <ClusterName>Cluster.cpp # Core class implementation ├── CodegenIntegration.h # App-specific Bridge: ZAP ↔ code-driven cluster ├── CodegenIntegration.cpp # App-specific ZAP callbacks + FindClusterOnEndpoint() ├── BUILD.gn # Core files (does NOT include CodegenIntegration) ├── app_config_dependent_sources.cmake # Application code-generation dependencies ├── app_config_dependent_sources.gni # Application code-generation dependencies └── tests/ ├── BUILD.gn └── Test<ClusterName>Cluster.cpp ``` _Alternative: Legacy-Preserving Layout_ Some clusters (e.g., `on-off-server`) use a layout that keeps the legacy Ember/ZAP implementation in a `codegen/` subdirectory while placing the new code-driven implementation in the root. - **Not Typical:** This approach is not typical and is generally discouraged as it maintains two parallel implementation paths for the same cluster, increasing the burden on maintenance, testing, and validation. - **Last Resort:** This is only done as a last resort when complex cluster inter-dependencies make a full migration difficult without incurring significant code complexity or unacceptable resource (Flash/RAM) overhead. **Build system rules:** - Codegen integration files (whether `CodegenIntegration.cpp` or files in `codegen/`) go in `app_config_dependent_sources.cmake` and `app_config_dependent_sources.gni` (these are the codegen-dependent files). - All other files (`<ClusterName>Cluster.h/cpp`, test files) go in `BUILD.gn`. - **Sources must belong to a single build target**: Files referenced in `BUILD.gn` and `app_config_dependent_sources.*` must be mutually exclusive. - `app_config_dependent_sources.cmake` and `app_config_dependent_sources.gni` must not contain non-codegen files. - The cluster should be added to the cluster list compiled in `src/app/clusters/BUILD.gn`. - Every source file must appear somewhere: either `BUILD.gn` (if no App-specific dependencies) or `app_config_dependent_sources.*` if depending on application ZAP configuration. Unlisted headers or cpp files are a review red flag. --- ## Core Cluster Class ### Header (`<ClusterName>Cluster.h`) ```cpp #pragma once #include <app/server-cluster/DefaultServerCluster.h> #include <app/server-cluster/OptionalAttributeSet.h> #include <clusters/<ClusterName>/Attributes.h> #include <clusters/<ClusterName>/Metadata.h> namespace chip::app::Clusters { class FooCluster : public DefaultServerCluster { public: // Optional attributes are tracked as a compile-time bitset. using OptionalAttributeSet = app::OptionalAttributeSet< Foo::Attributes::SomeOptional::Id, Foo::Attributes::AnotherOptional::Id>; // Use a Config/StartupConfiguration class (or struct for simple cases) for // constructor arguments that may be optional or have defaults. Use a class // with private members and builder-style .WithXxx() setters to prevent // misconfiguration in non-trivial cases. class Config { public: Config & WithMinValue(DataModel::Nullable<int16_t> min) { mMinValue = min; return *this; } Config & WithMaxValue(DataModel::Nullable<int16_t> max) { mMaxValue = max; return *this; } Config & WithOptionalAttributes(OptionalAttributeSet attrs) { mOptionalAttributes = attrs; return *this; } private: friend class FooCluster; DataModel::Nullable<int16_t> mMinValue{}; DataModel::Nullable<int16_t> mMaxValue{}; OptionalAttributeSet mOptionalAttributes{}; }; FooCluster(EndpointId endpointId, const Config & config = {}); // --- ServerClusterInterface overrides --- DataModel::ActionReturnStatus ReadAttribute(const DataModel::ReadAttributeRequest & request, AttributeValueEncoder & encoder) override; CHIP_ERROR Attributes(const ConcreteClusterPath & path, ReadOnlyBufferBuilder<DataModel::AttributeEntry> & builder) override; // Application-facing API CHIP_ERROR SetMeasuredValue(DataModel::Nullable<int16_t> value); DataModel::Nullable<int16_t> GetMeasuredValue() const { return mMeasuredValue; } protected: const BitFlags<Foo::Feature> mFeatureMap; OptionalAttributeSet mOptionalAttributeSet; DataModel::Nullable<int16_t> mMeasuredValue{}; // ... other member variables }; } // namespace chip::app::Clusters ``` Key points: - Inherit from `DefaultServerCluster`. Pass `{ endpointId, ClusterId }` to the base constructor. - Declare `OptionalAttributeSet` as a `using` alias so callers can refer to it via `FooCluster::OptionalAttributeSet`. - **Use a `Config` type:** For constructor arguments that may be optional or have defaults. Prefer a `class` with private members and builder-style `.WithXxx()` setters in non-trivial cases, and use a `struct` only for simple passive configuration bundles. - **Store Separate Variables:** Extract fields from the `Config` object into separate member variables in the cluster class. This allows marking immutable fields as `const` and prevents accidental runtime modification. - Validate constructor arguments with `VerifyOrDie` (programming errors that indicate a logic bug at call site, not a recoverable runtime error). - Expose application-facing setters/getters; keep attribute storage in `protected` or `private` members. ### Implementation (`<ClusterName>Cluster.cpp`) **When to call the base class**: You MUST call the base class from `Startup()` and `Shutdown()`. Do NOT call the base class from `ReadAttribute`, `WriteAttribute`, or `InvokeCommand` — there is no base-class behavior for these methods; return `UnsupportedAttribute` / `UnsupportedCommand` directly in the `default` case instead. **Only override `Startup`/`Shutdown` when custom code is needed** (e.g. reading persisted state on startup, registering a timer on startup and cancelling it on shutdown). If your override would only call the base, omit it entirely — a `Startup` that just calls `DefaultServerCluster::Startup` is dead weight. #### `ReadAttribute` ```cpp DataModel::ActionReturnStatus FooCluster::ReadAttribute( const DataModel::ReadAttributeRequest & request, AttributeValueEncoder & encoder) { using namespace Foo::Attributes; switch (request.path.mAttributeId) { case ClusterRevision::Id: return encoder.Encode(Foo::kRevision); case FeatureMap::Id: return encoder.Encode(static_cast<uint32_t>(mFeatureMap.Raw())); case MeasuredValue::Id: return encoder.Encode(mMeasuredValue); // ... other attributes default: return Protocols::InteractionModel::Status::UnsupportedAttribute; } } ``` - **Return `Protocols::InteractionModel::Status::UnsupportedAttribute` directly in the `default` case. Do not call `DefaultServerCluster::ReadAttribute` — it has no base behavior for read operations.** - The framework pre-filters requests so `ReadAttribute` is only called for paths that are in the `Attributes()` list; returning `UnsupportedAttribute` for anything unrecognised is the correct and consistent pattern. - Always encode/handle `ClusterRevision` and `FeatureMap` explicitly. - **Do not add path-validity checks** before the switch — they add code size and are redundant because the framework guarantees the path exists. - **Do not add feature-flag checks inside the switch cases** for optional attributes if those attributes are already conditionally included in the `Attributes()` list. The framework pre-filters requests based on the supported attributes list. - Do not add returning `UnsupportedAttribute` inside attribute switch handling. Existent path checks ensure those code lines would never be used. #### `Attributes` ```cpp CHIP_ERROR FooCluster::Attributes( const ConcreteClusterPath & path, ReadOnlyBufferBuilder<DataModel::AttributeEntry> & builder) { AttributeListBuilder listBuilder(builder); const DataModel::AttributeEntry optionalAttrs[] = { Foo::Attributes::SomeOptional::kMetadataEntry, }; return listBuilder.Append(Span(Foo::Attributes::kMandatoryMetadata), Span(optionalAttrs), mOptionalAttributeSet); } ``` - Use `AttributeListBuilder` from `<app/server-cluster/AttributeListBuilder.h>`. - `kMandatoryMetadata` is typically defined in `<clusters/<ClusterName>/Metadata.h>` (generated). - Pass the `OptionalAttributeSet` so optional attributes are only included when enabled. #### Attribute mutation helpers Use the inherited helpers to update values; they handle checking for changes and notifying subscribers automatically: ```cpp // Updates the value AND notifies subscribers automatically: SetAttributeValue(mSomeField, newValue, Foo::Attributes::SomeField::Id); // For nullable attributes: SetAttributeValue(mNullableField, DataModel::NullNullable, Foo::Attributes::NullableField::Id); ``` `SetAttributeValue` returns `true` if the value actually changed (and thus a notification was sent). `NotifyAttributeChanged` should only be used directly for manually-complex cases (e.g. updating a list member) where it increments the data version and notifies the IM engine. #### Spec constraint validation Return `CHIP_IM_GLOBAL_STATUS(ConstraintError)` (not a `VerifyOrDie`) for out-of-range values coming from the application at runtime: ```cpp CHIP_ERROR FooCluster::SetMeasuredValue(DataModel::Nullable<int16_t> value) { if (!value.IsNull()) { VerifyOrReturnError(value.Value() >= kMinAllowed && value.Value() <= kMaxAllowed, CHIP_IM_GLOBAL_STATUS(ConstraintError)); } SetAttributeValue(mMeasuredValue, value, Foo::Attributes::MeasuredValue::Id); return CHIP_NO_ERROR; } ``` Use `VerifyOrDie` in the constructor for invariants that must hold at construction time (programming errors), and `VerifyOrReturnError` for runtime checks. #### Writable attributes Override `WriteAttribute` only when the cluster has spec-defined writable attributes. Use `AttributeValueDecoder` to decode the incoming TLV: ```cpp DataModel::ActionReturnStatus FooCluster::WriteAttribute( const DataModel::WriteAttributeRequest & request, AttributeValueDecoder & decoder) { using namespace Foo::Attributes; switch (request.path.mAttributeId) { case WritableAttr::Id: { uint16_t value{}; ReturnErrorOnFailure(decoder.Decode(value)); return SetWritableAttr(value); } default: return Protocols::InteractionModel::Status::UnsupportedAttribute; } } ``` Return `Protocols::InteractionModel::Status::UnsupportedAttribute` directly in the `default` case. Do not delegate to `DefaultServerCluster::WriteAttribute` — there is no base-class behavior for write operations. #### Commands ```cpp std::optional<DataModel::ActionReturnStatus> FooCluster::InvokeCommand( const DataModel::InvokeRequest & request, chip::TLV::TLVReader & input_arguments, CommandHandler * handler) { using namespace Foo::Commands; switch (request.path.mCommandId) { case DoSomething::Id: { DoSomething::DecodableType req; ReturnErrorOnFailure(DataModel::Decode(input_arguments, req)); return HandleDoSomething(req, handler); } default: return Protocols::InteractionModel::Status::UnsupportedCommand; } } ``` ##### Return Codes for `InvokeCommand` The return type of `InvokeCommand` is `std::optional<DataModel::ActionReturnStatus>`. Understanding what to return is critical to avoid encoding duplicate responses: - **Any return except `std::nullopt` implies an automatic call to `handler->AddStatus`.** - If you return `CHIP_NO_ERROR` (or `Protocols::InteractionModel::Status::Success`), the framework will automatically add a Success status response. - If you return an error (e.g., `CHIP_ERROR_INVALID_ARGUMENT` or a specific IM status), the framework will automatically add the corresponding error status. - **If you manually add a response or status to the handler, you MUST return `std::nullopt`.** - This applies if you call `handler->AddResponse(...)` or `handler->AddStatus(...)`. - Returning anything else (even `CHIP_NO_ERROR`) will cause the framework to try to add another status, resulting in a bug (dual response encoding). **Typical Patterns:** 1. **Command with data response:** ```cpp FooResponse::Type response; // fill response... handler->AddResponse(request.path, response); return std::nullopt; // Required because we used handler->AddResponse ``` 2. **Command with success status (no data):** ```cpp // Do the work... return CHIP_NO_ERROR; // Framework will automatically call AddStatus(Success) ``` _Note: `return Protocols::InteractionModel::Status::Success;` is also valid and equivalent._ 3. **Command with error status:** ```cpp if (error) { return Protocols::InteractionModel::Status::ConstraintError; // Framework will AddStatus } ``` **Common Anti-Patterns (Bugs):** - `handler->AddResponse(path, response); return CHIP_NO_ERROR;` (Bug: encodes response AND success status) - `handler->AddStatus(path, Status::Success); return CHIP_NO_ERROR;` (Bug: encodes success status twice) ```cpp CHIP_ERROR FooCluster::AcceptedCommands( const ConcreteClusterPath & path, ReadOnlyBufferBuilder<DataModel::AcceptedCommandEntry> & builder) { static constexpr DataModel::AcceptedCommandEntry kCommands[] = { Foo::Commands::DoSomething::kMetadataEntry, }; return builder.ReferenceExisting(Span(kCommands)); } ``` #### Events ```cpp // Emit a spec-defined event using the cluster context: Foo::Events::StateChanged::Type event{ /* fields */ }; mContext->interactionContext.eventsGenerator.GenerateEvent(event, mPath.mEndpointId); ``` Override `EventInfo` only when non-default read privileges are needed. --- ## CodegenIntegration Layer `CodegenIntegration.h/cpp` (or equivalent files in the `codegen/` subdirectory) is the **only** place where Ember/ZAP APIs are allowed. Its responsibilities are: 1. Declare a file-scope array of `LazyRegisteredServerCluster<FooCluster>` instances (never heap-allocate). 2. Read ZAP attribute store defaults and construct `Config` structs. 3. Register/unregister clusters via `CodegenClusterIntegration::RegisterServer`. 4. Implement `FindClusterOnEndpoint()` and optional convenience setters. 5. Provide empty stubs for legacy plugin callbacks. ### Typical pattern ```cpp // CodegenIntegration.cpp
Auf GitHub ansehen
Diese SKILL.md ist sehr gross, daher zeigt SkillsMP hier nur den ersten Abschnitt. Auf GitHub ansehen