From f301851d93863e337803a9037fe476283b1e3b11 Mon Sep 17 00:00:00 2001 From: Andrei Litvin Date: Mon, 3 Mar 2025 09:30:16 -0500 Subject: [PATCH] Server cluster interface --- docs/ids_and_codes/ERROR_CODES.md | 1 + scripts/build_coverage.sh | 5 +- src/BUILD.gn | 12 +- src/app/InteractionModelEngine.cpp | 2 +- src/app/data-model-provider/Provider.h | 4 +- .../tests/TestActionReturnStatus.cpp | 6 +- src/app/server-cluster/BUILD.gn | 35 ++++ .../server-cluster/DefaultServerCluster.cpp | 110 ++++++++++++ src/app/server-cluster/DefaultServerCluster.h | 84 +++++++++ .../server-cluster/ServerClusterInterface.h | 159 ++++++++++++++++++ src/app/server-cluster/tests/BUILD.gn | 31 ++++ .../tests/TestDefaultServerCluster.cpp | 152 +++++++++++++++++ src/app/tests/TestCommandInteraction.cpp | 7 +- src/app/tests/test-interaction-model-api.cpp | 5 +- src/app/tests/test-interaction-model-api.h | 5 +- .../tests/data_model/DataModelFixtures.cpp | 4 +- .../tests/data_model/DataModelFixtures.h | 5 +- .../codegen/CodegenDataModelProvider.cpp | 6 +- .../codegen/CodegenDataModelProvider.h | 5 +- .../tests/TestCodegenModelViaMocks.cpp | 4 +- src/lib/core/CHIPError.cpp | 3 + src/lib/core/CHIPError.h | 11 +- 22 files changed, 627 insertions(+), 29 deletions(-) create mode 100644 src/app/server-cluster/BUILD.gn create mode 100644 src/app/server-cluster/DefaultServerCluster.cpp create mode 100644 src/app/server-cluster/DefaultServerCluster.h create mode 100644 src/app/server-cluster/ServerClusterInterface.h create mode 100644 src/app/server-cluster/tests/BUILD.gn create mode 100644 src/app/server-cluster/tests/TestDefaultServerCluster.cpp diff --git a/docs/ids_and_codes/ERROR_CODES.md b/docs/ids_and_codes/ERROR_CODES.md index ff549e6df6b6dc..5bd5807e4ed442 100644 --- a/docs/ids_and_codes/ERROR_CODES.md +++ b/docs/ids_and_codes/ERROR_CODES.md @@ -55,6 +55,7 @@ This file was **AUTOMATICALLY** generated by | 37 | 0x25 | `CHIP_ERROR_UNKNOWN_IMPLICIT_TLV_TAG` | | 38 | 0x26 | `CHIP_ERROR_WRONG_TLV_TYPE` | | 39 | 0x27 | `CHIP_ERROR_TLV_CONTAINER_OPEN` | +| 40 | 0x28 | `CHIP_ERROR_IN_USE` | | 42 | 0x2A | `CHIP_ERROR_INVALID_MESSAGE_TYPE` | | 43 | 0x2B | `CHIP_ERROR_UNEXPECTED_TLV_ELEMENT` | | 45 | 0x2D | `CHIP_ERROR_NOT_IMPLEMENTED` | diff --git a/scripts/build_coverage.sh b/scripts/build_coverage.sh index d21aef5a77eb7e..5c29e7a9482649 100755 --- a/scripts/build_coverage.sh +++ b/scripts/build_coverage.sh @@ -222,6 +222,7 @@ lcov --initial --capture --directory "$OUTPUT_ROOT/obj/src" \ --exclude="$PWD"/zzz_generated/* \ --exclude="$PWD"/third_party/* \ --exclude=/usr/include/* \ + --ignore-errors inconsistent \ --output-file "$COVERAGE_ROOT/lcov_base.info" lcov --capture --directory "$OUTPUT_ROOT/obj/src" \ @@ -229,9 +230,11 @@ lcov --capture --directory "$OUTPUT_ROOT/obj/src" \ --exclude="$PWD"/zzz_generated/* \ --exclude="$PWD"/third_party/* \ --exclude=/usr/include/* \ + --ignore-errors inconsistent \ --output-file "$COVERAGE_ROOT/lcov_test.info" -lcov --add-tracefile "$COVERAGE_ROOT/lcov_base.info" \ +lcov --ignore-errors inconsistent \ + --add-tracefile "$COVERAGE_ROOT/lcov_base.info" \ --add-tracefile "$COVERAGE_ROOT/lcov_test.info" \ --ignore-errors inconsistent \ --output-file "$COVERAGE_ROOT/lcov_final.info" diff --git a/src/BUILD.gn b/src/BUILD.gn index d290cf04cf2737..2a8fa7c388d206 100644 --- a/src/BUILD.gn +++ b/src/BUILD.gn @@ -51,13 +51,14 @@ if (chip_build_tests) { deps = [] tests = [ "${chip_root}/src/access/tests", - "${chip_root}/src/app/data-model/tests", "${chip_root}/src/app/cluster-building-blocks/tests", "${chip_root}/src/app/data-model-provider/tests", + "${chip_root}/src/app/data-model/tests", "${chip_root}/src/app/icd/server/tests", "${chip_root}/src/crypto/tests", "${chip_root}/src/inet/tests", "${chip_root}/src/lib/address_resolve/tests", + "${chip_root}/src/app/server-cluster/tests", "${chip_root}/src/lib/asn1/tests", "${chip_root}/src/lib/core/tests", "${chip_root}/src/lib/format/tests", @@ -94,12 +95,19 @@ if (chip_build_tests) { # symbols for ember mocks which are used by other tests. tests += [ - "${chip_root}/src/data-model-providers/codegen/tests", "${chip_root}/src/setup_payload/tests", "${chip_root}/src/transport/raw/tests", ] } + if (current_os != "zephyr" && current_os != "mbed" && + chip_device_platform != "efr32") { + # Disabled from "one single binary" since the "mocks" contain duplicate sybmols + # with ember + # Disabled on EFR32 since "include " fails with `::fabs` not defined + tests += [ "${chip_root}/src/data-model-providers/codegen/tests" ] + } + if (chip_device_platform != "efr32") { tests += [ "${chip_root}/src/app/tests", diff --git a/src/app/InteractionModelEngine.cpp b/src/app/InteractionModelEngine.cpp index d92c459a94d438..9a64332aea3fa9 100644 --- a/src/app/InteractionModelEngine.cpp +++ b/src/app/InteractionModelEngine.cpp @@ -1734,7 +1734,7 @@ void InteractionModelEngine::DispatchCommand(CommandHandlerImpl & apCommandObj, request.invokeFlags.Set(DataModel::InvokeFlags::kTimed, apCommandObj.IsTimedInvoke()); request.subjectDescriptor = &subjectDescriptor; - std::optional status = GetDataModelProvider()->Invoke(request, apPayload, &apCommandObj); + std::optional status = GetDataModelProvider()->InvokeCommand(request, apPayload, &apCommandObj); // Provider indicates that handler status or data was already set (or will be set asynchronously) by // returning std::nullopt. If any other value is returned, it is requesting that a status is set. This diff --git a/src/app/data-model-provider/Provider.h b/src/app/data-model-provider/Provider.h index 27a86b63fa0cf2..67a8ccabb12e24 100644 --- a/src/app/data-model-provider/Provider.h +++ b/src/app/data-model-provider/Provider.h @@ -114,8 +114,8 @@ class Provider : public ProviderMetadataTree /// - if a value is returned (not nullopt) then the handler response MUST NOT be filled. The caller /// will then issue `handler->AddStatus(request.path, ->GetStatusCode())`. This is a /// convenience to make writing Invoke calls easier. - virtual std::optional Invoke(const InvokeRequest & request, chip::TLV::TLVReader & input_arguments, - CommandHandler * handler) = 0; + virtual std::optional InvokeCommand(const InvokeRequest & request, chip::TLV::TLVReader & input_arguments, + CommandHandler * handler) = 0; private: InteractionModelContext mContext = { nullptr }; diff --git a/src/app/data-model-provider/tests/TestActionReturnStatus.cpp b/src/app/data-model-provider/tests/TestActionReturnStatus.cpp index 8ca1993d149e1a..451c65640c55d7 100644 --- a/src/app/data-model-provider/tests/TestActionReturnStatus.cpp +++ b/src/app/data-model-provider/tests/TestActionReturnStatus.cpp @@ -15,13 +15,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - -#include "lib/core/CHIPError.h" -#include "protocols/interaction_model/StatusCode.h" -#include "pw_unit_test/framework_backend.h" #include #include +#include #include +#include #include diff --git a/src/app/server-cluster/BUILD.gn b/src/app/server-cluster/BUILD.gn new file mode 100644 index 00000000000000..cd8b7e3b48f816 --- /dev/null +++ b/src/app/server-cluster/BUILD.gn @@ -0,0 +1,35 @@ +# Copyright (c) 2025 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("server-cluster") { + sources = [ + "DefaultServerCluster.cpp", + "DefaultServerCluster.h", + "ServerClusterInterface.h", + ] + + public_deps = [ + "${chip_root}/src/app:attribute-access", + "${chip_root}/src/app:command-handler-interface", + "${chip_root}/src/app:paths", + "${chip_root}/src/app/common:ids", + "${chip_root}/src/app/data-model-provider", + "${chip_root}/src/app/data-model-provider:metadata", + "${chip_root}/src/crypto", + "${chip_root}/src/lib/core:error", + "${chip_root}/src/lib/core:types", + "${chip_root}/src/lib/support", + ] +} diff --git a/src/app/server-cluster/DefaultServerCluster.cpp b/src/app/server-cluster/DefaultServerCluster.cpp new file mode 100644 index 00000000000000..db2b7d3b3cf4ae --- /dev/null +++ b/src/app/server-cluster/DefaultServerCluster.cpp @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2025 Project CHIP Authors + * All rights reserved. + * + * 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. + */ +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace chip { +namespace app { +namespace { + +using namespace chip::app::Clusters; +using namespace chip::app::DataModel; + +constexpr std::array kGlobalAttributeEntries{ { + { + Globals::Attributes::ClusterRevision::Id, + BitFlags(), + Access::Privilege::kView, + std::nullopt, + }, + { + Globals::Attributes::FeatureMap::Id, + BitFlags(), + Access::Privilege::kView, + std::nullopt, + }, + { + Globals::Attributes::AttributeList::Id, + BitFlags(AttributeQualityFlags::kListAttribute), + Access::Privilege::kView, + std::nullopt, + }, + { + Globals::Attributes::AcceptedCommandList::Id, + BitFlags(AttributeQualityFlags::kListAttribute), + Access::Privilege::kView, + std::nullopt, + }, + { + Globals::Attributes::GeneratedCommandList::Id, + BitFlags(AttributeQualityFlags::kListAttribute), + Access::Privilege::kView, + std::nullopt, + }, +} }; + +} // namespace + +DefaultServerCluster::DefaultServerCluster() +{ + // SPEC - 7.10.3. Cluster Data Version + // A cluster data version SHALL be initialized randomly when it is first published. + mDataVersion = Crypto::GetRandU32(); +} + +CHIP_ERROR DefaultServerCluster::Attributes(const ConcreteClusterPath & path, DataModel::ListBuilder & builder) +{ + + return builder.ReferenceExisting(kGlobalAttributeEntries); +} + +BitFlags DefaultServerCluster::GetClusterFlags() const +{ + return {}; +} + +ActionReturnStatus DefaultServerCluster::WriteAttribute(const WriteAttributeRequest & request, AttributeValueDecoder & decoder) +{ + return Protocols::InteractionModel::Status::UnsupportedWrite; +} + +std::optional +DefaultServerCluster::InvokeCommand(const InvokeRequest & request, chip::TLV::TLVReader & input_arguments, CommandHandler * handler) +{ + return Protocols::InteractionModel::Status::UnsupportedCommand; +} + +CHIP_ERROR DefaultServerCluster::AcceptedCommands(const ConcreteClusterPath & path, + DataModel::ListBuilder & builder) +{ + return CHIP_NO_ERROR; +} + +CHIP_ERROR DefaultServerCluster::GeneratedCommands(const ConcreteClusterPath & path, DataModel::ListBuilder & builder) +{ + return CHIP_NO_ERROR; +} + +} // namespace app +} // namespace chip diff --git a/src/app/server-cluster/DefaultServerCluster.h b/src/app/server-cluster/DefaultServerCluster.h new file mode 100644 index 00000000000000..3463d39882bddc --- /dev/null +++ b/src/app/server-cluster/DefaultServerCluster.h @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2025 Project CHIP Authors + * All rights reserved. + * + * 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. + */ +#pragma once + +#include + +namespace chip { +namespace app { + +/// Provides an implementation of most methods for a `ServerClusterInterface` +/// to make it easier to implement spec-compliant classes. +/// +/// In particular it does: +/// - maintains a data version and provides `IncreaseDataVersion`. Ensures this +/// version is spec-compliant initialized (with a random value) +/// - Provides default implementations for most virtual methods EXCEPT: +/// - ReadAttribute (since that one needs to handle featuremap and revision) +/// - GetClusterId (since every implementation is for different clusters) +/// +/// +class DefaultServerCluster : public ServerClusterInterface +{ +public: + DefaultServerCluster(); + ~DefaultServerCluster() override = default; + + //////////////////////////// ServerClusterInterface implementation //////////////////////////////////////// + + [[nodiscard]] DataVersion GetDataVersion() const override { return mDataVersion; } + [[nodiscard]] BitFlags GetClusterFlags() const override; + + /// Default implementation errors out with an unsupported write on every attribute. + DataModel::ActionReturnStatus WriteAttribute(const DataModel::WriteAttributeRequest & request, + AttributeValueDecoder & decoder) override; + + /// Must only be implemented if support for any non-global attributes + /// is required. + /// + /// Default implementation just returns the global attributes required by the API contract. + CHIP_ERROR Attributes(const ConcreteClusterPath & path, DataModel::ListBuilder & builder) override; + + ///////////////////////////////////// Command Support ///////////////////////////////////////////////////////// + + /// Must only be implemented if commands are supported by the cluster + /// + /// Default implementation errors out with an UnsupportedCommand error. + std::optional InvokeCommand(const DataModel::InvokeRequest & request, + chip::TLV::TLVReader & input_arguments, + CommandHandler * handler) override; + + /// Must only be implemented if commands are supported by the cluster + /// + /// Default implementation is a NOOP (no list items generated) + CHIP_ERROR AcceptedCommands(const ConcreteClusterPath & path, + DataModel::ListBuilder & builder) override; + + /// Must only be implemented if commands that return values are supported by the cluster. + /// + /// Default implementation is a NOOP (no list items generated) + CHIP_ERROR GeneratedCommands(const ConcreteClusterPath & path, DataModel::ListBuilder & builder) override; + +protected: + void IncreaseDataVersion() { mDataVersion++; } + +private: + DataVersion mDataVersion; // will be random-initialized as per spec +}; + +} // namespace app +} // namespace chip diff --git a/src/app/server-cluster/ServerClusterInterface.h b/src/app/server-cluster/ServerClusterInterface.h new file mode 100644 index 00000000000000..086a37cbab529b --- /dev/null +++ b/src/app/server-cluster/ServerClusterInterface.h @@ -0,0 +1,159 @@ +/* + * Copyright (c) 2025 Project CHIP Authors + * All rights reserved. + * + * 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. + */ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace chip { +namespace app { + +/// Handles cluster interactions for a specific cluster id. +/// +/// A `ServerClusterInterface` instance is associated with a single endpointId and represents +/// a cluster that exists at a given `endpointId/clusterId` path. +/// +/// Provides metadata as well as interaction processing (attribute read/write and command handling). +class ServerClusterInterface +{ +public: + virtual ~ServerClusterInterface() = default; + + ///////////////////////////////////// Cluster Metadata Support ////////////////////////////////////////////////// + [[nodiscard]] virtual ClusterId GetClusterId() const = 0; + + /// Gets the data version for this cluster instance. + /// + /// Every cluster instance must have a data version. + /// + /// SPEC - 7.10.3. Cluster Data Version + /// A cluster data version is a metadata increment-only counter value, maintained for each cluster instance. + /// [...] + /// A cluster data version SHALL increment or be set (wrap) to zero if incrementing would exceed its + /// maximum value. A cluster data version SHALL be maintained for each cluster instance. + /// [...] + /// A cluster data version SHALL be incremented if any attribute data changes. + [[nodiscard]] virtual DataVersion GetDataVersion() const = 0; + + [[nodiscard]] virtual BitFlags GetClusterFlags() const = 0; + + ///////////////////////////////////// Attribute Support //////////////////////////////////////////////////////// + + /// Indicates the start/end of a series of list operations. This function will be called either before the first + /// Write operation or after the last one of a series of consecutive attribute data values received for the same attribute. + /// + /// 1) This function will be called if the client tries to set a nullable list attribute to null. + /// 2) This function will only be called at the beginning and end of a series of consecutive attribute data + /// blocks for the same attribute, no matter what list operations those data blocks represent. + /// 3) The opType argument indicates the type of notification (Start, Failure, Success). + virtual void ListAttributeWriteNotification(const ConcreteAttributePath & aPath, DataModel::ListWriteOperation opType) {} + + /// Reads the value of an existing attribute. + /// + /// ReadAttribute MUST be done on an "existent" attribute path: only on attributes that are + /// returned in an `Attributes` call for this cluster. ReadAttribute is not expected to perform + /// that verification; the caller is responsible for it. + /// + /// `request.path` is expected to have `GetClusterId` as the cluster id as well as an attribute that is + /// included in an `Attributes` call. + /// + /// This MUST HANDLE the following global attributes: + /// - FeatureMap::Id + /// - ClusterRevision::Id + /// + /// This function WILL NOT be called for attributes that can be derived from cluster metadata. + /// Specifically this WILL NOT be called (and does not need to implement handling for) the + /// following attribute IDs: + /// - AcceptedCommandList::Id + /// - AttributeList::Id + /// - GeneratedCommandList::Id + virtual DataModel::ActionReturnStatus ReadAttribute(const DataModel::ReadAttributeRequest & request, + AttributeValueEncoder & encoder) = 0; + + /// Writes a value to an existing attribute. + /// + /// WriteAttribute MUST be done on an "existent" attribute path: only on attributes that are + /// returned in an `Attributes` call for this cluster. WriteAttribute is not expected to perform + /// that verification; the caller is responsible for it. + /// + /// `request.path` is expected to have `GetClusterId` as the cluster id as well as an attribute that is + /// included in a `Attributes` call. + virtual DataModel::ActionReturnStatus WriteAttribute(const DataModel::WriteAttributeRequest & request, + AttributeValueDecoder & decoder) = 0; + + /// Retrieves the list of attributes supported by this cluster. + /// + /// Attribute list MUST contain global attributes. + /// + /// Specifically these attributes MUST always exist in the list for all clusters: + /// - ClusterRevision::Id + /// - FeatureMap::Id + /// - AcceptedCommandList::Id + /// - AttributeList::Id + /// - GeneratedCommandList::Id + /// See SPEC 7.13 Global Elements: `Global Attributes` table + virtual CHIP_ERROR Attributes(const ConcreteClusterPath & path, + DataModel::ListBuilder & builder) = 0; + + ///////////////////////////////////// Command Support ///////////////////////////////////////////////////////// + + /// Handles the invocation of a command. + /// + /// `handler` is used to send back the response. + /// - returning `nullopt` means that return value was placed in handler directly. + /// This includes cases where command handling and value return will be done asynchronously. + /// - returning a value other than Success implies an error reply (error and data are mutually exclusive) + /// + /// InvokeCommand MUST be done on an "existent" attribute path: only on commands that are + /// returned in an `AcceptedCommand` call for this cluster. + /// + /// Return value expectations: + /// - if a response has been placed into `handler` then std::nullopt MUST be returned. In particular + /// note that CHIP_NO_ERROR is NOT the same as std::nullopt: + /// > CHIP_NO_ERROR means handler had no status set and we expect the caller to AddStatus(success) + /// > std::nullopt means that handler has added an appropriate data/status response + /// - if a value is returned (not nullopt) then the handler response MUST NOT be filled. The caller + /// will then issue `handler->AddStatus(request.path, ->GetStatusCode())`. This is a + /// convenience to make writing Invoke calls easier. + virtual std::optional + InvokeCommand(const DataModel::InvokeRequest & request, chip::TLV::TLVReader & input_arguments, CommandHandler * handler) = 0; + + /// Retrieves a list of commands accepted by this cluster. + /// + /// Returning `CHIP_NO_ERROR` without adding anything to the `builder` list is expected + /// if no commands are supported by the cluster. + virtual CHIP_ERROR AcceptedCommands(const ConcreteClusterPath & path, + DataModel::ListBuilder & builder) = 0; + + /// Retrieves a list of commands generated by this cluster. + /// + /// Returning `CHIP_NO_ERROR` without adding anything to the `builder` list is expected + /// if no commands are generated by processing accepted commands. + virtual CHIP_ERROR GeneratedCommands(const ConcreteClusterPath & path, DataModel::ListBuilder & builder) = 0; +}; + +} // namespace app +} // namespace chip diff --git a/src/app/server-cluster/tests/BUILD.gn b/src/app/server-cluster/tests/BUILD.gn new file mode 100644 index 00000000000000..d9f7af30ec5e45 --- /dev/null +++ b/src/app/server-cluster/tests/BUILD.gn @@ -0,0 +1,31 @@ +# Copyright (c) 2025 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/build.gni") +import("//build_overrides/chip.gni") +import("${chip_root}/build/chip/chip_test_suite.gni") + +chip_test_suite("tests") { + output_name = "libServerClusterInterfaceTests" + + test_sources = [ "TestDefaultServerCluster.cpp" ] + + cflags = [ "-Wconversion" ] + + public_deps = [ + "${chip_root}/src/app/common:ids", + "${chip_root}/src/app/data-model-provider/tests:encode-decode", + "${chip_root}/src/app/server-cluster", + "${chip_root}/src/lib/core:string-builder-adapters", + ] +} diff --git a/src/app/server-cluster/tests/TestDefaultServerCluster.cpp b/src/app/server-cluster/tests/TestDefaultServerCluster.cpp new file mode 100644 index 00000000000000..a44791625d4a5b --- /dev/null +++ b/src/app/server-cluster/tests/TestDefaultServerCluster.cpp @@ -0,0 +1,152 @@ +/* + * Copyright (c) 2025 Project CHIP Authors + * All rights reserved. + * + * 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. + */ +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +using namespace chip; +using namespace chip::app; +using namespace chip::app::Clusters; +using namespace chip::app::DataModel; +using namespace chip::app::Testing; +using namespace chip::Protocols::InteractionModel; + +namespace { + +class FakeDefaultServerCluster : public DefaultServerCluster +{ +public: + FakeDefaultServerCluster(ClusterId id) : mClusterId(id) {} + + ClusterId GetClusterId() const override { return mClusterId; } + + DataModel::ActionReturnStatus ReadAttribute(const DataModel::ReadAttributeRequest & request, + AttributeValueEncoder & encoder) override + { + switch (request.path.mAttributeId) + { + case Globals::Attributes::FeatureMap::Id: + return encoder.Encode(0); + case Globals::Attributes::ClusterRevision::Id: + return encoder.Encode(123); + } + return CHIP_ERROR_INVALID_ARGUMENT; + } + + void TestIncreaseDataVersion() { IncreaseDataVersion(); } + +private: + ClusterId mClusterId; +}; + +} // namespace + +TEST(TestDefaultServerCluster, TestDataVersion) +{ + FakeDefaultServerCluster cluster(1); + + DataVersion v1 = cluster.GetDataVersion(); + cluster.TestIncreaseDataVersion(); + ASSERT_EQ(cluster.GetDataVersion(), v1 + 1); +} + +TEST(TestDefaultServerCluster, TestFlagsDefault) +{ + FakeDefaultServerCluster cluster(1); + ASSERT_EQ(cluster.GetClusterFlags().Raw(), 0u); +} + +TEST(TestDefaultServerCluster, AttributesDefault) +{ + FakeDefaultServerCluster cluster(1); + + DataModel::ListBuilder attributes; + + ASSERT_EQ(cluster.Attributes({ 1, 1 }, attributes), CHIP_NO_ERROR); + + ReadOnlyBuffer data = attributes.TakeBuffer(); + + // 5 global attributes are currently supported. Ensure they are returned. + ASSERT_EQ(data.size(), 5u); + + ASSERT_EQ(data[0].attributeId, Globals::Attributes::ClusterRevision::Id); + ASSERT_EQ(data[1].attributeId, Globals::Attributes::FeatureMap::Id); + ASSERT_EQ(data[2].attributeId, Globals::Attributes::AttributeList::Id); + ASSERT_EQ(data[3].attributeId, Globals::Attributes::AcceptedCommandList::Id); + ASSERT_EQ(data[4].attributeId, Globals::Attributes::GeneratedCommandList::Id); + + // first 2 are normal, the rest are list + for (size_t i = 0; i < 5; i++) + { + ASSERT_EQ(data[i].flags.Has(AttributeQualityFlags::kListAttribute), (i >= 2)); + ASSERT_EQ(data[i].readPrivilege, Access::Privilege::kView); + ASSERT_FALSE(data[i].writePrivilege.has_value()); + } +} + +TEST(TestDefaultServerCluster, CommandsDefault) +{ + FakeDefaultServerCluster cluster(1); + + DataModel::ListBuilder acceptedCommands; + ASSERT_EQ(cluster.AcceptedCommands({ 1, 1 }, acceptedCommands), CHIP_NO_ERROR); + ASSERT_TRUE(acceptedCommands.TakeBuffer().empty()); + + DataModel::ListBuilder generatedCommands; + ASSERT_EQ(cluster.GeneratedCommands({ 1, 1 }, generatedCommands), CHIP_NO_ERROR); + ASSERT_TRUE(generatedCommands.TakeBuffer().empty()); +} + +TEST(TestDefaultServerCluster, WriteAttributeDefault) +{ + FakeDefaultServerCluster cluster(1); + + WriteOperation test(0 /* endpoint */, 1 /* cluster */, 1234 /* attribute */); + test.SetSubjectDescriptor(kAdminSubjectDescriptor); + + AttributeValueDecoder decoder = test.DecoderFor(12345); + + ASSERT_EQ(cluster.WriteAttribute(test.GetRequest(), decoder), Status::UnsupportedWrite); + ASSERT_FALSE(decoder.TriedDecode()); +} + +TEST(TestDefaultServerCluster, InvokeDefault) +{ + FakeDefaultServerCluster cluster(1); + + TLV::TLVReader tlvReader; + InvokeRequest request; + + request.path = { 0 /* endpoint */, 1 /* cluster */, 1234 /* command */ }; + + ASSERT_EQ(cluster.InvokeCommand(request, tlvReader, nullptr /* command handler, assumed unused here */), + Status::UnsupportedCommand); +} diff --git a/src/app/tests/TestCommandInteraction.cpp b/src/app/tests/TestCommandInteraction.cpp index 363b43ccb4d5cf..65cab41b4848f9 100644 --- a/src/app/tests/TestCommandInteraction.cpp +++ b/src/app/tests/TestCommandInteraction.cpp @@ -403,10 +403,11 @@ class TestCommandInteractionModel : public TestImCustomDataModel return &instance; } - TestCommandInteractionModel() {} + TestCommandInteractionModel() = default; - std::optional Invoke(const DataModel::InvokeRequest & request, - chip::TLV::TLVReader & input_arguments, CommandHandler * handler) + std::optional InvokeCommand(const DataModel::InvokeRequest & request, + chip::TLV::TLVReader & input_arguments, + CommandHandler * handler) override { DispatchSingleClusterCommand(request.path, input_arguments, handler); return std::nullopt; // handler status is set by the dispatch diff --git a/src/app/tests/test-interaction-model-api.cpp b/src/app/tests/test-interaction-model-api.cpp index d5659420dd6a8a..2c7f393856b825 100644 --- a/src/app/tests/test-interaction-model-api.cpp +++ b/src/app/tests/test-interaction-model-api.cpp @@ -143,8 +143,9 @@ ActionReturnStatus TestImCustomDataModel::WriteAttribute(const WriteAttributeReq return CHIP_NO_ERROR; } -std::optional TestImCustomDataModel::Invoke(const InvokeRequest & request, - chip::TLV::TLVReader & input_arguments, CommandHandler * handler) +std::optional TestImCustomDataModel::InvokeCommand(const InvokeRequest & request, + chip::TLV::TLVReader & input_arguments, + CommandHandler * handler) { return std::make_optional(CHIP_ERROR_NOT_IMPLEMENTED); } diff --git a/src/app/tests/test-interaction-model-api.h b/src/app/tests/test-interaction-model-api.h index 54efc316da95a1..f3ad060286ee50 100644 --- a/src/app/tests/test-interaction-model-api.h +++ b/src/app/tests/test-interaction-model-api.h @@ -108,8 +108,9 @@ class TestImCustomDataModel : public CodegenDataModelProvider AttributeValueEncoder & encoder) override; DataModel::ActionReturnStatus WriteAttribute(const DataModel::WriteAttributeRequest & request, AttributeValueDecoder & decoder) override; - std::optional Invoke(const DataModel::InvokeRequest & request, - chip::TLV::TLVReader & input_arguments, CommandHandler * handler) override; + std::optional InvokeCommand(const DataModel::InvokeRequest & request, + chip::TLV::TLVReader & input_arguments, + CommandHandler * handler) override; }; } // namespace app diff --git a/src/controller/tests/data_model/DataModelFixtures.cpp b/src/controller/tests/data_model/DataModelFixtures.cpp index 930c74690392e2..55bc4e33fe06db 100644 --- a/src/controller/tests/data_model/DataModelFixtures.cpp +++ b/src/controller/tests/data_model/DataModelFixtures.cpp @@ -470,8 +470,8 @@ ActionReturnStatus CustomDataModel::WriteAttribute(const WriteAttributeRequest & return CHIP_ERROR_UNSUPPORTED_CHIP_FEATURE; } -std::optional CustomDataModel::Invoke(const InvokeRequest & request, chip::TLV::TLVReader & input_arguments, - CommandHandler * handler) +std::optional CustomDataModel::InvokeCommand(const InvokeRequest & request, + chip::TLV::TLVReader & input_arguments, CommandHandler * handler) { DispatchSingleClusterCommand(request.path, input_arguments, handler); return std::nullopt; // handler status is set by the dispatch diff --git a/src/controller/tests/data_model/DataModelFixtures.h b/src/controller/tests/data_model/DataModelFixtures.h index d847e4b57439cf..c11cb7bfa92c5f 100644 --- a/src/controller/tests/data_model/DataModelFixtures.h +++ b/src/controller/tests/data_model/DataModelFixtures.h @@ -120,8 +120,9 @@ class CustomDataModel : public CodegenDataModelProvider AttributeValueEncoder & encoder) override; DataModel::ActionReturnStatus WriteAttribute(const DataModel::WriteAttributeRequest & request, AttributeValueDecoder & decoder) override; - std::optional Invoke(const DataModel::InvokeRequest & request, - chip::TLV::TLVReader & input_arguments, CommandHandler * handler) override; + std::optional InvokeCommand(const DataModel::InvokeRequest & request, + chip::TLV::TLVReader & input_arguments, + CommandHandler * handler) override; }; } // namespace DataModelTests diff --git a/src/data-model-providers/codegen/CodegenDataModelProvider.cpp b/src/data-model-providers/codegen/CodegenDataModelProvider.cpp index 6913214ac803cc..8c84fa1740374d 100644 --- a/src/data-model-providers/codegen/CodegenDataModelProvider.cpp +++ b/src/data-model-providers/codegen/CodegenDataModelProvider.cpp @@ -158,9 +158,9 @@ CHIP_ERROR CodegenDataModelProvider::Startup(DataModel::InteractionModelContext return CHIP_NO_ERROR; } -std::optional CodegenDataModelProvider::Invoke(const DataModel::InvokeRequest & request, - TLV::TLVReader & input_arguments, - CommandHandler * handler) +std::optional CodegenDataModelProvider::InvokeCommand(const DataModel::InvokeRequest & request, + TLV::TLVReader & input_arguments, + CommandHandler * handler) { CommandHandlerInterface * handler_interface = CommandHandlerInterfaceRegistry::Instance().GetCommandHandler(request.path.mEndpointId, request.path.mClusterId); diff --git a/src/data-model-providers/codegen/CodegenDataModelProvider.h b/src/data-model-providers/codegen/CodegenDataModelProvider.h index 9edea20ed6a7a7..64d6b9511ef8e8 100644 --- a/src/data-model-providers/codegen/CodegenDataModelProvider.h +++ b/src/data-model-providers/codegen/CodegenDataModelProvider.h @@ -63,9 +63,10 @@ class CodegenDataModelProvider : public DataModel::Provider AttributeValueEncoder & encoder) override; DataModel::ActionReturnStatus WriteAttribute(const DataModel::WriteAttributeRequest & request, AttributeValueDecoder & decoder) override; + void ListAttributeWriteNotification(const ConcreteAttributePath & aPath, DataModel::ListWriteOperation opType) override; - std::optional Invoke(const DataModel::InvokeRequest & request, TLV::TLVReader & input_arguments, - CommandHandler * handler) override; + std::optional InvokeCommand(const DataModel::InvokeRequest & request, + TLV::TLVReader & input_arguments, CommandHandler * handler) override; /// attribute tree iteration CHIP_ERROR Endpoints(DataModel::ListBuilder & out) override; diff --git a/src/data-model-providers/codegen/tests/TestCodegenModelViaMocks.cpp b/src/data-model-providers/codegen/tests/TestCodegenModelViaMocks.cpp index bcd9d64d88d86d..b3dfcbff972d02 100644 --- a/src/data-model-providers/codegen/tests/TestCodegenModelViaMocks.cpp +++ b/src/data-model-providers/codegen/tests/TestCodegenModelViaMocks.cpp @@ -2351,7 +2351,7 @@ TEST_F(TestCodegenModelViaMocks, EmberInvokeTest) const uint32_t kDispatchCountPre = chip::Test::DispatchCount(); // Using a handler set to nullptr as it is not used by the impl - ASSERT_EQ(model.Invoke(kInvokeRequest, tlvReader, /* handler = */ nullptr), std::nullopt); + ASSERT_EQ(model.InvokeCommand(kInvokeRequest, tlvReader, /* handler = */ nullptr), std::nullopt); EXPECT_EQ(chip::Test::DispatchCount(), kDispatchCountPre + 1); // single dispatch EXPECT_EQ(chip::Test::GetLastDispatchPath(), kCommandPath); // for the right path @@ -2365,7 +2365,7 @@ TEST_F(TestCodegenModelViaMocks, EmberInvokeTest) const uint32_t kDispatchCountPre = chip::Test::DispatchCount(); // Using a handler set to nullpotr as it is not used by the impl - ASSERT_EQ(model.Invoke(kInvokeRequest, tlvReader, /* handler = */ nullptr), std::nullopt); + ASSERT_EQ(model.InvokeCommand(kInvokeRequest, tlvReader, /* handler = */ nullptr), std::nullopt); EXPECT_EQ(chip::Test::DispatchCount(), kDispatchCountPre + 1); // single dispatch EXPECT_EQ(chip::Test::GetLastDispatchPath(), kCommandPath); // for the right path diff --git a/src/lib/core/CHIPError.cpp b/src/lib/core/CHIPError.cpp index 2068a9fbc68c42..de1f0b3862c5e3 100644 --- a/src/lib/core/CHIPError.cpp +++ b/src/lib/core/CHIPError.cpp @@ -178,6 +178,9 @@ bool FormatCHIPError(char * buf, uint16_t bufSize, CHIP_ERROR err) case CHIP_ERROR_TLV_CONTAINER_OPEN.AsInteger(): desc = "TLV container open"; break; + case CHIP_ERROR_IN_USE.AsInteger(): + desc = "In use"; + break; case CHIP_ERROR_INVALID_MESSAGE_TYPE.AsInteger(): desc = "Invalid message type"; break; diff --git a/src/lib/core/CHIPError.h b/src/lib/core/CHIPError.h index 03217378eecb87..513967d1501726 100644 --- a/src/lib/core/CHIPError.h +++ b/src/lib/core/CHIPError.h @@ -837,7 +837,16 @@ using CHIP_ERROR = ::chip::ChipError; */ #define CHIP_ERROR_TLV_CONTAINER_OPEN CHIP_CORE_ERROR(0x27) -// AVAILABLE: 0x28 +/** + * @def CHIP_ERROR_IN_USE + * + * @brief + * A value is already used. Generally indicates an unavailable resource. + * As opposed to CHIP_ERROR_BUSY, the use is not considered transient/temporary. + * + */ +#define CHIP_ERROR_IN_USE CHIP_CORE_ERROR(0x28) + // AVAILABLE: 0x29 /**