From 8411df53f18988949e859fc6a53b640f188c26c3 Mon Sep 17 00:00:00 2001 From: Andrei Litvin Date: Mon, 10 Jun 2024 18:59:02 -0400 Subject: [PATCH] cluster model decoupling - declare the codegen (ember) version and implement iteration (#33345) * Initial copy with a clean history * make linter happy * Restyle * Fix typo * Add nolint: assert will return before we use the underlying value * 2 more fixes regarding unchecked access * Switch some asserts to expects, for better test logic * Model renames * Add renamed files * Add some attribute iteration hint * Make use of the attribute cache * Restyle * Add a cluster iteration hint * Add a few more hints. Ember code still contains loops though, so this may not be ideal still * Add some TODO items for using faster iterations for data. Ember index vs value duality still needs some work * Add a cluster type cache as well. This relies on ember being reasonably static * Add global attribute handling * Fix typing u16 vs unsigned * Fix auto-added include names * Remove back the initialization and make the comment more obvious * Update src/app/codegen-interaction-model/model.gni Co-authored-by: Karsten Sperling <113487422+ksperling-apple@users.noreply.github.com> * Code review feedback: added comments * Update src/app/codegen-interaction-model/CodegenDataModel.h Co-authored-by: Boris Zbarsky * Update src/app/codegen-interaction-model/CodegenDataModel.cpp Co-authored-by: Boris Zbarsky * Update src/app/codegen-interaction-model/BUILD.gn Co-authored-by: Tennessee Carmel-Veilleux * Some cleanup logic for event generation - naming and return values as eventid is not the same as event number * Comment fix * More naming updates * Several comment updates and renamed RequestContext to ActionContext * Restyle * Rename to InteractionModelContext * one more rename * Fix typo * Fix tests to compile * More renames of actions to context * One more comment added * Restyle * Address review comments * Restyle * make clang-tidy happy * Operator== exists on optional ... make use of that directly * Started renaming things * Use the right types in Model.h * Make things compile * Skip global attribute handling, add TODOs for reading extra bits from ember * Typo fix * Several flags and correct loading of privileges for attributes * Start implementing command iteration ... still feels awkward and caching will be a pain * We seem to also support fabric scoping detection * implementation is in theory done, need unit tests * Fix iterator name * Mock support for accepted/generated commands, start having unit tests * Better iteration tests on accepted commands * More unit tests and fix bugs * Restyle * More tests, one iteration bug fix * Slight update again * Aiming for more test coverage * More test coverage for edge cases in iteration * Fix code review comment * Restyle * Update src/app/interaction-model/Events.h Co-authored-by: Boris Zbarsky * Update src/app/interaction-model/IterationTypes.h Co-authored-by: Boris Zbarsky * Update src/app/interaction-model/Paths.h Co-authored-by: Boris Zbarsky * Fix comment about validity * Some kListBegin/End comment updates * Drop kListBegin/End alltogether * Drop groupId * Comment update * Update for data version to be mandatory, add more error reporting and logging * Update to use kInvalid instead of Invalid method * Update flags.set * Use IsServerMask on clusterr class * Use a struct instead of a typedef * Fix compile without error logging * Restyle * Remove command quality that is not supported * Restyle * Rename IsServerMask to IsServer --------- Co-authored-by: Andrei Litvin Co-authored-by: Karsten Sperling <113487422+ksperling-apple@users.noreply.github.com> Co-authored-by: Boris Zbarsky Co-authored-by: Tennessee Carmel-Veilleux --- src/BUILD.gn | 1 + src/app/ConcreteClusterPath.h | 2 +- src/app/codegen-interaction-model/BUILD.gn | 27 + .../CodegenDataModel.cpp | 548 ++++++++++++++++++ .../CodegenDataModel.h | 132 +++++ src/app/codegen-interaction-model/model.gni | 35 ++ .../codegen-interaction-model/tests/BUILD.gn | 35 ++ .../tests/TestCodegenModelViaMocks.cpp | 488 ++++++++++++++++ src/app/interaction-model/BUILD.gn | 3 +- src/app/interaction-model/IterationTypes.h | 99 ---- src/app/interaction-model/MetadataTypes.cpp | 35 ++ src/app/interaction-model/MetadataTypes.h | 155 +++++ src/app/interaction-model/Model.h | 9 +- src/app/interaction-model/OperationTypes.h | 1 - src/app/interaction-model/tests/BUILD.gn | 2 + src/app/util/af-types.h | 7 +- src/app/util/mock/MockNodeConfig.cpp | 30 +- src/app/util/mock/MockNodeConfig.h | 5 +- 18 files changed, 1499 insertions(+), 115 deletions(-) create mode 100644 src/app/codegen-interaction-model/BUILD.gn create mode 100644 src/app/codegen-interaction-model/CodegenDataModel.cpp create mode 100644 src/app/codegen-interaction-model/CodegenDataModel.h create mode 100644 src/app/codegen-interaction-model/model.gni create mode 100644 src/app/codegen-interaction-model/tests/BUILD.gn create mode 100644 src/app/codegen-interaction-model/tests/TestCodegenModelViaMocks.cpp delete mode 100644 src/app/interaction-model/IterationTypes.h create mode 100644 src/app/interaction-model/MetadataTypes.cpp create mode 100644 src/app/interaction-model/MetadataTypes.h diff --git a/src/BUILD.gn b/src/BUILD.gn index 13f09beab2d4df..c525f7a71d3a1c 100644 --- a/src/BUILD.gn +++ b/src/BUILD.gn @@ -55,6 +55,7 @@ if (chip_build_tests) { chip_test_group("tests") { deps = [] tests = [ + "${chip_root}/src/app/codegen-interaction-model/tests", "${chip_root}/src/app/interaction-model/tests", "${chip_root}/src/access/tests", "${chip_root}/src/crypto/tests", diff --git a/src/app/ConcreteClusterPath.h b/src/app/ConcreteClusterPath.h index 58b2f5b477f139..8b701efa83967b 100644 --- a/src/app/ConcreteClusterPath.h +++ b/src/app/ConcreteClusterPath.h @@ -52,7 +52,7 @@ struct ConcreteClusterPath // to alignment requirements it's "free" in the sense of not needing more // memory to put it here. But we don't initialize it, because that // increases codesize for the non-consumers. - bool mExpanded; // NOTE: in between larger members + bool mExpanded; // NOTE: in between larger members, NOT initialized (see above) ClusterId mClusterId = 0; }; diff --git a/src/app/codegen-interaction-model/BUILD.gn b/src/app/codegen-interaction-model/BUILD.gn new file mode 100644 index 00000000000000..418983a2fc8b8f --- /dev/null +++ b/src/app/codegen-interaction-model/BUILD.gn @@ -0,0 +1,27 @@ +# Copyright (c) 2024 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") +# This source set is TIGHLY coupled with code-generated data models +# as generally implemented by `src/app/util` +# +# Corresponding functions defined in attribute-storace.cpp/attribute-table.cpp must +# be available at link time for this model to use +# +# Use `model.gni` to get access to: +# CodegenDataModel.cpp +# CodegenDataModel.h +# +# The above list of files exists to satisfy the "dependency linter" +# since those files should technically be "visible to gn" even though we +# are supposed to go through model.gni constants diff --git a/src/app/codegen-interaction-model/CodegenDataModel.cpp b/src/app/codegen-interaction-model/CodegenDataModel.cpp new file mode 100644 index 00000000000000..f917a8e9c9c4b0 --- /dev/null +++ b/src/app/codegen-interaction-model/CodegenDataModel.cpp @@ -0,0 +1,548 @@ +/* + * Copyright (c) 2024 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 { + +/// Load the cluster information into the specified destination +std::variant LoadClusterInfo(const ConcreteClusterPath & path, + const EmberAfCluster & cluster) +{ + DataVersion * versionPtr = emberAfDataVersionStorage(path); + if (versionPtr == nullptr) + { + ChipLogError(AppServer, "Failed to get data version for %d/" ChipLogFormatMEI, static_cast(path.mEndpointId), + ChipLogValueMEI(cluster.clusterId)); + return CHIP_ERROR_NOT_FOUND; + } + + InteractionModel::ClusterInfo info(*versionPtr); + + // TODO: set entry flags: + // info->flags.Set(ClusterQualityFlags::kDiagnosticsData) + + return info; +} + +/// Converts a EmberAfCluster into a ClusterEntry +std::variant ClusterEntryFrom(EndpointId endpointId, const EmberAfCluster & cluster) +{ + ConcreteClusterPath clusterPath(endpointId, cluster.clusterId); + auto info = LoadClusterInfo(clusterPath, cluster); + + if (CHIP_ERROR * err = std::get_if(&info)) + { + return *err; + } + + if (InteractionModel::ClusterInfo * infoValue = std::get_if(&info)) + { + return InteractionModel::ClusterEntry{ + .path = clusterPath, + .info = *infoValue, + }; + } + return CHIP_ERROR_INCORRECT_STATE; +} + +/// Finds the first server cluster entry for the given endpoint data starting at [start_index] +/// +/// Returns an invalid entry if no more server clusters are found +InteractionModel::ClusterEntry FirstServerClusterEntry(EndpointId endpointId, const EmberAfEndpointType * endpoint, + unsigned start_index, unsigned & found_index) +{ + for (unsigned cluster_idx = start_index; cluster_idx < endpoint->clusterCount; cluster_idx++) + { + const EmberAfCluster & cluster = endpoint->cluster[cluster_idx]; + if (!cluster.IsServer()) + { + continue; + } + + found_index = cluster_idx; + auto entry = ClusterEntryFrom(endpointId, cluster); + + if (InteractionModel::ClusterEntry * entryValue = std::get_if(&entry)) + { + return *entryValue; + } + +#if CHIP_ERROR_LOGGING + if (CHIP_ERROR * errValue = std::get_if(&entry)) + { + ChipLogError(AppServer, "Failed to load cluster entry: %" CHIP_ERROR_FORMAT, errValue->Format()); + } + else + { + // Should NOT be possible: entryFrom has only 2 variants + ChipLogError(AppServer, "Failed to load cluster entry, UNKNOWN entry return type"); + } +#endif + } + + return InteractionModel::ClusterEntry::kInvalid; +} + +/// Load the attribute information into the specified destination +/// +/// `info` is assumed to be default-constructed/clear (i.e. this sets flags, but does not reset them). +void LoadAttributeInfo(const ConcreteAttributePath & path, const EmberAfAttributeMetadata & attribute, + InteractionModel::AttributeInfo * info) +{ + info->readPrivilege = RequiredPrivilege::ForReadAttribute(path); + if (attribute.IsReadOnly()) + { + info->writePrivilege = RequiredPrivilege::ForWriteAttribute(path); + } + + info->flags.Set(InteractionModel::AttributeQualityFlags::kListAttribute, (attribute.attributeType == ZCL_ARRAY_ATTRIBUTE_TYPE)); + info->flags.Set(InteractionModel::AttributeQualityFlags::kTimed, attribute.MustUseTimedWrite()); + + // NOTE: we do NOT provide additional info for: + // - IsExternal/IsSingleton/IsAutomaticallyPersisted is not used by IM handling + // - IsSingleton spec defines it for CLUSTERS where as we have it for ATTRIBUTES + // - Several specification flags are not available (reportable, quieter reporting, + // fixed, source attribution) + + // TODO: Set additional flags: + // info->flags.Set(InteractionModel::AttributeQualityFlags::kFabricScoped) + // info->flags.Set(InteractionModel::AttributeQualityFlags::kFabricSensitive) + // info->flags.Set(InteractionModel::AttributeQualityFlags::kChangesOmitted) +} + +InteractionModel::AttributeEntry AttributeEntryFrom(const ConcreteClusterPath & clusterPath, + const EmberAfAttributeMetadata & attribute) +{ + InteractionModel::AttributeEntry entry; + + entry.path = ConcreteAttributePath(clusterPath.mEndpointId, clusterPath.mClusterId, attribute.attributeId); + LoadAttributeInfo(entry.path, attribute, &entry.info); + + return entry; +} + +InteractionModel::CommandEntry CommandEntryFrom(const ConcreteClusterPath & clusterPath, CommandId clusterCommandId) +{ + InteractionModel::CommandEntry entry; + entry.path = ConcreteCommandPath(clusterPath.mEndpointId, clusterPath.mClusterId, clusterCommandId); + entry.info.invokePrivilege = RequiredPrivilege::ForInvokeCommand(entry.path); + + entry.info.flags.Set(InteractionModel::CommandQualityFlags::kTimed, + CommandNeedsTimedInvoke(clusterPath.mClusterId, clusterCommandId)); + + entry.info.flags.Set(InteractionModel::CommandQualityFlags::kFabricScoped, + CommandIsFabricScoped(clusterPath.mClusterId, clusterCommandId)); + + return entry; +} + +const ConcreteCommandPath kInvalidCommandPath(kInvalidEndpointId, kInvalidClusterId, kInvalidCommandId); + +} // namespace + +std::optional CodegenDataModel::EmberCommandListIterator::First(const CommandId * list) +{ + VerifyOrReturnValue(list != nullptr, std::nullopt); + mCurrentList = mCurrentHint = list; + + VerifyOrReturnValue(*mCurrentList != kInvalidCommandId, std::nullopt); + return *mCurrentList; +} + +std::optional CodegenDataModel::EmberCommandListIterator::Next(const CommandId * list, CommandId previousId) +{ + VerifyOrReturnValue(list != nullptr, std::nullopt); + VerifyOrReturnValue(previousId != kInvalidCommandId, std::nullopt); + + if (mCurrentList != list) + { + // invalidate the hint if switching lists... + mCurrentHint = nullptr; + mCurrentList = list; + } + + if ((mCurrentHint == nullptr) || (*mCurrentHint != previousId)) + { + // we did not find a usable hint. Search from the to set the hint + mCurrentHint = mCurrentList; + while ((*mCurrentHint != kInvalidCommandId) && (*mCurrentHint != previousId)) + { + mCurrentHint++; + } + } + + VerifyOrReturnValue(*mCurrentHint == previousId, std::nullopt); + + // hint is valid and can be used immediately + mCurrentHint++; // this is the next value + return (*mCurrentHint == kInvalidCommandId) ? std::nullopt : std::make_optional(*mCurrentHint); +} + +bool CodegenDataModel::EmberCommandListIterator::Exists(const CommandId * list, CommandId toCheck) +{ + VerifyOrReturnValue(list != nullptr, false); + VerifyOrReturnValue(toCheck != kInvalidCommandId, false); + + if (mCurrentList != list) + { + // invalidate the hint if switching lists... + mCurrentHint = nullptr; + mCurrentList = list; + } + + // maybe already positioned correctly + if ((mCurrentHint != nullptr) && (*mCurrentHint == toCheck)) + { + return true; + } + + // move and try to find it + mCurrentHint = mCurrentList; + while ((*mCurrentHint != kInvalidCommandId) && (*mCurrentHint != toCheck)) + { + mCurrentHint++; + } + + return (*mCurrentHint == toCheck); +} + +CHIP_ERROR CodegenDataModel::ReadAttribute(const InteractionModel::ReadAttributeRequest & request, + InteractionModel::ReadState & state, AttributeValueEncoder & encoder) +{ + // TODO: this needs an implementation + return CHIP_ERROR_NOT_IMPLEMENTED; +} + +CHIP_ERROR CodegenDataModel::WriteAttribute(const InteractionModel::WriteAttributeRequest & request, + AttributeValueDecoder & decoder) +{ + // TODO: this needs an implementation + return CHIP_ERROR_NOT_IMPLEMENTED; +} + +CHIP_ERROR CodegenDataModel::Invoke(const InteractionModel::InvokeRequest & request, TLV::TLVReader & input_arguments, + InteractionModel::InvokeReply & reply) +{ + // TODO: this needs an implementation + return CHIP_ERROR_NOT_IMPLEMENTED; +} + +EndpointId CodegenDataModel::FirstEndpoint() +{ + // find the first enabled index + const uint16_t lastEndpointIndex = emberAfEndpointCount(); + for (uint16_t endpoint_idx = 0; endpoint_idx < lastEndpointIndex; endpoint_idx++) + { + if (emberAfEndpointIndexIsEnabled(endpoint_idx)) + { + mEndpointIterationHint = endpoint_idx; + return emberAfEndpointFromIndex(endpoint_idx); + } + } + + // No enabled endpoint found. Give up + return kInvalidEndpointId; +} + +std::optional CodegenDataModel::TryFindEndpointIndex(EndpointId id) const +{ + const uint16_t lastEndpointIndex = emberAfEndpointCount(); + + if ((mEndpointIterationHint < lastEndpointIndex) && emberAfEndpointIndexIsEnabled(mEndpointIterationHint) && + (id == emberAfEndpointFromIndex(mEndpointIterationHint))) + { + return std::make_optional(mEndpointIterationHint); + } + + // Linear search, this may be slow + uint16_t idx = emberAfIndexFromEndpoint(id); + if (idx == kEmberInvalidEndpointIndex) + { + return std::nullopt; + } + + return std::make_optional(idx); +} + +EndpointId CodegenDataModel::NextEndpoint(EndpointId before) +{ + const unsigned lastEndpointIndex = emberAfEndpointCount(); + + std::optional before_idx = TryFindEndpointIndex(before); + if (!before_idx.has_value()) + { + return kInvalidEndpointId; + } + + // find the first enabled index + for (uint16_t endpoint_idx = static_cast(*before_idx + 1); endpoint_idx < lastEndpointIndex; endpoint_idx++) + { + if (emberAfEndpointIndexIsEnabled(endpoint_idx)) + { + mEndpointIterationHint = endpoint_idx; + return emberAfEndpointFromIndex(endpoint_idx); + } + } + + // No enabled enpoint after "before" was found, give up + return kInvalidEndpointId; +} + +InteractionModel::ClusterEntry CodegenDataModel::FirstCluster(EndpointId endpointId) +{ + const EmberAfEndpointType * endpoint = emberAfFindEndpointType(endpointId); + VerifyOrReturnValue(endpoint != nullptr, InteractionModel::ClusterEntry::kInvalid); + VerifyOrReturnValue(endpoint->clusterCount > 0, InteractionModel::ClusterEntry::kInvalid); + VerifyOrReturnValue(endpoint->cluster != nullptr, InteractionModel::ClusterEntry::kInvalid); + + return FirstServerClusterEntry(endpointId, endpoint, 0, mClusterIterationHint); +} + +std::optional CodegenDataModel::TryFindServerClusterIndex(const EmberAfEndpointType * endpoint, ClusterId id) const +{ + const unsigned clusterCount = endpoint->clusterCount; + + if (mClusterIterationHint < clusterCount) + { + const EmberAfCluster & cluster = endpoint->cluster[mClusterIterationHint]; + if (cluster.IsServer() && (cluster.clusterId == id)) + { + return std::make_optional(mClusterIterationHint); + } + } + + // linear search, this may be slow + // does NOT use emberAfClusterIndex to not iterate over endpoints as we have + // already found the correct endpoint + for (unsigned cluster_idx = 0; cluster_idx < clusterCount; cluster_idx++) + { + const EmberAfCluster & cluster = endpoint->cluster[cluster_idx]; + if (cluster.IsServer() && (cluster.clusterId == id)) + { + return std::make_optional(cluster_idx); + } + } + + return std::nullopt; +} + +InteractionModel::ClusterEntry CodegenDataModel::NextCluster(const ConcreteClusterPath & before) +{ + // TODO: This search still seems slow (ember will loop). Should use index hints as long + // as ember API supports it + const EmberAfEndpointType * endpoint = emberAfFindEndpointType(before.mEndpointId); + + VerifyOrReturnValue(endpoint != nullptr, InteractionModel::ClusterEntry::kInvalid); + VerifyOrReturnValue(endpoint->clusterCount > 0, InteractionModel::ClusterEntry::kInvalid); + VerifyOrReturnValue(endpoint->cluster != nullptr, InteractionModel::ClusterEntry::kInvalid); + + std::optional cluster_idx = TryFindServerClusterIndex(endpoint, before.mClusterId); + if (!cluster_idx.has_value()) + { + return InteractionModel::ClusterEntry::kInvalid; + } + + return FirstServerClusterEntry(before.mEndpointId, endpoint, *cluster_idx + 1, mClusterIterationHint); +} + +std::optional CodegenDataModel::GetClusterInfo(const ConcreteClusterPath & path) +{ + const EmberAfCluster * cluster = FindServerCluster(path); + + VerifyOrReturnValue(cluster != nullptr, std::nullopt); + + auto info = LoadClusterInfo(path, *cluster); + + if (CHIP_ERROR * err = std::get_if(&info)) + { +#if CHIP_ERROR_LOGGING + ChipLogError(AppServer, "Failed to load cluster info: %" CHIP_ERROR_FORMAT, err->Format()); +#else + (void) err->Format(); +#endif + return std::nullopt; + } + + return std::make_optional(std::get(info)); +} + +InteractionModel::AttributeEntry CodegenDataModel::FirstAttribute(const ConcreteClusterPath & path) +{ + const EmberAfCluster * cluster = FindServerCluster(path); + + VerifyOrReturnValue(cluster != nullptr, InteractionModel::AttributeEntry::kInvalid); + VerifyOrReturnValue(cluster->attributeCount > 0, InteractionModel::AttributeEntry::kInvalid); + VerifyOrReturnValue(cluster->attributes != nullptr, InteractionModel::AttributeEntry::kInvalid); + + mAttributeIterationHint = 0; + return AttributeEntryFrom(path, cluster->attributes[0]); +} + +std::optional CodegenDataModel::TryFindAttributeIndex(const EmberAfCluster * cluster, AttributeId id) const +{ + const unsigned attributeCount = cluster->attributeCount; + + // attempt to find this based on the embedded hint + if ((mAttributeIterationHint < attributeCount) && (cluster->attributes[mAttributeIterationHint].attributeId == id)) + { + return std::make_optional(mAttributeIterationHint); + } + + // linear search is required. This may be slow + for (unsigned attribute_idx = 0; attribute_idx < attributeCount; attribute_idx++) + { + + if (cluster->attributes[attribute_idx].attributeId == id) + { + return std::make_optional(attribute_idx); + } + } + + return std::nullopt; +} + +const EmberAfCluster * CodegenDataModel::FindServerCluster(const ConcreteClusterPath & path) +{ + // cache things + if (mPreviouslyFoundCluster.has_value() && (mPreviouslyFoundCluster->path == path)) + { + return mPreviouslyFoundCluster->cluster; + } + + const EmberAfCluster * cluster = emberAfFindServerCluster(path.mEndpointId, path.mClusterId); + if (cluster != nullptr) + { + mPreviouslyFoundCluster = std::make_optional(path, cluster); + } + return cluster; +} + +InteractionModel::AttributeEntry CodegenDataModel::NextAttribute(const ConcreteAttributePath & before) +{ + const EmberAfCluster * cluster = FindServerCluster(before); + VerifyOrReturnValue(cluster != nullptr, InteractionModel::AttributeEntry::kInvalid); + VerifyOrReturnValue(cluster->attributeCount > 0, InteractionModel::AttributeEntry::kInvalid); + VerifyOrReturnValue(cluster->attributes != nullptr, InteractionModel::AttributeEntry::kInvalid); + + // find the given attribute in the list and then return the next one + std::optional attribute_idx = TryFindAttributeIndex(cluster, before.mAttributeId); + if (!attribute_idx.has_value()) + { + return InteractionModel::AttributeEntry::kInvalid; + } + + unsigned next_idx = *attribute_idx + 1; + if (next_idx < cluster->attributeCount) + { + mAttributeIterationHint = next_idx; + return AttributeEntryFrom(before, cluster->attributes[next_idx]); + } + + // iteration complete + return InteractionModel::AttributeEntry::kInvalid; +} + +std::optional CodegenDataModel::GetAttributeInfo(const ConcreteAttributePath & path) +{ + const EmberAfCluster * cluster = FindServerCluster(path); + + VerifyOrReturnValue(cluster != nullptr, std::nullopt); + VerifyOrReturnValue(cluster->attributeCount > 0, std::nullopt); + VerifyOrReturnValue(cluster->attributes != nullptr, std::nullopt); + + std::optional attribute_idx = TryFindAttributeIndex(cluster, path.mAttributeId); + + if (!attribute_idx.has_value()) + { + return std::nullopt; + } + + InteractionModel::AttributeInfo info; + LoadAttributeInfo(path, cluster->attributes[*attribute_idx], &info); + return std::make_optional(info); +} + +InteractionModel::CommandEntry CodegenDataModel::FirstAcceptedCommand(const ConcreteClusterPath & path) +{ + const EmberAfCluster * cluster = FindServerCluster(path); + + VerifyOrReturnValue(cluster != nullptr, InteractionModel::CommandEntry::kInvalid); + + std::optional commandId = mAcceptedCommandsIterator.First(cluster->acceptedCommandList); + VerifyOrReturnValue(commandId.has_value(), InteractionModel::CommandEntry::kInvalid); + + return CommandEntryFrom(path, *commandId); +} + +InteractionModel::CommandEntry CodegenDataModel::NextAcceptedCommand(const ConcreteCommandPath & before) +{ + const EmberAfCluster * cluster = FindServerCluster(before); + + VerifyOrReturnValue(cluster != nullptr, InteractionModel::CommandEntry::kInvalid); + + std::optional commandId = mAcceptedCommandsIterator.Next(cluster->acceptedCommandList, before.mCommandId); + VerifyOrReturnValue(commandId.has_value(), InteractionModel::CommandEntry::kInvalid); + + return CommandEntryFrom(before, *commandId); +} + +std::optional CodegenDataModel::GetAcceptedCommandInfo(const ConcreteCommandPath & path) +{ + const EmberAfCluster * cluster = FindServerCluster(path); + + VerifyOrReturnValue(cluster != nullptr, std::nullopt); + VerifyOrReturnValue(mAcceptedCommandsIterator.Exists(cluster->acceptedCommandList, path.mCommandId), std::nullopt); + + return CommandEntryFrom(path, path.mCommandId).info; +} + +ConcreteCommandPath CodegenDataModel::FirstGeneratedCommand(const ConcreteClusterPath & path) +{ + const EmberAfCluster * cluster = FindServerCluster(path); + + VerifyOrReturnValue(cluster != nullptr, kInvalidCommandPath); + + std::optional commandId = mGeneratedCommandsIterator.First(cluster->generatedCommandList); + VerifyOrReturnValue(commandId.has_value(), kInvalidCommandPath); + return ConcreteCommandPath(path.mEndpointId, path.mClusterId, *commandId); +} + +ConcreteCommandPath CodegenDataModel::NextGeneratedCommand(const ConcreteCommandPath & before) +{ + const EmberAfCluster * cluster = FindServerCluster(before); + + VerifyOrReturnValue(cluster != nullptr, kInvalidCommandPath); + + std::optional commandId = mGeneratedCommandsIterator.Next(cluster->generatedCommandList, before.mCommandId); + VerifyOrReturnValue(commandId.has_value(), kInvalidCommandPath); + + return ConcreteCommandPath(before.mEndpointId, before.mClusterId, *commandId); +} + +} // namespace app +} // namespace chip diff --git a/src/app/codegen-interaction-model/CodegenDataModel.h b/src/app/codegen-interaction-model/CodegenDataModel.h new file mode 100644 index 00000000000000..43117fa48d2312 --- /dev/null +++ b/src/app/codegen-interaction-model/CodegenDataModel.h @@ -0,0 +1,132 @@ +/* + * Copyright (c) 2024 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 + +namespace chip { +namespace app { + +/// An implementation of `InteractionModel::Model` that relies on code-generation +/// via zap/ember. +/// +/// The Ember framework uses generated files (like endpoint-config.h and various +/// other generated metadata) to provide a cluster model. +/// +/// This class will use global functions generally residing in `app/util` +/// as well as application-specific overrides to provide data model functionality. +/// +/// Given that this relies on global data at link time, there generally can be +/// only one CodegenDataModel per application (you can create more instances, +/// however they would share the exact same underlying data and storage). +class CodegenDataModel : public chip::app::InteractionModel::Model +{ +private: + /// Ember commands are stored as a `CommandId *` pointer that is either null (i.e. no commands) + /// or is terminated with 0xFFFF_FFFF aka kInvalidCommandId + /// + /// Since iterator implementations in the data model use Next(before_path) calls, iterating + /// such lists from the beginning would be very inefficient as O(n^2). + /// + /// This class maintains a cached position inside such iteration, such that `Next` calls + /// can be faster. + class EmberCommandListIterator + { + private: + const CommandId * mCurrentList = nullptr; + const CommandId * mCurrentHint = nullptr; // Invariant: mCurrentHint is INSIDE mCurrentList + public: + EmberCommandListIterator() = default; + + /// Returns the first command in the given list (or nullopt if list is null or starts with 0xFFFFFFF) + std::optional First(const CommandId * list); + + /// Returns the command after `previousId` in the given list + std::optional Next(const CommandId * list, CommandId previousId); + + /// Checks if the given command id exists in the given list + bool Exists(const CommandId * list, CommandId toCheck); + }; + +public: + /// Generic model implementations + CHIP_ERROR Shutdown() override { return CHIP_NO_ERROR; } + + CHIP_ERROR ReadAttribute(const InteractionModel::ReadAttributeRequest & request, InteractionModel::ReadState & state, + AttributeValueEncoder & encoder) override; + CHIP_ERROR WriteAttribute(const InteractionModel::WriteAttributeRequest & request, AttributeValueDecoder & decoder) override; + CHIP_ERROR Invoke(const InteractionModel::InvokeRequest & request, chip::TLV::TLVReader & input_arguments, + InteractionModel::InvokeReply & reply) override; + + /// attribute tree iteration + EndpointId FirstEndpoint() override; + EndpointId NextEndpoint(EndpointId before) override; + + InteractionModel::ClusterEntry FirstCluster(EndpointId endpoint) override; + InteractionModel::ClusterEntry NextCluster(const ConcreteClusterPath & before) override; + std::optional GetClusterInfo(const ConcreteClusterPath & path) override; + + InteractionModel::AttributeEntry FirstAttribute(const ConcreteClusterPath & cluster) override; + InteractionModel::AttributeEntry NextAttribute(const ConcreteAttributePath & before) override; + std::optional GetAttributeInfo(const ConcreteAttributePath & path) override; + + InteractionModel::CommandEntry FirstAcceptedCommand(const ConcreteClusterPath & cluster) override; + InteractionModel::CommandEntry NextAcceptedCommand(const ConcreteCommandPath & before) override; + std::optional GetAcceptedCommandInfo(const ConcreteCommandPath & path) override; + + ConcreteCommandPath FirstGeneratedCommand(const ConcreteClusterPath & cluster) override; + ConcreteCommandPath NextGeneratedCommand(const ConcreteCommandPath & before) override; + +private: + // Iteration is often done in a tight loop going through all values. + // To avoid N^2 iterations, cache a hint of where something is positioned + uint16_t mEndpointIterationHint = 0; + unsigned mClusterIterationHint = 0; + unsigned mAttributeIterationHint = 0; + EmberCommandListIterator mAcceptedCommandsIterator; + EmberCommandListIterator mGeneratedCommandsIterator; + + // represents a remembered cluster reference that has been found as + // looking for clusters is very common (for every attribute iteration) + struct ClusterReference + { + ConcreteClusterPath path; + const EmberAfCluster * cluster; + + ClusterReference(const ConcreteClusterPath p, const EmberAfCluster * c) : path(p), cluster(c) {} + }; + std::optional mPreviouslyFoundCluster; + + /// Finds the specified ember cluster + /// + /// Effectively the same as `emberAfFindServerCluster` except with some caching capabilities + const EmberAfCluster * FindServerCluster(const ConcreteClusterPath & path); + + /// Find the index of the given attribute id + std::optional TryFindAttributeIndex(const EmberAfCluster * cluster, chip::AttributeId id) const; + + /// Find the index of the given cluster id + std::optional TryFindServerClusterIndex(const EmberAfEndpointType * endpoint, chip::ClusterId id) const; + + /// Find the index of the given endpoint id + std::optional TryFindEndpointIndex(chip::EndpointId id) const; +}; + +} // namespace app +} // namespace chip diff --git a/src/app/codegen-interaction-model/model.gni b/src/app/codegen-interaction-model/model.gni new file mode 100644 index 00000000000000..d1c4e85b90b433 --- /dev/null +++ b/src/app/codegen-interaction-model/model.gni @@ -0,0 +1,35 @@ +# Copyright (c) 2024 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") + +# The sources in this directory are TIGHTLY coupled with code-generated data models +# as generally implemented by `src/app/util` +# +# Corresponding functions defined in attribute-storace.cpp/attribute-table.cpp must +# be available at link time for this model to use and constants heavily depend +# on `zap-generated/endpoint_config.h` (generally compile-time constants that +# are code generated) +# +# As a result, the files here are NOT a source_set or similar because they cannot +# be cleanly built as a stand-alone and instead have to be imported as part of +# a different data model or compilation unit. +codegen_interaction_model_SOURCES = [ + "${chip_root}/src/app/codegen-interaction-model/CodegenDataModel.h", + "${chip_root}/src/app/codegen-interaction-model/CodegenDataModel.cpp", +] + +codegen_interaction_model_PUBLIC_DEPS = [ + "${chip_root}/src/app/common:attribute-type", + "${chip_root}/src/app/interaction-model", +] diff --git a/src/app/codegen-interaction-model/tests/BUILD.gn b/src/app/codegen-interaction-model/tests/BUILD.gn new file mode 100644 index 00000000000000..f543bc8d0cc24b --- /dev/null +++ b/src/app/codegen-interaction-model/tests/BUILD.gn @@ -0,0 +1,35 @@ +# Copyright (c) 2024 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") +import("${chip_root}/build/chip/chip_test_suite.gni") +import("${chip_root}/src/app/codegen-interaction-model/model.gni") + +source_set("mock_model") { + sources = codegen_interaction_model_SOURCES + + public_deps = codegen_interaction_model_PUBLIC_DEPS + + # this ties in the codegen model to an actual ember implementation + public_deps += [ "${chip_root}/src/app/util/mock:mock_ember" ] +} + +chip_test_suite("tests") { + output_name = "libCodegenInteractionModelTests" + + test_sources = [ "TestCodegenModelViaMocks.cpp" ] + + cflags = [ "-Wconversion" ] + + public_deps = [ ":mock_model" ] +} diff --git a/src/app/codegen-interaction-model/tests/TestCodegenModelViaMocks.cpp b/src/app/codegen-interaction-model/tests/TestCodegenModelViaMocks.cpp new file mode 100644 index 00000000000000..e8175bf8d3f340 --- /dev/null +++ b/src/app/codegen-interaction-model/tests/TestCodegenModelViaMocks.cpp @@ -0,0 +1,488 @@ +/* + * + * Copyright (c) 2024 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 + +using namespace chip; +using namespace chip::Test; +using namespace chip::app; +using namespace chip::app::InteractionModel; +using namespace chip::app::Clusters::Globals::Attributes; + +namespace { + +constexpr EndpointId kEndpointIdThatIsMissing = kMockEndpointMin - 1; + +static_assert(kEndpointIdThatIsMissing != kInvalidEndpointId); +static_assert(kEndpointIdThatIsMissing != kMockEndpoint1); +static_assert(kEndpointIdThatIsMissing != kMockEndpoint2); +static_assert(kEndpointIdThatIsMissing != kMockEndpoint3); + +// clang-format off +const MockNodeConfig gTestNodeConfig({ + MockEndpointConfig(kMockEndpoint1, { + MockClusterConfig(MockClusterId(1), { + ClusterRevision::Id, FeatureMap::Id, + }, { + MockEventId(1), MockEventId(2), + }), + MockClusterConfig(MockClusterId(2), { + ClusterRevision::Id, FeatureMap::Id, MockAttributeId(1), + }), + }), + MockEndpointConfig(kMockEndpoint2, { + MockClusterConfig(MockClusterId(1), { + ClusterRevision::Id, FeatureMap::Id, + }), + MockClusterConfig( + MockClusterId(2), + { + ClusterRevision::Id, + FeatureMap::Id, + MockAttributeId(1), + MockAttributeConfig(MockAttributeId(2), ZCL_ARRAY_ATTRIBUTE_TYPE), + }, /* attributes */ + {}, /* events */ + {1, 2, 23}, /* acceptedCommands */ + {2, 10} /* generatedCommands */ + ), + MockClusterConfig( + MockClusterId(3), + { + ClusterRevision::Id, FeatureMap::Id, MockAttributeId(1), MockAttributeId(2), MockAttributeId(3), + }, /* attributes */ + {}, /* events */ + {11}, /* acceptedCommands */ + {4, 6} /* generatedCommands */ + ), + }), + MockEndpointConfig(kMockEndpoint3, { + MockClusterConfig(MockClusterId(1), { + ClusterRevision::Id, FeatureMap::Id, MockAttributeId(1), + }), + MockClusterConfig(MockClusterId(2), { + ClusterRevision::Id, FeatureMap::Id, MockAttributeId(1), MockAttributeId(2), MockAttributeId(3), MockAttributeId(4), + }), + MockClusterConfig(MockClusterId(3), { + ClusterRevision::Id, FeatureMap::Id, + }), + MockClusterConfig(MockClusterId(4), { + ClusterRevision::Id, FeatureMap::Id, + }), + }), +}); +// clang-format on + +struct UseMockNodeConfig +{ + UseMockNodeConfig(const MockNodeConfig & config) { SetMockNodeConfig(config); } + ~UseMockNodeConfig() { ResetMockNodeConfig(); } +}; + +} // namespace + +TEST(TestCodegenModelViaMocks, IterateOverEndpoints) +{ + UseMockNodeConfig config(gTestNodeConfig); + chip::app::CodegenDataModel model; + + // This iteration relies on the hard-coding that occurs when mock_ember is used + EXPECT_EQ(model.FirstEndpoint(), kMockEndpoint1); + EXPECT_EQ(model.NextEndpoint(kMockEndpoint1), kMockEndpoint2); + EXPECT_EQ(model.NextEndpoint(kMockEndpoint2), kMockEndpoint3); + EXPECT_EQ(model.NextEndpoint(kMockEndpoint3), kInvalidEndpointId); + + /// Some out of order requests should work as well + EXPECT_EQ(model.NextEndpoint(kMockEndpoint2), kMockEndpoint3); + EXPECT_EQ(model.NextEndpoint(kMockEndpoint2), kMockEndpoint3); + EXPECT_EQ(model.NextEndpoint(kMockEndpoint1), kMockEndpoint2); + EXPECT_EQ(model.NextEndpoint(kMockEndpoint1), kMockEndpoint2); + EXPECT_EQ(model.NextEndpoint(kMockEndpoint2), kMockEndpoint3); + EXPECT_EQ(model.NextEndpoint(kMockEndpoint1), kMockEndpoint2); + EXPECT_EQ(model.NextEndpoint(kMockEndpoint3), kInvalidEndpointId); + EXPECT_EQ(model.NextEndpoint(kMockEndpoint3), kInvalidEndpointId); + EXPECT_EQ(model.FirstEndpoint(), kMockEndpoint1); + EXPECT_EQ(model.FirstEndpoint(), kMockEndpoint1); + + // invalid endpoiunts + EXPECT_EQ(model.NextEndpoint(kInvalidEndpointId), kInvalidEndpointId); + EXPECT_EQ(model.NextEndpoint(987u), kInvalidEndpointId); +} + +TEST(TestCodegenModelViaMocks, IterateOverClusters) +{ + UseMockNodeConfig config(gTestNodeConfig); + chip::app::CodegenDataModel model; + + chip::Test::ResetVersion(); + + EXPECT_FALSE(model.FirstCluster(kEndpointIdThatIsMissing).path.HasValidIds()); + EXPECT_FALSE(model.FirstCluster(kInvalidEndpointId).path.HasValidIds()); + EXPECT_FALSE(model.NextCluster(ConcreteClusterPath(kInvalidEndpointId, 123)).path.HasValidIds()); + EXPECT_FALSE(model.NextCluster(ConcreteClusterPath(kMockEndpoint1, kInvalidClusterId)).path.HasValidIds()); + EXPECT_FALSE(model.NextCluster(ConcreteClusterPath(kMockEndpoint1, 981u)).path.HasValidIds()); + + // mock endpoint 1 has 2 mock clusters: 1 and 2 + ClusterEntry entry = model.FirstCluster(kMockEndpoint1); + ASSERT_TRUE(entry.path.HasValidIds()); + EXPECT_EQ(entry.path.mEndpointId, kMockEndpoint1); + EXPECT_EQ(entry.path.mClusterId, MockClusterId(1)); + EXPECT_EQ(entry.info.dataVersion, 0u); + EXPECT_EQ(entry.info.flags.Raw(), 0u); + + chip::Test::BumpVersion(); + + entry = model.NextCluster(entry.path); + ASSERT_TRUE(entry.path.HasValidIds()); + EXPECT_EQ(entry.path.mEndpointId, kMockEndpoint1); + EXPECT_EQ(entry.path.mClusterId, MockClusterId(2)); + EXPECT_EQ(entry.info.dataVersion, 1u); + EXPECT_EQ(entry.info.flags.Raw(), 0u); + + entry = model.NextCluster(entry.path); + EXPECT_FALSE(entry.path.HasValidIds()); + + // mock endpoint 3 has 4 mock clusters: 1 through 4 + entry = model.FirstCluster(kMockEndpoint3); + for (uint16_t clusterId = 1; clusterId <= 4; clusterId++) + { + ASSERT_TRUE(entry.path.HasValidIds()); + EXPECT_EQ(entry.path.mEndpointId, kMockEndpoint3); + EXPECT_EQ(entry.path.mClusterId, MockClusterId(clusterId)); + entry = model.NextCluster(entry.path); + } + EXPECT_FALSE(entry.path.HasValidIds()); + + // repeat calls should work + for (int i = 0; i < 10; i++) + { + entry = model.FirstCluster(kMockEndpoint1); + ASSERT_TRUE(entry.path.HasValidIds()); + EXPECT_EQ(entry.path.mEndpointId, kMockEndpoint1); + EXPECT_EQ(entry.path.mClusterId, MockClusterId(1)); + } + + for (int i = 0; i < 10; i++) + { + ClusterEntry nextEntry = model.NextCluster(entry.path); + ASSERT_TRUE(nextEntry.path.HasValidIds()); + EXPECT_EQ(nextEntry.path.mEndpointId, kMockEndpoint1); + EXPECT_EQ(nextEntry.path.mClusterId, MockClusterId(2)); + } +} + +TEST(TestCodegenModelViaMocks, GetClusterInfo) +{ + + UseMockNodeConfig config(gTestNodeConfig); + chip::app::CodegenDataModel model; + + chip::Test::ResetVersion(); + + ASSERT_FALSE(model.GetClusterInfo(ConcreteClusterPath(kInvalidEndpointId, kInvalidClusterId)).has_value()); + ASSERT_FALSE(model.GetClusterInfo(ConcreteClusterPath(kInvalidEndpointId, MockClusterId(1))).has_value()); + ASSERT_FALSE(model.GetClusterInfo(ConcreteClusterPath(kMockEndpoint1, kInvalidClusterId)).has_value()); + ASSERT_FALSE(model.GetClusterInfo(ConcreteClusterPath(kMockEndpoint1, MockClusterId(10))).has_value()); + + // now get the value + std::optional info = model.GetClusterInfo(ConcreteClusterPath(kMockEndpoint1, MockClusterId(1))); + ASSERT_TRUE(info.has_value()); + EXPECT_EQ(info->dataVersion, 0u); // NOLINT(bugprone-unchecked-optional-access) + EXPECT_EQ(info->flags.Raw(), 0u); // NOLINT(bugprone-unchecked-optional-access) + + chip::Test::BumpVersion(); + info = model.GetClusterInfo(ConcreteClusterPath(kMockEndpoint1, MockClusterId(1))); + ASSERT_TRUE(info.has_value()); + EXPECT_EQ(info->dataVersion, 1u); // NOLINT(bugprone-unchecked-optional-access) + EXPECT_EQ(info->flags.Raw(), 0u); // NOLINT(bugprone-unchecked-optional-access) +} + +TEST(TestCodegenModelViaMocks, IterateOverAttributes) +{ + UseMockNodeConfig config(gTestNodeConfig); + chip::app::CodegenDataModel model; + + // invalid paths should return in "no more data" + ASSERT_FALSE(model.FirstAttribute(ConcreteClusterPath(kEndpointIdThatIsMissing, MockClusterId(1))).path.HasValidIds()); + ASSERT_FALSE(model.FirstAttribute(ConcreteClusterPath(kInvalidEndpointId, MockClusterId(1))).path.HasValidIds()); + ASSERT_FALSE(model.FirstAttribute(ConcreteClusterPath(kMockEndpoint1, MockClusterId(10))).path.HasValidIds()); + ASSERT_FALSE(model.FirstAttribute(ConcreteClusterPath(kMockEndpoint1, kInvalidClusterId)).path.HasValidIds()); + + ASSERT_FALSE(model.NextAttribute(ConcreteAttributePath(kEndpointIdThatIsMissing, MockClusterId(1), 1u)).path.HasValidIds()); + ASSERT_FALSE(model.NextAttribute(ConcreteAttributePath(kInvalidEndpointId, MockClusterId(1), 1u)).path.HasValidIds()); + ASSERT_FALSE(model.NextAttribute(ConcreteAttributePath(kMockEndpoint1, MockClusterId(10), 1u)).path.HasValidIds()); + ASSERT_FALSE(model.NextAttribute(ConcreteAttributePath(kMockEndpoint1, kInvalidClusterId, 1u)).path.HasValidIds()); + ASSERT_FALSE(model.NextAttribute(ConcreteAttributePath(kMockEndpoint1, MockClusterId(1), 987u)).path.HasValidIds()); + + // should be able to iterate over valid paths + AttributeEntry entry = model.FirstAttribute(ConcreteClusterPath(kMockEndpoint2, MockClusterId(2))); + ASSERT_TRUE(entry.path.HasValidIds()); + ASSERT_EQ(entry.path.mEndpointId, kMockEndpoint2); + ASSERT_EQ(entry.path.mClusterId, MockClusterId(2)); + ASSERT_EQ(entry.path.mAttributeId, ClusterRevision::Id); + ASSERT_FALSE(entry.info.flags.Has(AttributeQualityFlags::kListAttribute)); + + entry = model.NextAttribute(entry.path); + ASSERT_TRUE(entry.path.HasValidIds()); + ASSERT_EQ(entry.path.mEndpointId, kMockEndpoint2); + ASSERT_EQ(entry.path.mClusterId, MockClusterId(2)); + ASSERT_EQ(entry.path.mAttributeId, FeatureMap::Id); + ASSERT_FALSE(entry.info.flags.Has(AttributeQualityFlags::kListAttribute)); + + entry = model.NextAttribute(entry.path); + ASSERT_TRUE(entry.path.HasValidIds()); + ASSERT_EQ(entry.path.mEndpointId, kMockEndpoint2); + ASSERT_EQ(entry.path.mClusterId, MockClusterId(2)); + ASSERT_EQ(entry.path.mAttributeId, MockAttributeId(1)); + ASSERT_FALSE(entry.info.flags.Has(AttributeQualityFlags::kListAttribute)); + + entry = model.NextAttribute(entry.path); + ASSERT_TRUE(entry.path.HasValidIds()); + ASSERT_EQ(entry.path.mEndpointId, kMockEndpoint2); + ASSERT_EQ(entry.path.mClusterId, MockClusterId(2)); + ASSERT_EQ(entry.path.mAttributeId, MockAttributeId(2)); + ASSERT_TRUE(entry.info.flags.Has(AttributeQualityFlags::kListAttribute)); + + entry = model.NextAttribute(entry.path); + ASSERT_FALSE(entry.path.HasValidIds()); + + // repeated calls should work + for (int i = 0; i < 10; i++) + { + entry = model.FirstAttribute(ConcreteClusterPath(kMockEndpoint2, MockClusterId(2))); + ASSERT_TRUE(entry.path.HasValidIds()); + ASSERT_EQ(entry.path.mEndpointId, kMockEndpoint2); + ASSERT_EQ(entry.path.mClusterId, MockClusterId(2)); + ASSERT_EQ(entry.path.mAttributeId, ClusterRevision::Id); + ASSERT_FALSE(entry.info.flags.Has(AttributeQualityFlags::kListAttribute)); + } + + for (int i = 0; i < 10; i++) + { + entry = model.NextAttribute(ConcreteAttributePath(kMockEndpoint2, MockClusterId(2), MockAttributeId(1))); + ASSERT_TRUE(entry.path.HasValidIds()); + ASSERT_EQ(entry.path.mEndpointId, kMockEndpoint2); + ASSERT_EQ(entry.path.mClusterId, MockClusterId(2)); + ASSERT_EQ(entry.path.mAttributeId, MockAttributeId(2)); + ASSERT_TRUE(entry.info.flags.Has(AttributeQualityFlags::kListAttribute)); + } +} + +TEST(TestCodegenModelViaMocks, GetAttributeInfo) +{ + UseMockNodeConfig config(gTestNodeConfig); + chip::app::CodegenDataModel model; + + // various non-existent or invalid paths should return no info data + ASSERT_FALSE( + model.GetAttributeInfo(ConcreteAttributePath(kInvalidEndpointId, kInvalidClusterId, kInvalidAttributeId)).has_value()); + ASSERT_FALSE(model.GetAttributeInfo(ConcreteAttributePath(kInvalidEndpointId, kInvalidClusterId, FeatureMap::Id)).has_value()); + ASSERT_FALSE(model.GetAttributeInfo(ConcreteAttributePath(kInvalidEndpointId, MockClusterId(1), FeatureMap::Id)).has_value()); + ASSERT_FALSE(model.GetAttributeInfo(ConcreteAttributePath(kMockEndpoint1, kInvalidClusterId, FeatureMap::Id)).has_value()); + ASSERT_FALSE(model.GetAttributeInfo(ConcreteAttributePath(kMockEndpoint1, MockClusterId(10), FeatureMap::Id)).has_value()); + ASSERT_FALSE(model.GetAttributeInfo(ConcreteAttributePath(kMockEndpoint1, MockClusterId(10), kInvalidAttributeId)).has_value()); + ASSERT_FALSE(model.GetAttributeInfo(ConcreteAttributePath(kMockEndpoint1, MockClusterId(1), MockAttributeId(10))).has_value()); + + // valid info + std::optional info = + model.GetAttributeInfo(ConcreteAttributePath(kMockEndpoint1, MockClusterId(1), FeatureMap::Id)); + ASSERT_TRUE(info.has_value()); + EXPECT_FALSE(info->flags.Has(AttributeQualityFlags::kListAttribute)); // NOLINT(bugprone-unchecked-optional-access) + + info = model.GetAttributeInfo(ConcreteAttributePath(kMockEndpoint2, MockClusterId(2), MockAttributeId(2))); + ASSERT_TRUE(info.has_value()); + EXPECT_TRUE(info->flags.Has(AttributeQualityFlags::kListAttribute)); // NOLINT(bugprone-unchecked-optional-access) +} + +// global attributes are EXPLICITLY not supported +TEST(TestCodegenModelViaMocks, GlobalAttributeInfo) +{ + UseMockNodeConfig config(gTestNodeConfig); + chip::app::CodegenDataModel model; + + std::optional info = model.GetAttributeInfo( + ConcreteAttributePath(kMockEndpoint1, MockClusterId(1), Clusters::Globals::Attributes::GeneratedCommandList::Id)); + + ASSERT_FALSE(info.has_value()); + + info = model.GetAttributeInfo( + ConcreteAttributePath(kMockEndpoint1, MockClusterId(1), Clusters::Globals::Attributes::AttributeList::Id)); + ASSERT_FALSE(info.has_value()); +} + +TEST(TestCodegenModelViaMocks, IterateOverAcceptedCommands) +{ + UseMockNodeConfig config(gTestNodeConfig); + chip::app::CodegenDataModel model; + + // invalid paths should return in "no more data" + ASSERT_FALSE(model.FirstAcceptedCommand(ConcreteClusterPath(kEndpointIdThatIsMissing, MockClusterId(1))).path.HasValidIds()); + ASSERT_FALSE(model.FirstAcceptedCommand(ConcreteClusterPath(kInvalidEndpointId, MockClusterId(1))).path.HasValidIds()); + ASSERT_FALSE(model.FirstAcceptedCommand(ConcreteClusterPath(kMockEndpoint1, MockClusterId(10))).path.HasValidIds()); + ASSERT_FALSE(model.FirstAcceptedCommand(ConcreteClusterPath(kMockEndpoint1, kInvalidClusterId)).path.HasValidIds()); + + // should be able to iterate over valid paths + CommandEntry entry = model.FirstAcceptedCommand(ConcreteClusterPath(kMockEndpoint2, MockClusterId(2))); + ASSERT_TRUE(entry.path.HasValidIds()); + EXPECT_EQ(entry.path.mEndpointId, kMockEndpoint2); + EXPECT_EQ(entry.path.mClusterId, MockClusterId(2)); + EXPECT_EQ(entry.path.mCommandId, 1u); + + entry = model.NextAcceptedCommand(entry.path); + ASSERT_TRUE(entry.path.HasValidIds()); + EXPECT_EQ(entry.path.mEndpointId, kMockEndpoint2); + EXPECT_EQ(entry.path.mClusterId, MockClusterId(2)); + EXPECT_EQ(entry.path.mCommandId, 2u); + + entry = model.NextAcceptedCommand(entry.path); + ASSERT_TRUE(entry.path.HasValidIds()); + EXPECT_EQ(entry.path.mEndpointId, kMockEndpoint2); + EXPECT_EQ(entry.path.mClusterId, MockClusterId(2)); + EXPECT_EQ(entry.path.mCommandId, 23u); + + entry = model.NextAcceptedCommand(entry.path); + ASSERT_FALSE(entry.path.HasValidIds()); + + // attempt some out-of-order requests as well + entry = model.FirstAcceptedCommand(ConcreteClusterPath(kMockEndpoint2, MockClusterId(3))); + ASSERT_TRUE(entry.path.HasValidIds()); + EXPECT_EQ(entry.path.mEndpointId, kMockEndpoint2); + EXPECT_EQ(entry.path.mClusterId, MockClusterId(3)); + EXPECT_EQ(entry.path.mCommandId, 11u); + + for (int i = 0; i < 10; i++) + { + entry = model.NextAcceptedCommand(ConcreteCommandPath(kMockEndpoint2, MockClusterId(2), 2)); + ASSERT_TRUE(entry.path.HasValidIds()); + EXPECT_EQ(entry.path.mEndpointId, kMockEndpoint2); + EXPECT_EQ(entry.path.mClusterId, MockClusterId(2)); + EXPECT_EQ(entry.path.mCommandId, 23u); + } + + for (int i = 0; i < 10; i++) + { + entry = model.NextAcceptedCommand(ConcreteCommandPath(kMockEndpoint2, MockClusterId(2), 1)); + ASSERT_TRUE(entry.path.HasValidIds()); + EXPECT_EQ(entry.path.mEndpointId, kMockEndpoint2); + EXPECT_EQ(entry.path.mClusterId, MockClusterId(2)); + EXPECT_EQ(entry.path.mCommandId, 2u); + } + + for (int i = 0; i < 10; i++) + { + entry = model.NextAcceptedCommand(ConcreteCommandPath(kMockEndpoint2, MockClusterId(3), 10)); + EXPECT_FALSE(entry.path.HasValidIds()); + } +} + +TEST(TestCodegenModelViaMocks, AcceptedCommandInfo) +{ + UseMockNodeConfig config(gTestNodeConfig); + chip::app::CodegenDataModel model; + + // invalid paths should return in "no more data" + ASSERT_FALSE(model.GetAcceptedCommandInfo(ConcreteCommandPath(kEndpointIdThatIsMissing, MockClusterId(1), 1)).has_value()); + ASSERT_FALSE(model.GetAcceptedCommandInfo(ConcreteCommandPath(kInvalidEndpointId, MockClusterId(1), 1)).has_value()); + ASSERT_FALSE(model.GetAcceptedCommandInfo(ConcreteCommandPath(kMockEndpoint1, MockClusterId(10), 1)).has_value()); + ASSERT_FALSE(model.GetAcceptedCommandInfo(ConcreteCommandPath(kMockEndpoint1, kInvalidClusterId, 1)).has_value()); + ASSERT_FALSE( + model.GetAcceptedCommandInfo(ConcreteCommandPath(kMockEndpoint1, MockClusterId(1), kInvalidCommandId)).has_value()); + + std::optional info = model.GetAcceptedCommandInfo(ConcreteCommandPath(kMockEndpoint2, MockClusterId(2), 1u)); + ASSERT_TRUE(info.has_value()); + + info = model.GetAcceptedCommandInfo(ConcreteCommandPath(kMockEndpoint2, MockClusterId(2), 2u)); + ASSERT_TRUE(info.has_value()); + + info = model.GetAcceptedCommandInfo(ConcreteCommandPath(kMockEndpoint2, MockClusterId(2), 1u)); + ASSERT_TRUE(info.has_value()); + + info = model.GetAcceptedCommandInfo(ConcreteCommandPath(kMockEndpoint2, MockClusterId(2), 1u)); + ASSERT_TRUE(info.has_value()); + + info = model.GetAcceptedCommandInfo(ConcreteCommandPath(kMockEndpoint2, MockClusterId(2), 23u)); + ASSERT_TRUE(info.has_value()); + + info = model.GetAcceptedCommandInfo(ConcreteCommandPath(kMockEndpoint2, MockClusterId(2), 1234u)); + ASSERT_FALSE(info.has_value()); +} + +TEST(TestCodegenModelViaMocks, IterateOverGeneratedCommands) +{ + UseMockNodeConfig config(gTestNodeConfig); + chip::app::CodegenDataModel model; + + // invalid paths should return in "no more data" + ASSERT_FALSE(model.FirstGeneratedCommand(ConcreteClusterPath(kEndpointIdThatIsMissing, MockClusterId(1))).HasValidIds()); + ASSERT_FALSE(model.FirstGeneratedCommand(ConcreteClusterPath(kInvalidEndpointId, MockClusterId(1))).HasValidIds()); + ASSERT_FALSE(model.FirstGeneratedCommand(ConcreteClusterPath(kMockEndpoint1, MockClusterId(10))).HasValidIds()); + ASSERT_FALSE(model.FirstGeneratedCommand(ConcreteClusterPath(kMockEndpoint1, kInvalidClusterId)).HasValidIds()); + + // should be able to iterate over valid paths + ConcreteCommandPath path = model.FirstGeneratedCommand(ConcreteClusterPath(kMockEndpoint2, MockClusterId(2))); + ASSERT_TRUE(path.HasValidIds()); + EXPECT_EQ(path.mEndpointId, kMockEndpoint2); + EXPECT_EQ(path.mClusterId, MockClusterId(2)); + EXPECT_EQ(path.mCommandId, 2u); + + path = model.NextGeneratedCommand(path); + ASSERT_TRUE(path.HasValidIds()); + EXPECT_EQ(path.mEndpointId, kMockEndpoint2); + EXPECT_EQ(path.mClusterId, MockClusterId(2)); + EXPECT_EQ(path.mCommandId, 10u); + + path = model.NextGeneratedCommand(path); + ASSERT_FALSE(path.HasValidIds()); + + // attempt some out-of-order requests as well + path = model.FirstGeneratedCommand(ConcreteClusterPath(kMockEndpoint2, MockClusterId(3))); + ASSERT_TRUE(path.HasValidIds()); + EXPECT_EQ(path.mEndpointId, kMockEndpoint2); + EXPECT_EQ(path.mClusterId, MockClusterId(3)); + EXPECT_EQ(path.mCommandId, 4u); + + for (int i = 0; i < 10; i++) + { + path = model.NextGeneratedCommand(ConcreteCommandPath(kMockEndpoint2, MockClusterId(2), 2)); + ASSERT_TRUE(path.HasValidIds()); + EXPECT_EQ(path.mEndpointId, kMockEndpoint2); + EXPECT_EQ(path.mClusterId, MockClusterId(2)); + EXPECT_EQ(path.mCommandId, 10u); + } + + for (int i = 0; i < 10; i++) + { + path = model.NextGeneratedCommand(ConcreteCommandPath(kMockEndpoint2, MockClusterId(3), 4)); + ASSERT_TRUE(path.HasValidIds()); + EXPECT_EQ(path.mEndpointId, kMockEndpoint2); + EXPECT_EQ(path.mClusterId, MockClusterId(3)); + EXPECT_EQ(path.mCommandId, 6u); + } + + for (int i = 0; i < 10; i++) + { + path = model.NextGeneratedCommand(ConcreteCommandPath(kMockEndpoint2, MockClusterId(3), 6)); + EXPECT_FALSE(path.HasValidIds()); + } +} diff --git a/src/app/interaction-model/BUILD.gn b/src/app/interaction-model/BUILD.gn index a0967289c6c65c..19dd3de6c26291 100644 --- a/src/app/interaction-model/BUILD.gn +++ b/src/app/interaction-model/BUILD.gn @@ -19,7 +19,8 @@ source_set("interaction-model") { "Context.h", "Events.h", "InvokeResponder.h", - "IterationTypes.h", + "MetadataTypes.cpp", + "MetadataTypes.h", "Model.h", "OperationTypes.h", "Paths.h", diff --git a/src/app/interaction-model/IterationTypes.h b/src/app/interaction-model/IterationTypes.h deleted file mode 100644 index 441dd3acb7b81f..00000000000000 --- a/src/app/interaction-model/IterationTypes.h +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Copyright (c) 2024 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 - -namespace chip { -namespace app { -namespace InteractionModel { - -enum class ClusterQualityFlags : uint32_t -{ - kDiagnosticsData = 0x0001, // `K` quality, may be filtered out in subscriptions -}; - -struct ClusterInfo -{ - DataVersion dataVersion; // current version of this cluster - BitFlags flags; -}; - -struct ClusterEntry -{ - ConcreteClusterPath path; - ClusterInfo info; -}; - -enum class AttributeQualityFlags : uint32_t -{ - kListAttribute = 0x0001, // This attribute is a list attribute - kChangesOmitted = 0x0002, // `C` quality on attributes -}; - -struct AttributeInfo -{ - BitFlags flags; -}; - -struct AttributeEntry -{ - ConcreteAttributePath path; - AttributeInfo info; -}; - -/// Provides metadata information for a data model -/// -/// The data model can be viewed as a tree of endpoint/cluster/attribute -/// where each element can be iterated through independently -/// -/// Iteration rules: -/// - kInvalidEndpointId will be returned when iteration ends (or generally kInvalid* for paths) -/// - Any internal iteration errors are just logged (callers do not handle iteration CHIP_ERROR) -/// - Iteration order is NOT guaranteed globally. Only the following is guaranteed: -/// - when iterating over an endpoint, ALL clusters of that endpoint will be iterated first, before -/// switching the endpoint (order of clusters themselves not guaranteed) -/// - when iterating over a cluster, ALL attributes of that cluster will be iterated first, before -/// switching to a new cluster -/// - uniqueness and completeness (iterate over all possible distinct values as long as no -/// internal structural changes occur) -class AttributeTreeIterator -{ -public: - virtual ~AttributeTreeIterator() = default; - - virtual EndpointId FirstEndpoint() = 0; - virtual EndpointId NextEndpoint(EndpointId before) = 0; - - virtual ClusterEntry FirstCluster(EndpointId endpoint) = 0; - virtual ClusterEntry NextCluster(const ConcreteClusterPath & before) = 0; - virtual std::optional GetClusterInfo(const ConcreteClusterPath & path) = 0; - - virtual AttributeEntry FirstAttribute(const ConcreteClusterPath & cluster) = 0; - virtual AttributeEntry NextAttribute(const ConcreteAttributePath & before) = 0; - virtual std::optional GetAttributeInfo(const ConcreteAttributePath & path) = 0; -}; - -} // namespace InteractionModel -} // namespace app -} // namespace chip diff --git a/src/app/interaction-model/MetadataTypes.cpp b/src/app/interaction-model/MetadataTypes.cpp new file mode 100644 index 00000000000000..48c2e3db733a52 --- /dev/null +++ b/src/app/interaction-model/MetadataTypes.cpp @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2024 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 + +namespace chip { +namespace app { +namespace InteractionModel { + +const AttributeEntry AttributeEntry::kInvalid{ .path = ConcreteAttributePath(kInvalidEndpointId, kInvalidClusterId, + kInvalidAttributeId) }; + +const CommandEntry CommandEntry::kInvalid{ .path = ConcreteCommandPath(kInvalidEndpointId, kInvalidClusterId, kInvalidCommandId) }; + +const ClusterEntry ClusterEntry::kInvalid{ + .path = ConcreteClusterPath(kInvalidEndpointId, kInvalidClusterId), + .info = ClusterInfo(0 /* version */), // version of invalid cluster entry does not matter +}; + +} // namespace InteractionModel +} // namespace app +} // namespace chip diff --git a/src/app/interaction-model/MetadataTypes.h b/src/app/interaction-model/MetadataTypes.h new file mode 100644 index 00000000000000..5b3c62f0be2247 --- /dev/null +++ b/src/app/interaction-model/MetadataTypes.h @@ -0,0 +1,155 @@ +/* + * Copyright (c) 2024 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 + +namespace chip { +namespace app { +namespace InteractionModel { + +enum class ClusterQualityFlags : uint32_t +{ + kDiagnosticsData = 0x0001, // `K` quality, may be filtered out in subscriptions +}; + +struct ClusterInfo +{ + DataVersion dataVersion; // current cluster data version, + BitFlags flags; + + /// Constructor that marks data version as mandatory + /// for this structure. + ClusterInfo(DataVersion version) : dataVersion(version) {} +}; + +struct ClusterEntry +{ + ConcreteClusterPath path; + ClusterInfo info; + + bool IsValid() const { return path.HasValidIds(); } + + static const ClusterEntry kInvalid; +}; + +enum class AttributeQualityFlags : uint32_t +{ + kListAttribute = 0x0004, // This attribute is a list attribute + kFabricScoped = 0x0008, // 'F' quality on attributes + kFabricSensitive = 0x0010, // 'S' quality on attributes + kChangesOmitted = 0x0020, // `C` quality on attributes + kTimed = 0x0040, // `T` quality on attributes (writes require timed interactions) +}; + +struct AttributeInfo +{ + BitFlags flags; + + // read/write access will be missing if read/write is NOT allowed + std::optional readPrivilege; // generally defaults to View if readable + std::optional writePrivilege; // generally defaults to Operate if writable +}; + +struct AttributeEntry +{ + ConcreteAttributePath path; + AttributeInfo info; + + bool IsValid() const { return path.HasValidIds(); } + + static const AttributeEntry kInvalid; +}; + +enum class CommandQualityFlags : uint32_t +{ + kFabricScoped = 0x0001, + kTimed = 0x0002, // `T` quality on commands +}; + +struct CommandInfo +{ + BitFlags flags; + Access::Privilege invokePrivilege = Access::Privilege::kOperate; +}; + +struct CommandEntry +{ + ConcreteCommandPath path; + CommandInfo info; + + bool IsValid() const { return path.HasValidIds(); } + + static const CommandEntry kInvalid; +}; + +/// Provides metadata information for a data model +/// +/// The data model can be viewed as a tree of endpoint/cluster/(attribute+commands+events) +/// where each element can be iterated through independently. +/// +/// Iteration rules: +/// - Invalid paths will be returned when iteration ends (IDs will be kInvalid* and in particular +/// mEndpointId will be kInvalidEndpointId). See `::kInvalid` constants for entries and +/// can use ::IsValid() to determine if the entry is valid or not. +/// - Global Attributes are NOT returned since they are implied +/// - Any internal iteration errors are just logged (callers do not handle iteration CHIP_ERROR) +/// - Iteration order is NOT guaranteed globally. Only the following is guaranteed: +/// - Complete tree iteration (e.g. when iterating an endpoint, ALL clusters of that endpoint +/// are returned, when iterating over a cluster, all attributes/commands are iterated over) +/// - uniqueness and completeness (iterate over all possible distinct values as long as no +/// internal structural changes occur) +class DataModelMetadataTree +{ +public: + virtual ~DataModelMetadataTree() = default; + + virtual EndpointId FirstEndpoint() = 0; + virtual EndpointId NextEndpoint(EndpointId before) = 0; + + virtual ClusterEntry FirstCluster(EndpointId endpoint) = 0; + virtual ClusterEntry NextCluster(const ConcreteClusterPath & before) = 0; + virtual std::optional GetClusterInfo(const ConcreteClusterPath & path) = 0; + + // Attribute iteration and accessors provide cluster-level access over + // attributes + virtual AttributeEntry FirstAttribute(const ConcreteClusterPath & cluster) = 0; + virtual AttributeEntry NextAttribute(const ConcreteAttributePath & before) = 0; + virtual std::optional GetAttributeInfo(const ConcreteAttributePath & path) = 0; + + // Command iteration and accessors provide cluster-level access over commands + virtual CommandEntry FirstAcceptedCommand(const ConcreteClusterPath & cluster) = 0; + virtual CommandEntry NextAcceptedCommand(const ConcreteCommandPath & before) = 0; + virtual std::optional GetAcceptedCommandInfo(const ConcreteCommandPath & path) = 0; + + // "generated" commands are purely for reporting what types of command ids can be + // returned as responses. + virtual ConcreteCommandPath FirstGeneratedCommand(const ConcreteClusterPath & cluster) = 0; + virtual ConcreteCommandPath NextGeneratedCommand(const ConcreteCommandPath & before) = 0; +}; + +} // namespace InteractionModel +} // namespace app +} // namespace chip diff --git a/src/app/interaction-model/Model.h b/src/app/interaction-model/Model.h index 151065f4c540d2..5ab973901a433b 100644 --- a/src/app/interaction-model/Model.h +++ b/src/app/interaction-model/Model.h @@ -24,7 +24,7 @@ #include #include -#include +#include #include namespace chip { @@ -38,7 +38,7 @@ namespace InteractionModel { /// thread or equivalent /// - class is allowed to attempt to cache indexes/locations for faster /// lookups of things (e.g during iterations) -class Model : public AttributeTreeIterator +class Model : public DataModelMetadataTree { public: virtual ~Model() = default; @@ -77,11 +77,6 @@ class Model : public AttributeTreeIterator /// When this is invoked, caller is expected to have already done some validations: /// - cluster `data version` has been checked for the incoming request if applicable /// - /// List operation support: - /// - the first list write will have `request.writeFlags.Has(WriteFlags::kListBegin)` - /// - the last list write will have `request.writeFlags.Has(WriteFlags::kListEnd)` - /// - the last list write MAY have empty data (no list items) - /// /// When `request.writeFlags.Has(WriteFlags::kForceInternal)` the request is from an internal app update /// and SHOULD bypass some internal checks (like timed enforcement, potentially read-only restrictions) /// diff --git a/src/app/interaction-model/OperationTypes.h b/src/app/interaction-model/OperationTypes.h index 57499a8dbafb49..feb2e173d91600 100644 --- a/src/app/interaction-model/OperationTypes.h +++ b/src/app/interaction-model/OperationTypes.h @@ -85,7 +85,6 @@ enum class InvokeFlags : uint32_t struct InvokeRequest : OperationRequest { ConcreteCommandPath path; - std::optional groupRequestId; // set if and only if this was a group request BitFlags invokeFlags; }; diff --git a/src/app/interaction-model/tests/BUILD.gn b/src/app/interaction-model/tests/BUILD.gn index c7d36b4f1dcdca..4767d61dd12ed1 100644 --- a/src/app/interaction-model/tests/BUILD.gn +++ b/src/app/interaction-model/tests/BUILD.gn @@ -15,6 +15,8 @@ import("//build_overrides/chip.gni") import("${chip_root}/build/chip/chip_test_suite.gni") chip_test_suite("tests") { + output_name = "libIMInterfaceTests" + test_sources = [ "TestEventEmitting.cpp" ] cflags = [ "-Wconversion" ] diff --git a/src/app/util/af-types.h b/src/app/util/af-types.h index c608854c346fba..929ad055d0fc1e 100644 --- a/src/app/util/af-types.h +++ b/src/app/util/af-types.h @@ -23,6 +23,7 @@ * @{ */ +#include "att-storage.h" #include // For bool #include // For various uint*_t types @@ -63,7 +64,7 @@ typedef void (*EmberAfGenericClusterFunction)(void); /** * @brief Struct describing cluster */ -typedef struct +struct EmberAfCluster { /** * ID of cluster according to ZCL spec @@ -116,7 +117,9 @@ typedef struct * Total number of events supported by the cluster instance (in eventList array). */ uint16_t eventCount; -} EmberAfCluster; + + bool IsServer() const { return (mask & CLUSTER_MASK_SERVER) != 0; } +}; /** * @brief Struct that represents a logical device type consisting diff --git a/src/app/util/mock/MockNodeConfig.cpp b/src/app/util/mock/MockNodeConfig.cpp index 79c886f532a8a2..5670966e2ebb29 100644 --- a/src/app/util/mock/MockNodeConfig.cpp +++ b/src/app/util/mock/MockNodeConfig.cpp @@ -54,9 +54,12 @@ const T * findById(const std::vector & vector, decltype(std::declval().id) } // namespace MockClusterConfig::MockClusterConfig(ClusterId aId, std::initializer_list aAttributes, - std::initializer_list aEvents) : + std::initializer_list aEvents, + std::initializer_list aAcceptedCommands, + std::initializer_list aGeneratedCommands) : id(aId), - attributes(aAttributes), events(aEvents), mEmberCluster{} + attributes(aAttributes), events(aEvents), mEmberCluster{}, mAcceptedCommands(aAcceptedCommands), + mGeneratedCommands(aGeneratedCommands) { VerifyOrDie(aAttributes.size() < UINT16_MAX); @@ -71,6 +74,18 @@ MockClusterConfig::MockClusterConfig(ClusterId aId, std::initializer_list(mEmberEventList.size()); mEmberCluster.eventList = mEmberEventList.data(); + if (!mAcceptedCommands.empty()) + { + mAcceptedCommands.push_back(kInvalidCommandId); + mEmberCluster.acceptedCommandList = mAcceptedCommands.data(); + } + + if (!mGeneratedCommands.empty()) + { + mGeneratedCommands.push_back(kInvalidCommandId); + mEmberCluster.generatedCommandList = mGeneratedCommands.data(); + } + for (auto & attr : attributes) { mAttributeMetaData.push_back(attr.attributeMetaData); @@ -82,10 +97,19 @@ MockClusterConfig::MockClusterConfig(ClusterId aId, std::initializer_list aAttributes = {}, - std::initializer_list aEvents = {}); + std::initializer_list aEvents = {}, std::initializer_list aAcceptedCommands = {}, + std::initializer_list aGeneratedCommands = {}); // Cluster-config is self-referential: mEmberCluster.attributes references mAttributeMetaData.data() MockClusterConfig(const MockClusterConfig & other); @@ -86,6 +87,8 @@ struct MockClusterConfig EmberAfCluster mEmberCluster; std::vector mEmberEventList; std::vector mAttributeMetaData; + std::vector mAcceptedCommands; + std::vector mGeneratedCommands; }; struct MockEndpointConfig