diff --git a/src/app/AttributeCache.cpp b/src/app/AttributeCache.cpp index e25f5958d15f6f..0970b7847b8936 100644 --- a/src/app/AttributeCache.cpp +++ b/src/app/AttributeCache.cpp @@ -29,6 +29,17 @@ CHIP_ERROR AttributeCache::UpdateCache(const ConcreteDataAttributePath & aPath, AttributeState state; System::PacketBufferHandle handle; System::PacketBufferTLVWriter writer; + bool endpointIsNew = false; + + if (mCache.find(aPath.mEndpointId) == mCache.end()) + { + // + // Since we might potentially be creating a new entry at mCache[aPath.mEndpointId][aPath.mClusterId] that + // wasn't there before, we need to check if an entry didn't exist there previously and remember that so that + // we can appropriately notify our clients of the addition of a new endpoint. + // + endpointIsNew = true; + } if (apData) { @@ -46,6 +57,35 @@ CHIP_ERROR AttributeCache::UpdateCache(const ConcreteDataAttributePath & aPath, handle.RightSize(); state.Set(std::move(handle)); + + // + // Clear out the committed data version and only set it again once we have received all data for this cluster. + // Otherwise, we may have incomplete data that looks like it's complete since it has a valid data version. + // + mCache[aPath.mEndpointId][aPath.mClusterId].mCommittedDataVersion.ClearValue(); + + // This commits a pending data version if the last report path is valid and it is different from the current path. + if (mLastReportDataPath.IsValidConcreteClusterPath() && mLastReportDataPath != aPath) + { + CommitPendingDataVersion(); + } + + bool foundEncompassingWildcardPath = false; + for (const auto & path : mRequestPathSet) + { + if (path.IncludesAllAttributesInCluster(aPath)) + { + foundEncompassingWildcardPath = true; + break; + } + } + + // if this data item is encompassed by a wildcard path, let's go ahead and update its pending data version. + if (foundEncompassingWildcardPath) + { + mCache[aPath.mEndpointId][aPath.mClusterId].mPendingDataVersion = aPath.mDataVersion; + } + mLastReportDataPath = aPath; } else { @@ -56,25 +96,43 @@ CHIP_ERROR AttributeCache::UpdateCache(const ConcreteDataAttributePath & aPath, // if the endpoint didn't exist previously, let's track the insertion // so that we can inform our callback of a new endpoint being added appropriately. // - if (mCache.find(aPath.mEndpointId) == mCache.end()) + if (endpointIsNew) { mAddedEndpoints.push_back(aPath.mEndpointId); } - mCache[aPath.mEndpointId][aPath.mClusterId][aPath.mAttributeId] = std::move(state); + mCache[aPath.mEndpointId][aPath.mClusterId].mAttributes[aPath.mAttributeId] = std::move(state); mChangedAttributeSet.insert(aPath); return CHIP_NO_ERROR; } void AttributeCache::OnReportBegin() { + mLastReportDataPath = ConcreteClusterPath(kInvalidEndpointId, kInvalidClusterId); mChangedAttributeSet.clear(); mAddedEndpoints.clear(); mCallback.OnReportBegin(); } +void AttributeCache::CommitPendingDataVersion() +{ + if (!mLastReportDataPath.IsValidConcreteClusterPath()) + { + return; + } + + auto & lastClusterInfo = mCache[mLastReportDataPath.mEndpointId][mLastReportDataPath.mClusterId]; + if (lastClusterInfo.mPendingDataVersion.HasValue()) + { + lastClusterInfo.mCommittedDataVersion = lastClusterInfo.mPendingDataVersion; + lastClusterInfo.mPendingDataVersion.ClearValue(); + } +} + void AttributeCache::OnReportEnd() { + CommitPendingDataVersion(); + mLastReportDataPath = ConcreteClusterPath(kInvalidEndpointId, kInvalidClusterId); std::set> changedClusters; // @@ -151,6 +209,15 @@ CHIP_ERROR AttributeCache::Get(const ConcreteAttributePath & path, TLV::TLVReade return CHIP_NO_ERROR; } +CHIP_ERROR AttributeCache::GetVersion(EndpointId mEndpointId, ClusterId mClusterId, Optional & aVersion) +{ + CHIP_ERROR err; + auto clusterState = GetClusterState(mEndpointId, mClusterId, err); + ReturnErrorOnFailure(err); + aVersion = clusterState->mCommittedDataVersion; + return CHIP_NO_ERROR; +} + AttributeCache::EndpointState * AttributeCache::GetEndpointState(EndpointId endpointId, CHIP_ERROR & err) { auto endpointIter = mCache.find(endpointId); @@ -192,8 +259,8 @@ AttributeCache::AttributeState * AttributeCache::GetAttributeState(EndpointId en return nullptr; } - auto attributeState = clusterState->find(attributeId); - if (attributeState == clusterState->end()) + auto attributeState = clusterState->mAttributes.find(attributeId); + if (attributeState == clusterState->mAttributes.end()) { err = CHIP_ERROR_KEY_NOT_FOUND; return nullptr; @@ -219,5 +286,123 @@ CHIP_ERROR AttributeCache::GetStatus(const ConcreteAttributePath & path, StatusI return CHIP_NO_ERROR; } +void AttributeCache::GetSortedFilters(std::vector> & aVector) +{ + for (auto const & endpointIter : mCache) + { + EndpointId endpointId = endpointIter.first; + for (auto const & clusterIter : endpointIter.second) + { + if (!clusterIter.second.mCommittedDataVersion.HasValue()) + { + continue; + } + DataVersion dataVersion = clusterIter.second.mCommittedDataVersion.Value(); + uint32_t clusterSize = 0; + ClusterId clusterId = clusterIter.first; + + for (auto const & attributeIter : clusterIter.second.mAttributes) + { + if (attributeIter.second.Is()) + { + clusterSize += + 5; // 1 byte: anonymous tag control byte for struct. 1 byte: control byte for uint8 value. 1 byte: + // context-specific tag for uint8 value.1 byte: the uint8 value. 1 byte: end of container. + if (attributeIter.second.Get().mClusterStatus.HasValue()) + { + clusterSize += 3; // 1 byte: control byte for uint8 value. 1 byte: context-specific tag for uint8 value. 1 + // byte: the uint8 value. + } + } + else + { + System::PacketBufferTLVReader bufReader; + bufReader.Init(attributeIter.second.Get().Retain()); + ReturnOnFailure(bufReader.Next()); + // Skip to the end of the element. + ReturnOnFailure(bufReader.Skip()); + + // Compute the amount of value data + clusterSize += bufReader.GetLengthRead(); + } + } + if (clusterSize == 0) + { + continue; + } + + DataVersionFilter filter(endpointId, clusterId, dataVersion); + + aVector.push_back(std::make_pair(filter, clusterSize)); + } + } + std::sort(aVector.begin(), aVector.end(), + [](const std::pair & x, const std::pair & y) { + return x.second > y.second; + }); +} + +CHIP_ERROR AttributeCache::OnUpdateDataVersionFilterList(DataVersionFilterIBs::Builder & aDataVersionFilterIBsBuilder, + const Span & aAttributePaths, + bool & aEncodedDataVersionList) +{ + CHIP_ERROR err = CHIP_NO_ERROR; + TLV::TLVWriter backup; + + for (auto & attribute : aAttributePaths) + { + if (attribute.HasAttributeWildcard()) + { + mRequestPathSet.insert(attribute); + } + } + + std::vector> filterVector; + GetSortedFilters(filterVector); + + aEncodedDataVersionList = false; + for (auto & filter : filterVector) + { + bool intersected = false; + aDataVersionFilterIBsBuilder.Checkpoint(backup); + + // if the particular cached cluster does not intersect with user provided attribute paths, skip the cached one + for (const auto & attributePath : aAttributePaths) + { + if (attributePath.IncludesAttributesInCluster(filter.first)) + { + intersected = true; + break; + } + } + if (!intersected) + { + continue; + } + + DataVersionFilterIB::Builder & filterIB = aDataVersionFilterIBsBuilder.CreateDataVersionFilter(); + SuccessOrExit(err = aDataVersionFilterIBsBuilder.GetError()); + ClusterPathIB::Builder & filterPath = filterIB.CreatePath(); + SuccessOrExit(err = filterIB.GetError()); + SuccessOrExit( + err = filterPath.Endpoint(filter.first.mEndpointId).Cluster(filter.first.mClusterId).EndOfClusterPathIB().GetError()); + SuccessOrExit(err = filterIB.DataVersion(filter.first.mDataVersion.Value()).EndOfDataVersionFilterIB().GetError()); + ChipLogProgress(DataManagement, + "Update DataVersionFilter: Endpoint=%" PRIu16 " Cluster=" ChipLogFormatMEI " Version=%" PRIu32, + filter.first.mEndpointId, ChipLogValueMEI(filter.first.mClusterId), filter.first.mDataVersion.Value()); + + aEncodedDataVersionList = true; + } + +exit: + if (err == CHIP_ERROR_NO_MEMORY || err == CHIP_ERROR_BUFFER_TOO_SMALL) + { + ChipLogProgress(DataManagement, "OnUpdateDataVersionFilterList out of space; rolling back"); + aDataVersionFilterIBsBuilder.Rollback(backup); + err = CHIP_NO_ERROR; + } + return err; +} + } // namespace app } // namespace chip diff --git a/src/app/AttributeCache.h b/src/app/AttributeCache.h index 37e782dc613e71..3a602b1fa85f76 100644 --- a/src/app/AttributeCache.h +++ b/src/app/AttributeCache.h @@ -34,7 +34,6 @@ namespace chip { namespace app { - /* * This implements an attribute cache designed to aggregate attribute data received by a client * from either read or subscribe interactions and keep it resident and available for clients to @@ -57,7 +56,9 @@ namespace app { * through to a registered callback. In addition, it provides its own enhancements to the base ReadClient::Callback * to make it easier to know what has changed in the cache. * - * **NOTE** This already includes the BufferedReadCallback, so there is no need to add that to the ReadClient callback chain. + * **NOTE** + * 1. This already includes the BufferedReadCallback, so there is no need to add that to the ReadClient callback chain. + * 2. The same cache cannot be used by multiple subscribe/read interactions at the same time. * */ class AttributeCache : protected ReadClient::Callback @@ -217,6 +218,14 @@ class AttributeCache : protected ReadClient::Callback */ CHIP_ERROR Get(const ConcreteAttributePath & path, TLV::TLVReader & reader); + /* + * Retrieve the data version for the given cluster. If there is no data for the specified path in the cache, + * CHIP_ERROR_KEY_NOT_FOUND shall be returned. Otherwise aVersion will be set to the + * current data version for the cluster (which may have no value if we don't have a known data version + * for it, for example because none of our paths were wildcards that covered the whole cluster). + */ + CHIP_ERROR GetVersion(EndpointId mEndpointId, ClusterId mClusterId, Optional & aVersion); + /* * Execute an iterator function that is called for every attribute * in a given endpoint and cluster. The function when invoked is provided a concrete attribute path @@ -241,7 +250,7 @@ class AttributeCache : protected ReadClient::Callback auto clusterState = GetClusterState(endpointId, clusterId, err); ReturnErrorOnFailure(err); - for (auto & attributeIter : *clusterState) + for (auto & attributeIter : clusterState->mAttributes) { const ConcreteAttributePath path(endpointId, clusterId, attributeIter.first); ReturnErrorOnFailure(func(path)); @@ -272,7 +281,7 @@ class AttributeCache : protected ReadClient::Callback { if (clusterIter.first == clusterId) { - for (auto & attributeIter : clusterIter.second) + for (auto & attributeIter : clusterIter.second.mAttributes) { const ConcreteAttributePath path(endpointIter.first, clusterId, attributeIter.first); ReturnErrorOnFailure(func(path)); @@ -312,10 +321,27 @@ class AttributeCache : protected ReadClient::Callback private: using AttributeState = Variant; - using ClusterState = std::map; - using EndpointState = std::map; - using NodeState = std::map; + // mPendingDataVersion represents a tentative data version for a cluster that we have gotten some reports for. + // + // mCurrentDataVersion represents a known data version for a cluster. In order for this to have a + // value the cluster must be included in a path in mRequestPathSet that has a wildcard attribute + // and we must not be in the middle of receiving reports for that cluster. + struct ClusterState + { + std::map mAttributes; + Optional mPendingDataVersion; + Optional mCommittedDataVersion; + }; + using EndpointState = std::map; + using NodeState = std::map; + struct Comparator + { + bool operator()(const AttributePathParams & x, const AttributePathParams & y) const + { + return x.mEndpointId < y.mEndpointId || x.mClusterId < y.mClusterId; + } + }; /* * These functions provide a way to index into the cached state with different sub-sets of a path, returning * appropriate slices of the data as requested. @@ -344,26 +370,45 @@ class AttributeCache : protected ReadClient::Callback void OnReportBegin() override; void OnReportEnd() override; void OnAttributeData(const ConcreteDataAttributePath & aPath, TLV::TLVReader * apData, const StatusIB & aStatus) override; - void OnError(CHIP_ERROR aError) override { return mCallback.OnError(aError); } + void OnError(CHIP_ERROR aError) override { mCallback.OnError(aError); } void OnEventData(const EventHeader & aEventHeader, TLV::TLVReader * apData, const StatusIB * apStatus) override { - return mCallback.OnEventData(aEventHeader, apData, apStatus); + mCallback.OnEventData(aEventHeader, apData, apStatus); + } + + void OnDone() override + { + mRequestPathSet.clear(); + return mCallback.OnDone(); } - void OnDone() override { return mCallback.OnDone(); } void OnSubscriptionEstablished(uint64_t aSubscriptionId) override { mCallback.OnSubscriptionEstablished(aSubscriptionId); } void OnDeallocatePaths(chip::app::ReadPrepareParams && aReadPrepareParams) override { - return mCallback.OnDeallocatePaths(std::move(aReadPrepareParams)); + mCallback.OnDeallocatePaths(std::move(aReadPrepareParams)); } + virtual CHIP_ERROR OnUpdateDataVersionFilterList(DataVersionFilterIBs::Builder & aDataVersionFilterIBsBuilder, + const Span & aAttributePaths, + bool & aEncodedDataVersionList) override; + + // Commit the pending cluster data version, if there is one. + void CommitPendingDataVersion(); + + // Get our list of data version filters, sorted from larges to smallest by the total size of the TLV + // payload for the filter's cluster. Applying filters in this order should maximize space savings + // on the wire if not all filters can be applied. + void GetSortedFilters(std::vector> & aVector); + Callback & mCallback; NodeState mCache; std::set mChangedAttributeSet; + std::set mRequestPathSet; // wildcard attribute request path only std::vector mAddedEndpoints; BufferedReadCallback mBufferedReader; + ConcreteClusterPath mLastReportDataPath = ConcreteClusterPath(kInvalidEndpointId, kInvalidClusterId); }; }; // namespace app diff --git a/src/app/AttributePathParams.h b/src/app/AttributePathParams.h index 515c2f2b322cde..786ed3e3fbc883 100644 --- a/src/app/AttributePathParams.h +++ b/src/app/AttributePathParams.h @@ -19,10 +19,12 @@ #pragma once #include +#include #include namespace chip { namespace app { +class ReadClient; struct AttributePathParams { // @@ -81,6 +83,22 @@ struct AttributePathParams return true; } + bool IncludesAttributesInCluster(const DataVersionFilter & other) const + { + VerifyOrReturnError(HasWildcardEndpointId() || mEndpointId == other.mEndpointId, false); + VerifyOrReturnError(HasWildcardClusterId() || mClusterId == other.mClusterId, false); + + return true; + } + + // check if input concrete cluster path is subset of current wildcard attribute + bool IncludesAllAttributesInCluster(const ConcreteClusterPath & aOther) const + { + VerifyOrReturnError(HasWildcardEndpointId() || mEndpointId == aOther.mEndpointId, false); + VerifyOrReturnError(HasWildcardClusterId() || mClusterId == aOther.mClusterId, false); + return HasWildcardAttributeId(); + } + ClusterId mClusterId = kInvalidClusterId; // uint32 AttributeId mAttributeId = kInvalidAttributeId; // uint32 EndpointId mEndpointId = kInvalidEndpointId; // uint16 diff --git a/src/app/BufferedReadCallback.h b/src/app/BufferedReadCallback.h index a4b2ce750a1038..bd382e975e14a4 100644 --- a/src/app/BufferedReadCallback.h +++ b/src/app/BufferedReadCallback.h @@ -83,6 +83,13 @@ class BufferedReadCallback : public ReadClient::Callback return mCallback.OnDeallocatePaths(std::move(aReadPrepareParams)); } + virtual CHIP_ERROR OnUpdateDataVersionFilterList(DataVersionFilterIBs::Builder & aDataVersionFilterIBsBuilder, + const Span & aAttributePaths, + bool & aEncodedDataVersionList) override + { + return mCallback.OnUpdateDataVersionFilterList(aDataVersionFilterIBsBuilder, aAttributePaths, aEncodedDataVersionList); + } + /* * Given a reader positioned at a list element, allocate a packet buffer, copy the list item where * the reader is positioned into that buffer and add it to our buffered list for tracking. diff --git a/src/app/ConcreteClusterPath.h b/src/app/ConcreteClusterPath.h index ec880e8df404b2..52d646cc8815aa 100644 --- a/src/app/ConcreteClusterPath.h +++ b/src/app/ConcreteClusterPath.h @@ -36,6 +36,8 @@ struct ConcreteClusterPath ConcreteClusterPath(const ConcreteClusterPath & aOther) = default; ConcreteClusterPath & operator=(const ConcreteClusterPath & aOther) = default; + bool IsValidConcreteClusterPath() const { return !(mEndpointId == kInvalidEndpointId || mClusterId == kInvalidClusterId); } + bool operator==(const ConcreteClusterPath & aOther) const { return mEndpointId == aOther.mEndpointId && mClusterId == aOther.mClusterId; diff --git a/src/app/DataVersionFilter.h b/src/app/DataVersionFilter.h index 2794048170e53f..83ad93207119b1 100644 --- a/src/app/DataVersionFilter.h +++ b/src/app/DataVersionFilter.h @@ -35,6 +35,11 @@ struct DataVersionFilter return (mEndpointId != kInvalidEndpointId) && (mClusterId != kInvalidClusterId) && (mDataVersion.HasValue()); } + bool operator==(const DataVersionFilter & aOther) const + { + return mEndpointId == aOther.mEndpointId && mClusterId == aOther.mClusterId && mDataVersion == aOther.mDataVersion; + } + ClusterId mClusterId = kInvalidClusterId; // uint32 Optional mDataVersion; // uint32 EndpointId mEndpointId = kInvalidEndpointId; // uint16 diff --git a/src/app/InteractionModelHelper.h b/src/app/InteractionModelHelper.h new file mode 100644 index 00000000000000..530ba2be196347 --- /dev/null +++ b/src/app/InteractionModelHelper.h @@ -0,0 +1,42 @@ +/* + * + * Copyright (c) 2021 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 { + +static CHIP_ERROR InitWriterWithSpaceReserved(System::PacketBufferTLVWriter & aWriter, uint32_t aReserveSpace) +{ + System::PacketBufferHandle msgBuf = System::PacketBufferHandle::New(kMaxSecureSduLengthBytes); + VerifyOrReturnError(!msgBuf.IsNull(), CHIP_ERROR_NO_MEMORY); + uint16_t reservedSize = 0; + + if (msgBuf->AvailableDataLength() > kMaxSecureSduLengthBytes) + { + reservedSize = static_cast(msgBuf->AvailableDataLength() - kMaxSecureSduLengthBytes); + } + + reservedSize = static_cast(reservedSize + Crypto::CHIP_CRYPTO_AEAD_MIC_LENGTH_BYTES + aReserveSpace); + aWriter.Init(std::move(msgBuf)); + ReturnErrorOnFailure(aWriter.ReserveBuffer(reservedSize)); + return CHIP_NO_ERROR; +}; +} // namespace app +} // namespace chip diff --git a/src/app/MessageDef/ReadRequestMessage.h b/src/app/MessageDef/ReadRequestMessage.h index 07e46d9102e09a..8e1465618420ff 100644 --- a/src/app/MessageDef/ReadRequestMessage.h +++ b/src/app/MessageDef/ReadRequestMessage.h @@ -36,10 +36,10 @@ namespace ReadRequestMessage { enum class Tag : uint8_t { kAttributeRequests = 0, - kDataVersionFilters = 1, - kEventRequests = 2, - kEventFilters = 3, - kIsFabricFiltered = 4, + kEventRequests = 1, + kEventFilters = 2, + kIsFabricFiltered = 3, + kDataVersionFilters = 4, }; class Parser : public MessageParser diff --git a/src/app/MessageDef/SubscribeRequestMessage.cpp b/src/app/MessageDef/SubscribeRequestMessage.cpp index 38f5acbd0b47cc..fb4f485db22181 100644 --- a/src/app/MessageDef/SubscribeRequestMessage.cpp +++ b/src/app/MessageDef/SubscribeRequestMessage.cpp @@ -91,7 +91,7 @@ CHIP_ERROR SubscribeRequestMessage::Parser::CheckSchemaValidity() const break; case to_underlying(Tag::kDataVersionFilters): // check if this tag has appeared before - VerifyOrReturnError(!(tagPresenceMask & (1 << to_underlying(Tag::kEventFilters))), CHIP_ERROR_INVALID_TLV_TAG); + VerifyOrReturnError(!(tagPresenceMask & (1 << to_underlying(Tag::kDataVersionFilters))), CHIP_ERROR_INVALID_TLV_TAG); tagPresenceMask |= (1 << to_underlying(Tag::kDataVersionFilters)); { DataVersionFilterIBs::Parser dataVersionFilters; diff --git a/src/app/MessageDef/SubscribeRequestMessage.h b/src/app/MessageDef/SubscribeRequestMessage.h index f874b6b927c032..d70451e5677da4 100644 --- a/src/app/MessageDef/SubscribeRequestMessage.h +++ b/src/app/MessageDef/SubscribeRequestMessage.h @@ -39,11 +39,11 @@ enum class Tag : uint8_t kMinIntervalFloorSeconds = 1, kMaxIntervalCeilingSeconds = 2, kAttributeRequests = 3, - kDataVersionFilters = 4, - kEventRequests = 5, - kEventFilters = 6, - kIsProxy = 7, - kIsFabricFiltered = 8, + kEventRequests = 4, + kEventFilters = 5, + kIsProxy = 6, + kIsFabricFiltered = 7, + kDataVersionFilters = 8, }; class Parser : public MessageParser diff --git a/src/app/ReadClient.cpp b/src/app/ReadClient.cpp index f06af4f1a87335..e1208eb2cd8bc7 100644 --- a/src/app/ReadClient.cpp +++ b/src/app/ReadClient.cpp @@ -24,6 +24,7 @@ #include #include +#include #include #include #include @@ -154,6 +155,7 @@ void ReadClient::Close(CHIP_ERROR aError) { StopResubscription(); } + mpCallback.OnDone(); } @@ -200,63 +202,76 @@ CHIP_ERROR ReadClient::SendReadRequest(ReadPrepareParams & aReadPrepareParams) { // TODO: SendRequest parameter is too long, need to have the structure to represent it CHIP_ERROR err = CHIP_NO_ERROR; - System::PacketBufferHandle msgBuf; + ChipLogDetail(DataManagement, "%s ReadClient[%p]: Sending Read Request", __func__, this); VerifyOrReturnError(ClientState::Idle == mState, err = CHIP_ERROR_INCORRECT_STATE); - { - System::PacketBufferTLVWriter writer; - ReadRequestMessage::Builder request; + Span attributePaths(aReadPrepareParams.mpAttributePathParamsList, + aReadPrepareParams.mAttributePathParamsListSize); + Span eventPaths(aReadPrepareParams.mpEventPathParamsList, aReadPrepareParams.mEventPathParamsListSize); + Span dataVersionFilters(aReadPrepareParams.mpDataVersionFilterList, + aReadPrepareParams.mDataVersionFilterListSize); - msgBuf = System::PacketBufferHandle::New(kMaxSecureSduLengthBytes); - VerifyOrReturnError(!msgBuf.IsNull(), err = CHIP_ERROR_NO_MEMORY); + System::PacketBufferHandle msgBuf; + ReadRequestMessage::Builder request; + System::PacketBufferTLVWriter writer; - writer.Init(std::move(msgBuf)); + InitWriterWithSpaceReserved(writer, kReservedSizeForTLVEncodingOverhead); + ReturnErrorOnFailure(request.Init(&writer)); - ReturnErrorOnFailure(request.Init(&writer)); + if (!attributePaths.empty()) + { + AttributePathIBs::Builder & attributePathListBuilder = request.CreateAttributeRequests(); + ReturnErrorOnFailure(err = request.GetError()); + ReturnErrorOnFailure(GenerateAttributePaths(attributePathListBuilder, attributePaths)); + } - if (aReadPrepareParams.mAttributePathParamsListSize != 0 && aReadPrepareParams.mpAttributePathParamsList != nullptr) - { - AttributePathIBs::Builder & attributePathListBuilder = request.CreateAttributeRequests(); - ReturnErrorOnFailure(err = request.GetError()); - ReturnErrorOnFailure(GenerateAttributePathList(attributePathListBuilder, aReadPrepareParams.mpAttributePathParamsList, - aReadPrepareParams.mAttributePathParamsListSize)); - if (aReadPrepareParams.mDataVersionFilterListSize != 0 && aReadPrepareParams.mpDataVersionFilterList != nullptr) - { - DataVersionFilterIBs::Builder & dataVersionFilterListBuilder = request.CreateDataVersionFilters(); - ReturnErrorOnFailure(request.GetError()); - ReturnErrorOnFailure(GenerateDataVersionFilterList(dataVersionFilterListBuilder, - aReadPrepareParams.mpDataVersionFilterList, - aReadPrepareParams.mDataVersionFilterListSize)); - } - } + if (!eventPaths.empty()) + { + EventPathIBs::Builder & eventPathListBuilder = request.CreateEventRequests(); + ReturnErrorOnFailure(err = request.GetError()); - if (aReadPrepareParams.mEventPathParamsListSize != 0 && aReadPrepareParams.mpEventPathParamsList != nullptr) - { - EventPathIBs::Builder & eventPathListBuilder = request.CreateEventRequests(); - ReturnErrorOnFailure(err = request.GetError()); + ReturnErrorOnFailure(GenerateEventPaths(eventPathListBuilder, eventPaths)); - ReturnErrorOnFailure(GenerateEventPaths(eventPathListBuilder, aReadPrepareParams.mpEventPathParamsList, - aReadPrepareParams.mEventPathParamsListSize)); + if (aReadPrepareParams.mEventNumber != 0) + { + // EventFilter is optional + EventFilterIBs::Builder & eventFilters = request.CreateEventFilters(); + ReturnErrorOnFailure(request.GetError()); - if (aReadPrepareParams.mEventNumber != 0) - { - // EventFilter is optional - EventFilterIBs::Builder & eventFilters = request.CreateEventFilters(); - ReturnErrorOnFailure(request.GetError()); - - EventFilterIB::Builder & eventFilter = eventFilters.CreateEventFilter(); - ReturnErrorOnFailure(eventFilters.GetError()); - ReturnErrorOnFailure(eventFilter.EventMin(aReadPrepareParams.mEventNumber).EndOfEventFilterIB().GetError()); - ReturnErrorOnFailure(eventFilters.EndOfEventFilters().GetError()); - } + EventFilterIB::Builder & eventFilter = eventFilters.CreateEventFilter(); + ReturnErrorOnFailure(eventFilters.GetError()); + ReturnErrorOnFailure(eventFilter.EventMin(aReadPrepareParams.mEventNumber).EndOfEventFilterIB().GetError()); + ReturnErrorOnFailure(eventFilters.EndOfEventFilters().GetError()); } + } + + ReturnErrorOnFailure(request.IsFabricFiltered(aReadPrepareParams.mIsFabricFiltered).GetError()); - ReturnErrorOnFailure(request.IsFabricFiltered(aReadPrepareParams.mIsFabricFiltered).EndOfReadRequestMessage().GetError()); - ReturnErrorOnFailure(writer.Finalize(&msgBuf)); + bool encodedDataVersionList = false; + TLV::TLVWriter backup; + request.Checkpoint(backup); + DataVersionFilterIBs::Builder & dataVersionFilterListBuilder = request.CreateDataVersionFilters(); + ReturnErrorOnFailure(request.GetError()); + if (!attributePaths.empty()) + { + ReturnErrorOnFailure(GenerateDataVersionFilterList(dataVersionFilterListBuilder, attributePaths, dataVersionFilters, + encodedDataVersionList)); + } + ReturnErrorOnFailure(dataVersionFilterListBuilder.GetWriter()->UnreserveBuffer(kReservedSizeForTLVEncodingOverhead)); + if (encodedDataVersionList) + { + ReturnErrorOnFailure(dataVersionFilterListBuilder.EndOfDataVersionFilterIBs().GetError()); + } + else + { + request.Rollback(backup); } + ReturnErrorOnFailure(request.EndOfReadRequestMessage().GetError()); + ReturnErrorOnFailure(writer.Finalize(&msgBuf)); + mpExchangeCtx = mpExchangeMgr->NewContext(aReadPrepareParams.mSessionHolder.Get(), this); VerifyOrReturnError(mpExchangeCtx != nullptr, err = CHIP_ERROR_NO_MEMORY); @@ -273,57 +288,88 @@ CHIP_ERROR ReadClient::SendReadRequest(ReadPrepareParams & aReadPrepareParams) return CHIP_NO_ERROR; } -CHIP_ERROR ReadClient::GenerateEventPaths(EventPathIBs::Builder & aEventPathsBuilder, EventPathParams * apEventPathParamsList, - size_t aEventPathParamsListSize) +CHIP_ERROR ReadClient::GenerateEventPaths(EventPathIBs::Builder & aEventPathsBuilder, const Span & aEventPaths) { - for (size_t index = 0; index < aEventPathParamsListSize; ++index) + for (auto & event : aEventPaths) { - VerifyOrReturnError(apEventPathParamsList[index].IsValidEventPath(), CHIP_ERROR_IM_MALFORMED_ATTRIBUTE_PATH); + VerifyOrReturnError(event.IsValidEventPath(), CHIP_ERROR_IM_MALFORMED_ATTRIBUTE_PATH); EventPathIB::Builder & path = aEventPathsBuilder.CreatePath(); ReturnErrorOnFailure(aEventPathsBuilder.GetError()); - ReturnErrorOnFailure(path.Encode(apEventPathParamsList[index])); + ReturnErrorOnFailure(path.Encode(event)); } aEventPathsBuilder.EndOfEventPaths(); return aEventPathsBuilder.GetError(); } -CHIP_ERROR ReadClient::GenerateAttributePathList(AttributePathIBs::Builder & aAttributePathIBsBuilder, - AttributePathParams * apAttributePathParamsList, - size_t aAttributePathParamsListSize) +CHIP_ERROR ReadClient::GenerateAttributePaths(AttributePathIBs::Builder & aAttributePathIBsBuilder, + const Span & aAttributePaths) { - for (size_t index = 0; index < aAttributePathParamsListSize; index++) + for (auto & attribute : aAttributePaths) { - VerifyOrReturnError(apAttributePathParamsList[index].IsValidAttributePath(), CHIP_ERROR_IM_MALFORMED_ATTRIBUTE_PATH); + VerifyOrReturnError(attribute.IsValidAttributePath(), CHIP_ERROR_IM_MALFORMED_ATTRIBUTE_PATH); AttributePathIB::Builder & path = aAttributePathIBsBuilder.CreatePath(); ReturnErrorOnFailure(aAttributePathIBsBuilder.GetError()); - ReturnErrorOnFailure(path.Encode(apAttributePathParamsList[index])); + ReturnErrorOnFailure(path.Encode(attribute)); } aAttributePathIBsBuilder.EndOfAttributePathIBs(); return aAttributePathIBsBuilder.GetError(); } -CHIP_ERROR ReadClient::GenerateDataVersionFilterList(DataVersionFilterIBs::Builder & aDataVersionFilterIBsBuilder, - DataVersionFilter * apDataVersionFilterList, size_t aDataVersionFilterListSize) +CHIP_ERROR ReadClient::BuildDataVersionFilterList(DataVersionFilterIBs::Builder & aDataVersionFilterIBsBuilder, + const Span & aAttributePaths, + const Span & aDataVersionFilters, + bool & aEncodedDataVersionList) { - for (size_t index = 0; index < aDataVersionFilterListSize; index++) + for (auto & filter : aDataVersionFilters) { - VerifyOrReturnError(apDataVersionFilterList[index].IsValidDataVersionFilter(), CHIP_ERROR_INVALID_ARGUMENT); - DataVersionFilterIB::Builder & filter = aDataVersionFilterIBsBuilder.CreateDataVersionFilter(); + VerifyOrReturnError(filter.IsValidDataVersionFilter(), CHIP_ERROR_INVALID_ARGUMENT); + + // If data version filter is for some cluster none of whose attributes are included in our paths, discard this filter. + bool intersected = false; + for (auto & path : aAttributePaths) + { + if (path.IncludesAttributesInCluster(filter)) + { + intersected = true; + break; + } + } + if (!intersected) + { + continue; + } + + DataVersionFilterIB::Builder & filterIB = aDataVersionFilterIBsBuilder.CreateDataVersionFilter(); ReturnErrorOnFailure(aDataVersionFilterIBsBuilder.GetError()); - ClusterPathIB::Builder & path = filter.CreatePath(); - ReturnErrorOnFailure(filter.GetError()); - ReturnErrorOnFailure(path.Endpoint(apDataVersionFilterList[index].mEndpointId) - .Cluster(apDataVersionFilterList[index].mClusterId) - .EndOfClusterPathIB() - .GetError()); - VerifyOrReturnError(apDataVersionFilterList[index].mDataVersion.HasValue(), CHIP_ERROR_INVALID_ARGUMENT); + ClusterPathIB::Builder & path = filterIB.CreatePath(); + ReturnErrorOnFailure(filterIB.GetError()); + ReturnErrorOnFailure(path.Endpoint(filter.mEndpointId).Cluster(filter.mClusterId).EndOfClusterPathIB().GetError()); + VerifyOrReturnError(filter.mDataVersion.HasValue(), CHIP_ERROR_INVALID_ARGUMENT); + ReturnErrorOnFailure(filterIB.DataVersion(filter.mDataVersion.Value()).EndOfDataVersionFilterIB().GetError()); + aEncodedDataVersionList = true; + } + return CHIP_NO_ERROR; +} + +CHIP_ERROR ReadClient::GenerateDataVersionFilterList(DataVersionFilterIBs::Builder & aDataVersionFilterIBsBuilder, + const Span & aAttributePaths, + const Span & aDataVersionFilters, + bool & aEncodedDataVersionList) +{ + if (!aDataVersionFilters.empty()) + { + ReturnErrorOnFailure(BuildDataVersionFilterList(aDataVersionFilterIBsBuilder, aAttributePaths, aDataVersionFilters, + aEncodedDataVersionList)); + } + else + { ReturnErrorOnFailure( - filter.DataVersion(apDataVersionFilterList[index].mDataVersion.Value()).EndOfDataVersionFilterIB().GetError()); + mpCallback.OnUpdateDataVersionFilterList(aDataVersionFilterIBsBuilder, aAttributePaths, aEncodedDataVersionList)); } - return aDataVersionFilterIBsBuilder.EndOfDataVersionFilterIBs().GetError(); + return CHIP_NO_ERROR; } CHIP_ERROR ReadClient::OnMessageReceived(Messaging::ExchangeContext * apExchangeContext, const PayloadHeader & aPayloadHeader, @@ -793,18 +839,23 @@ CHIP_ERROR ReadClient::SendAutoResubscribeRequest(ReadPrepareParams && aReadPrep CHIP_ERROR ReadClient::SendSubscribeRequest(ReadPrepareParams & aReadPrepareParams) { CHIP_ERROR err = CHIP_NO_ERROR; - System::PacketBufferHandle msgBuf; - System::PacketBufferTLVWriter writer; - SubscribeRequestMessage::Builder request; VerifyOrReturnError(ClientState::Idle == mState, err = CHIP_ERROR_INCORRECT_STATE); - msgBuf = System::PacketBufferHandle::New(kMaxSecureSduLengthBytes); - VerifyOrReturnError(!msgBuf.IsNull(), err = CHIP_ERROR_NO_MEMORY); + // Todo: Remove the below, Update span in ReadPrepareParams + Span attributePaths(aReadPrepareParams.mpAttributePathParamsList, + aReadPrepareParams.mAttributePathParamsListSize); + Span eventPaths(aReadPrepareParams.mpEventPathParamsList, aReadPrepareParams.mEventPathParamsListSize); + Span dataVersionFilters(aReadPrepareParams.mpDataVersionFilterList, + aReadPrepareParams.mDataVersionFilterListSize); VerifyOrReturnError(aReadPrepareParams.mMinIntervalFloorSeconds <= aReadPrepareParams.mMaxIntervalCeilingSeconds, err = CHIP_ERROR_INVALID_ARGUMENT); - writer.Init(std::move(msgBuf)); + + System::PacketBufferHandle msgBuf; + System::PacketBufferTLVWriter writer; + SubscribeRequestMessage::Builder request; + InitWriterWithSpaceReserved(writer, kReservedSizeForTLVEncodingOverhead); ReturnErrorOnFailure(request.Init(&writer)); @@ -812,28 +863,18 @@ CHIP_ERROR ReadClient::SendSubscribeRequest(ReadPrepareParams & aReadPreparePara .MinIntervalFloorSeconds(aReadPrepareParams.mMinIntervalFloorSeconds) .MaxIntervalCeilingSeconds(aReadPrepareParams.mMaxIntervalCeilingSeconds); - if (aReadPrepareParams.mAttributePathParamsListSize != 0 && aReadPrepareParams.mpAttributePathParamsList != nullptr) + if (!attributePaths.empty()) { AttributePathIBs::Builder & attributePathListBuilder = request.CreateAttributeRequests(); ReturnErrorOnFailure(err = attributePathListBuilder.GetError()); - ReturnErrorOnFailure(GenerateAttributePathList(attributePathListBuilder, aReadPrepareParams.mpAttributePathParamsList, - aReadPrepareParams.mAttributePathParamsListSize)); - if (aReadPrepareParams.mDataVersionFilterListSize != 0 && aReadPrepareParams.mpDataVersionFilterList != nullptr) - { - DataVersionFilterIBs::Builder & dataVersionFilterListBuilder = request.CreateDataVersionFilters(); - ReturnErrorOnFailure(request.GetError()); - ReturnErrorOnFailure(GenerateDataVersionFilterList(dataVersionFilterListBuilder, - aReadPrepareParams.mpDataVersionFilterList, - aReadPrepareParams.mDataVersionFilterListSize)); - } + ReturnErrorOnFailure(GenerateAttributePaths(attributePathListBuilder, attributePaths)); } - if (aReadPrepareParams.mEventPathParamsListSize != 0 && aReadPrepareParams.mpEventPathParamsList != nullptr) + if (!eventPaths.empty()) { EventPathIBs::Builder & eventPathListBuilder = request.CreateEventRequests(); ReturnErrorOnFailure(err = eventPathListBuilder.GetError()); - ReturnErrorOnFailure(GenerateEventPaths(eventPathListBuilder, aReadPrepareParams.mpEventPathParamsList, - aReadPrepareParams.mEventPathParamsListSize)); + ReturnErrorOnFailure(GenerateEventPaths(eventPathListBuilder, eventPaths)); if (aReadPrepareParams.mEventNumber != 0) { @@ -850,8 +891,29 @@ CHIP_ERROR ReadClient::SendSubscribeRequest(ReadPrepareParams & aReadPreparePara ReturnErrorOnFailure(err = eventFilters.GetError()); } - request.IsFabricFiltered(aReadPrepareParams.mIsFabricFiltered).EndOfSubscribeRequestMessage(); - ReturnErrorOnFailure(err = request.GetError()); + ReturnErrorOnFailure(err = request.IsFabricFiltered(aReadPrepareParams.mIsFabricFiltered).GetError()); + + bool encodedDataVersionList = false; + TLV::TLVWriter backup; + request.Checkpoint(backup); + DataVersionFilterIBs::Builder & dataVersionFilterListBuilder = request.CreateDataVersionFilters(); + ReturnErrorOnFailure(request.GetError()); + if (!attributePaths.empty()) + { + ReturnErrorOnFailure(GenerateDataVersionFilterList(dataVersionFilterListBuilder, attributePaths, dataVersionFilters, + encodedDataVersionList)); + } + ReturnErrorOnFailure(dataVersionFilterListBuilder.GetWriter()->UnreserveBuffer(kReservedSizeForTLVEncodingOverhead)); + if (encodedDataVersionList) + { + ReturnErrorOnFailure(dataVersionFilterListBuilder.EndOfDataVersionFilterIBs().GetError()); + } + else + { + request.Rollback(backup); + } + + ReturnErrorOnFailure(err = request.EndOfSubscribeRequestMessage().GetError()); ReturnErrorOnFailure(writer.Finalize(&msgBuf)); mpExchangeCtx = mpExchangeMgr->NewContext(aReadPrepareParams.mSessionHolder.Get(), this); diff --git a/src/app/ReadClient.h b/src/app/ReadClient.h index b23326722683b9..921c14873ba460 100644 --- a/src/app/ReadClient.h +++ b/src/app/ReadClient.h @@ -165,6 +165,24 @@ class ReadClient : public Messaging::ExchangeDelegate * SendAutoResubscribeRequest is not called, this function will not be called. */ virtual void OnDeallocatePaths(ReadPrepareParams && aReadPrepareParams) {} + + /** + * This function is invoked when constructing a read/subscribeRequest that does not have data + * version filters specified, to give the callback a chance to provide some. + * + * This function is expected to encode as many complete data version filters as will fit into + * the buffer, rolling back any partially-encoded filters if it runs out of space, and set the + * aEncodedDataVersionList boolean to true if it has successfully encoded at least one data version filter. + * + * Otherwise aEncodedDataVersionList will be set to false. + */ + virtual CHIP_ERROR OnUpdateDataVersionFilterList(DataVersionFilterIBs::Builder & aDataVersionFilterIBsBuilder, + const Span & aAttributePaths, + bool & aEncodedDataVersionList) + { + aEncodedDataVersionList = false; + return CHIP_NO_ERROR; + } }; enum class InteractionType : uint8_t @@ -301,12 +319,16 @@ class ReadClient : public Messaging::ExchangeDelegate bool IsAwaitingInitialReport() const { return mState == ClientState::AwaitingInitialReport; } bool IsAwaitingSubscribeResponse() const { return mState == ClientState::AwaitingSubscribeResponse; } - CHIP_ERROR GenerateEventPaths(EventPathIBs::Builder & aEventPathsBuilder, EventPathParams * apEventPathParamsList, - size_t aEventPathParamsListSize); - CHIP_ERROR GenerateAttributePathList(AttributePathIBs::Builder & aAttributePathIBsBuilder, - AttributePathParams * apAttributePathParamsList, size_t aAttributePathParamsListSize); + CHIP_ERROR GenerateEventPaths(EventPathIBs::Builder & aEventPathsBuilder, const Span & aEventPaths); + CHIP_ERROR GenerateAttributePaths(AttributePathIBs::Builder & aAttributePathIBsBuilder, + const Span & aAttributePaths); + CHIP_ERROR GenerateDataVersionFilterList(DataVersionFilterIBs::Builder & aDataVersionFilterIBsBuilder, - DataVersionFilter * apDataVersionFilterList, size_t aDataVersionFilterListSize); + const Span & aAttributePaths, + const Span & aDataVersionFilters, bool & aEncodedDataVersionList); + CHIP_ERROR BuildDataVersionFilterList(DataVersionFilterIBs::Builder & aDataVersionFilterIBsBuilder, + const Span & aAttributePaths, + const Span & aDataVersionFilters, bool & aEncodedDataVersionList); CHIP_ERROR ProcessAttributeReportIBs(TLV::TLVReader & aAttributeDataIBsReader); CHIP_ERROR ProcessEventReportIBs(TLV::TLVReader & aEventReportIBsReader); @@ -361,6 +383,16 @@ class ReadClient : public Messaging::ExchangeDelegate InteractionModelEngine * mpImEngine = nullptr; ReadPrepareParams mReadPrepareParams; uint32_t mNumRetries = 0; + + // End Of Container (0x18) uses one byte. + static constexpr uint16_t kReservedSizeForEndOfContainer = 1; + // Reserved size for the uint8_t InteractionModelRevision flag, which takes up 1 byte for the control tag and 1 byte for the + // context tag, 1 byte for value + static constexpr uint16_t kReservedSizeForIMRevision = 1 + 1 + 1; + // Reserved buffer for TLV level overhead (the overhead for data version filter IBs EndOfContainer, IM reversion end + // of RequestMessage (another end of container)). + static constexpr uint16_t kReservedSizeForTLVEncodingOverhead = + kReservedSizeForEndOfContainer + kReservedSizeForIMRevision + kReservedSizeForEndOfContainer; }; }; // namespace app diff --git a/src/app/tests/TestAttributeCache.cpp b/src/app/tests/TestAttributeCache.cpp index a4d26b59e4493a..6c10e1206a5e66 100644 --- a/src/app/tests/TestAttributeCache.cpp +++ b/src/app/tests/TestAttributeCache.cpp @@ -191,6 +191,7 @@ void DataSeriesGenerator::Generate(ForwardedDataCallbackValidator & dataCallback writer.Init(std::move(handle), true); status = StatusIB(); path.mAttributeId = instruction.GetAttributeId(); + path.mDataVersion.SetValue(1); ChipLogProgress(DataManagement, "\t -- Generating Instruction ID %d", instruction.mInstructionId); diff --git a/src/app/tests/TestMessageDef.cpp b/src/app/tests/TestMessageDef.cpp index cb6f38081300d7..b3b61468648e0d 100644 --- a/src/app/tests/TestMessageDef.cpp +++ b/src/app/tests/TestMessageDef.cpp @@ -1092,10 +1092,6 @@ void BuildReadRequestMessage(nlTestSuite * apSuite, chip::TLV::TLVWriter & aWrit NL_TEST_ASSERT(apSuite, readRequestBuilder.GetError() == CHIP_NO_ERROR); BuildAttributePathList(apSuite, attributePathIBs); - DataVersionFilterIBs::Builder & dataVersionFilters = readRequestBuilder.CreateDataVersionFilters(); - NL_TEST_ASSERT(apSuite, readRequestBuilder.GetError() == CHIP_NO_ERROR); - BuildDataVersionFilterIBs(apSuite, dataVersionFilters); - EventPathIBs::Builder & eventPathList = readRequestBuilder.CreateEventRequests(); NL_TEST_ASSERT(apSuite, readRequestBuilder.GetError() == CHIP_NO_ERROR); @@ -1108,6 +1104,10 @@ void BuildReadRequestMessage(nlTestSuite * apSuite, chip::TLV::TLVWriter & aWrit readRequestBuilder.IsFabricFiltered(true); NL_TEST_ASSERT(apSuite, readRequestBuilder.GetError() == CHIP_NO_ERROR); + DataVersionFilterIBs::Builder & dataVersionFilters = readRequestBuilder.CreateDataVersionFilters(); + NL_TEST_ASSERT(apSuite, readRequestBuilder.GetError() == CHIP_NO_ERROR); + BuildDataVersionFilterIBs(apSuite, dataVersionFilters); + readRequestBuilder.EndOfReadRequestMessage(); NL_TEST_ASSERT(apSuite, readRequestBuilder.GetError() == CHIP_NO_ERROR); } @@ -1254,10 +1254,6 @@ void BuildSubscribeRequestMessage(nlTestSuite * apSuite, chip::TLV::TLVWriter & NL_TEST_ASSERT(apSuite, subscribeRequestBuilder.GetError() == CHIP_NO_ERROR); BuildAttributePathList(apSuite, attributePathIBs); - DataVersionFilterIBs::Builder & dataVersionFilters = subscribeRequestBuilder.CreateDataVersionFilters(); - NL_TEST_ASSERT(apSuite, subscribeRequestBuilder.GetError() == CHIP_NO_ERROR); - BuildDataVersionFilterIBs(apSuite, dataVersionFilters); - EventPathIBs::Builder & eventPathList = subscribeRequestBuilder.CreateEventRequests(); NL_TEST_ASSERT(apSuite, subscribeRequestBuilder.GetError() == CHIP_NO_ERROR); @@ -1273,6 +1269,10 @@ void BuildSubscribeRequestMessage(nlTestSuite * apSuite, chip::TLV::TLVWriter & subscribeRequestBuilder.IsFabricFiltered(true); NL_TEST_ASSERT(apSuite, subscribeRequestBuilder.GetError() == CHIP_NO_ERROR); + DataVersionFilterIBs::Builder & dataVersionFilters = subscribeRequestBuilder.CreateDataVersionFilters(); + NL_TEST_ASSERT(apSuite, subscribeRequestBuilder.GetError() == CHIP_NO_ERROR); + BuildDataVersionFilterIBs(apSuite, dataVersionFilters); + subscribeRequestBuilder.EndOfSubscribeRequestMessage(); NL_TEST_ASSERT(apSuite, subscribeRequestBuilder.GetError() == CHIP_NO_ERROR); } diff --git a/src/app/tests/TestReadInteraction.cpp b/src/app/tests/TestReadInteraction.cpp index f263284746240a..501cc4a30da380 100644 --- a/src/app/tests/TestReadInteraction.cpp +++ b/src/app/tests/TestReadInteraction.cpp @@ -485,11 +485,14 @@ void TestReadInteraction::TestReadClientGenerateAttributePathList(nlTestSuite * chip::app::ReadClient::InteractionType::Read); AttributePathParams attributePathParams[2]; - attributePathParams[0].mAttributeId = 0; - attributePathParams[1].mAttributeId = 0; - attributePathParams[1].mListIndex = 0; + attributePathParams[0].mAttributeId = 0; + attributePathParams[1].mAttributeId = 0; + attributePathParams[1].mListIndex = 0; + + Span attributePaths(attributePathParams, 2 /*aAttributePathParamsListSize*/); + AttributePathIBs::Builder & attributePathListBuilder = request.CreateAttributeRequests(); - err = readClient.GenerateAttributePathList(attributePathListBuilder, attributePathParams, 2 /*aAttributePathParamsListSize*/); + err = readClient.GenerateAttributePaths(attributePathListBuilder, attributePaths); NL_TEST_ASSERT(apSuite, err == CHIP_NO_ERROR); } @@ -512,10 +515,13 @@ void TestReadInteraction::TestReadClientGenerateInvalidAttributePathList(nlTestS NL_TEST_ASSERT(apSuite, err == CHIP_NO_ERROR); AttributePathParams attributePathParams[2]; - attributePathParams[0].mAttributeId = 0; - attributePathParams[1].mListIndex = 0; + attributePathParams[0].mAttributeId = 0; + attributePathParams[1].mListIndex = 0; + + Span attributePaths(attributePathParams, 2 /*aAttributePathParamsListSize*/); + AttributePathIBs::Builder & attributePathListBuilder = request.CreateAttributeRequests(); - err = readClient.GenerateAttributePathList(attributePathListBuilder, attributePathParams, 2 /*aAttributePathParamsListSize*/); + err = readClient.GenerateAttributePaths(attributePathListBuilder, attributePaths); NL_TEST_ASSERT(apSuite, err == CHIP_ERROR_IM_MALFORMED_ATTRIBUTE_PATH); } @@ -625,7 +631,8 @@ void TestReadInteraction::TestReadClientGenerateOneEventPaths(nlTestSuite * apSu eventPathParams[0].mEventId = 4; EventPathIBs::Builder & eventPathListBuilder = request.CreateEventRequests(); - err = readClient.GenerateEventPaths(eventPathListBuilder, eventPathParams, 1 /*aEventPathParamsListSize*/); + Span eventPaths(eventPathParams, 1 /*aEventPathParamsListSize*/); + err = readClient.GenerateEventPaths(eventPathListBuilder, eventPaths); NL_TEST_ASSERT(apSuite, err == CHIP_NO_ERROR); request.IsFabricFiltered(false).EndOfReadRequestMessage(); @@ -676,7 +683,8 @@ void TestReadInteraction::TestReadClientGenerateTwoEventPaths(nlTestSuite * apSu eventPathParams[1].mEventId = 5; EventPathIBs::Builder & eventPathListBuilder = request.CreateEventRequests(); - err = readClient.GenerateEventPaths(eventPathListBuilder, eventPathParams, 2 /*aEventPathParamsListSize*/); + Span eventPaths(eventPathParams, 2 /*aEventPathParamsListSize*/); + err = readClient.GenerateEventPaths(eventPathListBuilder, eventPaths); NL_TEST_ASSERT(apSuite, err == CHIP_NO_ERROR); request.IsFabricFiltered(false).EndOfReadRequestMessage(); diff --git a/src/app/util/mock/Constants.h b/src/app/util/mock/Constants.h index 73e965a8028cd5..26abe2fc6649a0 100644 --- a/src/app/util/mock/Constants.h +++ b/src/app/util/mock/Constants.h @@ -40,7 +40,7 @@ constexpr AttributeId MockAttributeId(const uint16_t & id) return (0xFFF1'0000 | id); } -constexpr AttributeId MockClusterId(const uint16_t & id) +constexpr ClusterId MockClusterId(const uint16_t & id) { return (0xFFF1'0000 | id); } diff --git a/src/app/util/mock/Functions.h b/src/app/util/mock/Functions.h index 404bbde4a58926..1dc692669b4cab 100644 --- a/src/app/util/mock/Functions.h +++ b/src/app/util/mock/Functions.h @@ -34,5 +34,7 @@ namespace Test { CHIP_ERROR ReadSingleMockClusterData(FabricIndex aAccessingFabricIndex, const app::ConcreteAttributePath & aPath, app::AttributeReportIBs::Builder & aAttributeReports, app::AttributeValueEncoder::AttributeEncodeState * apEncoderState); +void BumpVersion(); +DataVersion GetVersion(); } // namespace Test } // namespace chip diff --git a/src/app/util/mock/attribute-storage.cpp b/src/app/util/mock/attribute-storage.cpp index 1f1656e07736c7..9b40014d416f4a 100644 --- a/src/app/util/mock/attribute-storage.cpp +++ b/src/app/util/mock/attribute-storage.cpp @@ -56,7 +56,7 @@ using namespace chip::Test; using namespace chip::app; namespace { - +DataVersion dataVersion = 0; EndpointId endpoints[] = { kMockEndpoint1, kMockEndpoint2, kMockEndpoint3 }; uint16_t clusterIndex[] = { 0, 2, 5 }; uint8_t clusterCount[] = { 2, 3, 4 }; @@ -254,6 +254,16 @@ AttributeAccessInterface * GetAttributeAccessOverride(EndpointId aEndpointId, Cl } // namespace app namespace Test { +void BumpVersion() +{ + dataVersion++; +} + +DataVersion GetVersion() +{ + return dataVersion; +} + CHIP_ERROR ReadSingleMockClusterData(FabricIndex aAccessingFabricIndex, const ConcreteAttributePath & aPath, AttributeReportIBs::Builder & aAttributeReports, AttributeValueEncoder::AttributeEncodeState * apEncoderState) @@ -261,8 +271,8 @@ CHIP_ERROR ReadSingleMockClusterData(FabricIndex aAccessingFabricIndex, const Co bool dataExists = (emberAfGetServerAttributeIndexByAttributeId(aPath.mEndpointId, aPath.mClusterId, aPath.mAttributeId) != UINT16_MAX); - ChipLogDetail(DataManagement, "Reading Mock Cluster %" PRIx32 ", Field %" PRIx32 " is dirty", aPath.mClusterId, - aPath.mAttributeId); + ChipLogDetail(DataManagement, "Reading Mock Endpoint %" PRIx32 "Mock Cluster %" PRIx32 ", Field %" PRIx32 " is dirty", + aPath.mEndpointId, aPath.mClusterId, aPath.mAttributeId); if (!dataExists) { @@ -288,7 +298,7 @@ CHIP_ERROR ReadSingleMockClusterData(FabricIndex aAccessingFabricIndex, const Co { AttributeValueEncoder::AttributeEncodeState state = (apEncoderState == nullptr ? AttributeValueEncoder::AttributeEncodeState() : *apEncoderState); - AttributeValueEncoder valueEncoder(aAttributeReports, aAccessingFabricIndex, aPath, 0, false, state); + AttributeValueEncoder valueEncoder(aAttributeReports, aAccessingFabricIndex, aPath, dataVersion, false, state); CHIP_ERROR err = valueEncoder.EncodeList([](const auto & encoder) -> CHIP_ERROR { for (int i = 0; i < 6; i++) @@ -309,7 +319,7 @@ CHIP_ERROR ReadSingleMockClusterData(FabricIndex aAccessingFabricIndex, const Co ReturnErrorOnFailure(aAttributeReports.GetError()); AttributeDataIB::Builder & attributeData = attributeReport.CreateAttributeData(); ReturnErrorOnFailure(attributeReport.GetError()); - attributeData.DataVersion(0); + attributeData.DataVersion(dataVersion); AttributePathIB::Builder & attributePath = attributeData.CreatePath(); ReturnErrorOnFailure(attributeData.GetError()); attributePath.Endpoint(aPath.mEndpointId).Cluster(aPath.mClusterId).Attribute(aPath.mAttributeId).EndOfAttributePathIB(); diff --git a/src/controller/tests/data_model/TestRead.cpp b/src/controller/tests/data_model/TestRead.cpp index 8e0d5b9b6b22b7..3bb43c7f7143f4 100644 --- a/src/controller/tests/data_model/TestRead.cpp +++ b/src/controller/tests/data_model/TestRead.cpp @@ -18,8 +18,11 @@ #include "transport/SecureSession.h" #include +#include #include #include +#include +#include #include #include #include @@ -59,6 +62,11 @@ CHIP_ERROR ReadSingleClusterData(const Access::SubjectDescriptor & aSubjectDescr const ConcreteReadAttributePath & aPath, AttributeReportIBs::Builder & aAttributeReports, AttributeValueEncoder::AttributeEncodeState * apEncoderState) { + if (aPath.mClusterId >= Test::kMockEndpointMin) + { + return Test::ReadSingleMockClusterData(aSubjectDescriptor.fabricIndex, aPath, aAttributeReports, apEncoderState); + } + if (responseDirective == kSendDataResponse) { if (aPath.mClusterId == app::Clusters::TestCluster::Id && @@ -66,8 +74,8 @@ CHIP_ERROR ReadSingleClusterData(const Access::SubjectDescriptor & aSubjectDescr { AttributeValueEncoder::AttributeEncodeState state = (apEncoderState == nullptr ? AttributeValueEncoder::AttributeEncodeState() : *apEncoderState); - AttributeValueEncoder valueEncoder(aAttributeReports, aSubjectDescriptor.fabricIndex, aPath, 0 /* data version */, - aIsFabricFiltered, state); + AttributeValueEncoder valueEncoder(aAttributeReports, aSubjectDescriptor.fabricIndex, aPath, + kDataVersion /* data version */, aIsFabricFiltered, state); return valueEncoder.EncodeList([aSubjectDescriptor](const auto & encoder) -> CHIP_ERROR { app::Clusters::TestCluster::Structs::TestFabricScoped::Type val; @@ -83,8 +91,8 @@ CHIP_ERROR ReadSingleClusterData(const Access::SubjectDescriptor & aSubjectDescr { AttributeValueEncoder::AttributeEncodeState state = (apEncoderState == nullptr ? AttributeValueEncoder::AttributeEncodeState() : *apEncoderState); - AttributeValueEncoder valueEncoder(aAttributeReports, aSubjectDescriptor.fabricIndex, aPath, 0 /* data version */, - aIsFabricFiltered, state); + AttributeValueEncoder valueEncoder(aAttributeReports, aSubjectDescriptor.fabricIndex, aPath, + kDataVersion /* data version */, aIsFabricFiltered, state); return valueEncoder.Encode(++totalReadCount); } @@ -133,9 +141,13 @@ CHIP_ERROR ReadSingleClusterData(const Access::SubjectDescriptor & aSubjectDescr return attributeReport.EndOfAttributeReportIB().GetError(); } -bool IsClusterDataVersionEqual(const ConcreteClusterPath & aConcreteClusterPath, DataVersion aRequiredDataVersion) +bool IsClusterDataVersionEqual(const ConcreteClusterPath & aConcreteClusterPath, DataVersion aRequiredVersion) { - if (aRequiredDataVersion == kDataVersion) + if (aRequiredVersion == kDataVersion) + { + return true; + } + if (Test::GetVersion() == aRequiredVersion) { return true; } @@ -173,6 +185,7 @@ class TestReadInteraction : public app::ReadHandler::ApplicationCallback static void TestReadHandler_SubscriptionAlteredReportingIntervals(nlTestSuite * apSuite, void * apContext); static void TestReadHandlerResourceExhaustion_MultipleSubscriptions(nlTestSuite * apSuite, void * apContext); static void TestReadHandlerResourceExhaustion_MultipleReads(nlTestSuite * apSuite, void * apContext); + static void TestReadSubscribeAttributeResponseWithCache(nlTestSuite * apSuite, void * apContext); private: static constexpr uint16_t kTestMinInterval = 33; @@ -186,7 +199,6 @@ class TestReadInteraction : public app::ReadHandler::ApplicationCallback { ReturnErrorOnFailure(aReadHandler.SetReportingIntervals(kTestMinInterval, kTestMaxInterval)); } - return CHIP_NO_ERROR; } @@ -209,6 +221,53 @@ class TestReadInteraction : public app::ReadHandler::ApplicationCallback TestReadInteraction gTestReadInteraction; +class MockInteractionModelApp : public chip::app::AttributeCache::Callback +{ +public: + void OnEventData(const chip::app::EventHeader & aEventHeader, chip::TLV::TLVReader * apData, + const chip::app::StatusIB * apStatus) override + {} + + void OnAttributeData(const chip::app::ConcreteDataAttributePath & aPath, chip::TLV::TLVReader * apData, + const chip::app::StatusIB & status) override + { + if (status.mStatus == chip::Protocols::InteractionModel::Status::Success) + { + mNumAttributeResponse++; + } + } + + void OnError(CHIP_ERROR aError) override + { + mError = aError; + mReadError = true; + } + + void OnDone() override {} + + void OnDeallocatePaths(chip::app::ReadPrepareParams && aReadPrepareParams) override + { + if (aReadPrepareParams.mpAttributePathParamsList != nullptr) + { + delete[] aReadPrepareParams.mpAttributePathParamsList; + } + + if (aReadPrepareParams.mpEventPathParamsList != nullptr) + { + delete[] aReadPrepareParams.mpEventPathParamsList; + } + + if (aReadPrepareParams.mpDataVersionFilterList != nullptr) + { + delete[] aReadPrepareParams.mpDataVersionFilterList; + } + } + + int mNumAttributeResponse = 0; + bool mReadError = false; + CHIP_ERROR mError = CHIP_NO_ERROR; +}; + void TestReadInteraction::TestReadAttributeResponse(nlTestSuite * apSuite, void * apContext) { TestContext & ctx = *static_cast(apContext); @@ -295,6 +354,417 @@ void TestReadInteraction::TestReadDataVersionFilter(nlTestSuite * apSuite, void NL_TEST_ASSERT(apSuite, ctx.GetExchangeManager().GetNumActiveExchanges() == 0); } +void TestReadInteraction::TestReadSubscribeAttributeResponseWithCache(nlTestSuite * apSuite, void * apContext) +{ + TestContext & ctx = *static_cast(apContext); + CHIP_ERROR err = CHIP_NO_ERROR; + responseDirective = kSendDataResponse; + + MockInteractionModelApp delegate; + chip::app::AttributeCache cache(delegate); + auto * engine = chip::app::InteractionModelEngine::GetInstance(); + err = engine->Init(&ctx.GetExchangeManager()); + NL_TEST_ASSERT(apSuite, err == CHIP_NO_ERROR); + + chip::app::EventPathParams eventPathParams[100]; + for (uint32_t index = 0; index < 100; index++) + { + eventPathParams[index].mEndpointId = Test::kMockEndpoint3; + eventPathParams[index].mClusterId = Test::MockClusterId(2); + eventPathParams[index].mEventId = 0; + } + + chip::app::ReadPrepareParams readPrepareParams(ctx.GetSessionBobToAlice()); + readPrepareParams.mMinIntervalFloorSeconds = 0; + readPrepareParams.mMaxIntervalCeilingSeconds = 4; + // + // Test the application callback as well to ensure we get the right number of SubscriptionEstablishment/Termination + // callbacks. + // + app::InteractionModelEngine::GetInstance()->RegisterReadHandlerAppCallback(&gTestReadInteraction); + + int testId = 0; + // Initial Read of E2C3A1, E2C3A2 and E3C2A2. + // Expect no versions would be cached. + { + testId++; + ChipLogProgress(DataManagement, "\t -- Running Read with AttributeCache Test ID %d", testId); + app::ReadClient readClient(chip::app::InteractionModelEngine::GetInstance(), &ctx.GetExchangeManager(), + cache.GetBufferedCallback(), chip::app::ReadClient::InteractionType::Read); + chip::app::AttributePathParams attributePathParams1[3]; + attributePathParams1[0].mEndpointId = Test::kMockEndpoint2; + attributePathParams1[0].mClusterId = Test::MockClusterId(3); + attributePathParams1[0].mAttributeId = Test::MockAttributeId(1); + + attributePathParams1[1].mEndpointId = Test::kMockEndpoint2; + attributePathParams1[1].mClusterId = Test::MockClusterId(3); + attributePathParams1[1].mAttributeId = Test::MockAttributeId(2); + + attributePathParams1[2].mEndpointId = Test::kMockEndpoint3; + attributePathParams1[2].mClusterId = Test::MockClusterId(2); + attributePathParams1[2].mAttributeId = Test::MockAttributeId(2); + + readPrepareParams.mpAttributePathParamsList = attributePathParams1; + readPrepareParams.mAttributePathParamsListSize = 3; + err = readClient.SendRequest(readPrepareParams); + NL_TEST_ASSERT(apSuite, err == CHIP_NO_ERROR); + + ctx.DrainAndServiceIO(); + NL_TEST_ASSERT(apSuite, delegate.mNumAttributeResponse == 3); + NL_TEST_ASSERT(apSuite, !delegate.mReadError); + Optional version1; + NL_TEST_ASSERT(apSuite, cache.GetVersion(Test::kMockEndpoint2, Test::MockClusterId(3), version1) == CHIP_NO_ERROR); + NL_TEST_ASSERT(apSuite, !version1.HasValue()); + Optional version2; + NL_TEST_ASSERT(apSuite, cache.GetVersion(Test::kMockEndpoint3, Test::MockClusterId(2), version2) == CHIP_NO_ERROR); + NL_TEST_ASSERT(apSuite, !version2.HasValue()); + delegate.mNumAttributeResponse = 0; + } + // Second read of E2C2A* and E3C2A2. We cannot use the stored data versions in the cache since there is no cached version from + // previous test. Expect cache E2C2 version + { + testId++; + ChipLogProgress(DataManagement, "\t -- Running Read with AttributeCache Test ID %d", testId); + app::ReadClient readClient(chip::app::InteractionModelEngine::GetInstance(), &ctx.GetExchangeManager(), + cache.GetBufferedCallback(), chip::app::ReadClient::InteractionType::Read); + chip::app::AttributePathParams attributePathParams2[2]; + attributePathParams2[0].mEndpointId = Test::kMockEndpoint2; + attributePathParams2[0].mClusterId = Test::MockClusterId(3); + attributePathParams2[0].mAttributeId = kInvalidAttributeId; + + attributePathParams2[1].mEndpointId = Test::kMockEndpoint3; + attributePathParams2[1].mClusterId = Test::MockClusterId(2); + attributePathParams2[1].mAttributeId = Test::MockAttributeId(2); + readPrepareParams.mpAttributePathParamsList = attributePathParams2; + readPrepareParams.mAttributePathParamsListSize = 2; + err = readClient.SendRequest(readPrepareParams); + NL_TEST_ASSERT(apSuite, err == CHIP_NO_ERROR); + + ctx.DrainAndServiceIO(); + // There are supported 2 global and 3 non-global attributes in E2C2A* and 1 E3C2A2 + NL_TEST_ASSERT(apSuite, delegate.mNumAttributeResponse == 6); + NL_TEST_ASSERT(apSuite, !delegate.mReadError); + Optional version1; + NL_TEST_ASSERT(apSuite, cache.GetVersion(Test::kMockEndpoint2, Test::MockClusterId(3), version1) == CHIP_NO_ERROR); + NL_TEST_ASSERT(apSuite, version1.HasValue() && (version1.Value() == 0)); + Optional version2; + NL_TEST_ASSERT(apSuite, cache.GetVersion(Test::kMockEndpoint3, Test::MockClusterId(2), version2) == CHIP_NO_ERROR); + NL_TEST_ASSERT(apSuite, !version2.HasValue()); + delegate.mNumAttributeResponse = 0; + } + + // Third read of E2C3A1, E2C3A2, and E3C2A2. It would use the stored data versions in the cache since our subsequent read's C1A1 + // path intersects with previous cached data version Expect no E2C3 attributes in report, only E3C2A1 attribute in report + { + testId++; + ChipLogProgress(DataManagement, "\t -- Running Read with AttributeCache Test ID %d", testId); + app::ReadClient readClient(chip::app::InteractionModelEngine::GetInstance(), &ctx.GetExchangeManager(), + cache.GetBufferedCallback(), chip::app::ReadClient::InteractionType::Read); + chip::app::AttributePathParams attributePathParams1[3]; + attributePathParams1[0].mEndpointId = Test::kMockEndpoint2; + attributePathParams1[0].mClusterId = Test::MockClusterId(3); + attributePathParams1[0].mAttributeId = Test::MockAttributeId(1); + + attributePathParams1[1].mEndpointId = Test::kMockEndpoint2; + attributePathParams1[1].mClusterId = Test::MockClusterId(3); + attributePathParams1[1].mAttributeId = Test::MockAttributeId(2); + + attributePathParams1[2].mEndpointId = Test::kMockEndpoint3; + attributePathParams1[2].mClusterId = Test::MockClusterId(2); + attributePathParams1[2].mAttributeId = Test::MockAttributeId(2); + + readPrepareParams.mpAttributePathParamsList = attributePathParams1; + readPrepareParams.mAttributePathParamsListSize = 3; + err = readClient.SendRequest(readPrepareParams); + NL_TEST_ASSERT(apSuite, err == CHIP_NO_ERROR); + + ctx.DrainAndServiceIO(); + NL_TEST_ASSERT(apSuite, delegate.mNumAttributeResponse == 1); + NL_TEST_ASSERT(apSuite, !delegate.mReadError); + Optional version1; + NL_TEST_ASSERT(apSuite, cache.GetVersion(Test::kMockEndpoint2, Test::MockClusterId(3), version1) == CHIP_NO_ERROR); + NL_TEST_ASSERT(apSuite, version1.HasValue() && (version1.Value() == 0)); + Optional version2; + NL_TEST_ASSERT(apSuite, cache.GetVersion(Test::kMockEndpoint3, Test::MockClusterId(2), version2) == CHIP_NO_ERROR); + NL_TEST_ASSERT(apSuite, !version2.HasValue()); + delegate.mNumAttributeResponse = 0; + } + + // Fourth read of E2C3A* and E3C2A2. It would use the stored data versions in the cache since our subsequent read's C1A* path + // intersects with previous cached data version Expect no C1 attributes in report, only E3C2A2 attribute in report + { + testId++; + ChipLogProgress(DataManagement, "\t -- Running Read with AttributeCache Test ID %d", testId); + app::ReadClient readClient(chip::app::InteractionModelEngine::GetInstance(), &ctx.GetExchangeManager(), + cache.GetBufferedCallback(), chip::app::ReadClient::InteractionType::Read); + chip::app::AttributePathParams attributePathParams2[2]; + attributePathParams2[0].mEndpointId = Test::kMockEndpoint2; + attributePathParams2[0].mClusterId = Test::MockClusterId(3); + attributePathParams2[0].mAttributeId = kInvalidAttributeId; + + attributePathParams2[1].mEndpointId = Test::kMockEndpoint3; + attributePathParams2[1].mClusterId = Test::MockClusterId(2); + attributePathParams2[1].mAttributeId = Test::MockAttributeId(2); + readPrepareParams.mpAttributePathParamsList = attributePathParams2; + readPrepareParams.mAttributePathParamsListSize = 2; + err = readClient.SendRequest(readPrepareParams); + NL_TEST_ASSERT(apSuite, err == CHIP_NO_ERROR); + + ctx.DrainAndServiceIO(); + NL_TEST_ASSERT(apSuite, delegate.mNumAttributeResponse == 1); + NL_TEST_ASSERT(apSuite, !delegate.mReadError); + Optional version1; + NL_TEST_ASSERT(apSuite, cache.GetVersion(Test::kMockEndpoint2, Test::MockClusterId(3), version1) == CHIP_NO_ERROR); + NL_TEST_ASSERT(apSuite, version1.HasValue() && (version1.Value() == 0)); + Optional version2; + NL_TEST_ASSERT(apSuite, cache.GetVersion(Test::kMockEndpoint3, Test::MockClusterId(2), version2) == CHIP_NO_ERROR); + NL_TEST_ASSERT(apSuite, !version2.HasValue()); + delegate.mNumAttributeResponse = 0; + } + + Test::BumpVersion(); + + // Fifth read of E2C3A1, E2C3A2 and E3C2A2. It would use the stored data versions in the cache since our subsequent read's C1A* + // path intersects with previous cached data version, server's version is changed. Expect E2C3A1, E2C3A2 and E3C2A2 attribute in + // report, and invalidate the cached pending and committed data version since no wildcard attributes exists in mRequestPathSet. + { + testId++; + ChipLogProgress(DataManagement, "\t -- Running Read with AttributeCache Test ID %d", testId); + app::ReadClient readClient(chip::app::InteractionModelEngine::GetInstance(), &ctx.GetExchangeManager(), + cache.GetBufferedCallback(), chip::app::ReadClient::InteractionType::Read); + chip::app::AttributePathParams attributePathParams1[3]; + attributePathParams1[0].mEndpointId = Test::kMockEndpoint2; + attributePathParams1[0].mClusterId = Test::MockClusterId(3); + attributePathParams1[0].mAttributeId = Test::MockAttributeId(1); + + attributePathParams1[1].mEndpointId = Test::kMockEndpoint2; + attributePathParams1[1].mClusterId = Test::MockClusterId(3); + attributePathParams1[1].mAttributeId = Test::MockAttributeId(2); + + attributePathParams1[2].mEndpointId = Test::kMockEndpoint3; + attributePathParams1[2].mClusterId = Test::MockClusterId(2); + attributePathParams1[2].mAttributeId = Test::MockAttributeId(2); + + readPrepareParams.mpAttributePathParamsList = attributePathParams1; + readPrepareParams.mAttributePathParamsListSize = 3; + err = readClient.SendRequest(readPrepareParams); + NL_TEST_ASSERT(apSuite, err == CHIP_NO_ERROR); + + ctx.DrainAndServiceIO(); + NL_TEST_ASSERT(apSuite, delegate.mNumAttributeResponse == 3); + NL_TEST_ASSERT(apSuite, !delegate.mReadError); + Optional version1; + NL_TEST_ASSERT(apSuite, cache.GetVersion(Test::kMockEndpoint2, Test::MockClusterId(3), version1) == CHIP_NO_ERROR); + NL_TEST_ASSERT(apSuite, !version1.HasValue()); + Optional version2; + NL_TEST_ASSERT(apSuite, cache.GetVersion(Test::kMockEndpoint3, Test::MockClusterId(2), version2) == CHIP_NO_ERROR); + NL_TEST_ASSERT(apSuite, !version2.HasValue()); + delegate.mNumAttributeResponse = 0; + } + + // Sixth read of E2C3A1, E2C3A2 and E3C2A2. It would use none stored data versions in the cache since previous read does not + // cache any committed data version. Expect E2C3A1, E2C3A2 and E3C2A2 attribute in report + { + testId++; + ChipLogProgress(DataManagement, "\t -- Running Read with AttributeCache Test ID %d", testId); + app::ReadClient readClient(chip::app::InteractionModelEngine::GetInstance(), &ctx.GetExchangeManager(), + cache.GetBufferedCallback(), chip::app::ReadClient::InteractionType::Read); + chip::app::AttributePathParams attributePathParams1[3]; + attributePathParams1[0].mEndpointId = Test::kMockEndpoint2; + attributePathParams1[0].mClusterId = Test::MockClusterId(3); + attributePathParams1[0].mAttributeId = Test::MockAttributeId(1); + + attributePathParams1[1].mEndpointId = Test::kMockEndpoint2; + attributePathParams1[1].mClusterId = Test::MockClusterId(3); + attributePathParams1[1].mAttributeId = Test::MockAttributeId(2); + + attributePathParams1[2].mEndpointId = Test::kMockEndpoint3; + attributePathParams1[2].mClusterId = Test::MockClusterId(2); + attributePathParams1[2].mAttributeId = Test::MockAttributeId(2); + + readPrepareParams.mpAttributePathParamsList = attributePathParams1; + readPrepareParams.mAttributePathParamsListSize = 3; + err = readClient.SendRequest(readPrepareParams); + NL_TEST_ASSERT(apSuite, err == CHIP_NO_ERROR); + + ctx.DrainAndServiceIO(); + NL_TEST_ASSERT(apSuite, delegate.mNumAttributeResponse == 3); + NL_TEST_ASSERT(apSuite, !delegate.mReadError); + Optional version1; + NL_TEST_ASSERT(apSuite, cache.GetVersion(Test::kMockEndpoint2, Test::MockClusterId(3), version1) == CHIP_NO_ERROR); + NL_TEST_ASSERT(apSuite, !version1.HasValue()); + Optional version2; + NL_TEST_ASSERT(apSuite, cache.GetVersion(Test::kMockEndpoint3, Test::MockClusterId(2), version2) == CHIP_NO_ERROR); + NL_TEST_ASSERT(apSuite, !version2.HasValue()); + delegate.mNumAttributeResponse = 0; + } + + // Seventh read of E2C3A* and E3C2A2, here there is no cached data version filter + // Expect E2C3A* attributes in report, and E3C2A2 attribute in report and cache latest data version + { + testId++; + ChipLogProgress(DataManagement, "\t -- Running Read with AttributeCache Test ID %d", testId); + app::ReadClient readClient(chip::app::InteractionModelEngine::GetInstance(), &ctx.GetExchangeManager(), + cache.GetBufferedCallback(), chip::app::ReadClient::InteractionType::Read); + chip::app::AttributePathParams attributePathParams2[2]; + attributePathParams2[0].mEndpointId = Test::kMockEndpoint2; + attributePathParams2[0].mClusterId = Test::MockClusterId(3); + attributePathParams2[0].mAttributeId = kInvalidAttributeId; + + attributePathParams2[1].mEndpointId = Test::kMockEndpoint3; + attributePathParams2[1].mClusterId = Test::MockClusterId(2); + attributePathParams2[1].mAttributeId = Test::MockAttributeId(2); + readPrepareParams.mpAttributePathParamsList = attributePathParams2; + readPrepareParams.mAttributePathParamsListSize = 2; + err = readClient.SendRequest(readPrepareParams); + NL_TEST_ASSERT(apSuite, err == CHIP_NO_ERROR); + + ctx.DrainAndServiceIO(); + NL_TEST_ASSERT(apSuite, delegate.mNumAttributeResponse == 6); + NL_TEST_ASSERT(apSuite, !delegate.mReadError); + Optional version1; + NL_TEST_ASSERT(apSuite, cache.GetVersion(Test::kMockEndpoint2, Test::MockClusterId(3), version1) == CHIP_NO_ERROR); + NL_TEST_ASSERT(apSuite, version1.HasValue() && (version1.Value() == 1)); + Optional version2; + NL_TEST_ASSERT(apSuite, cache.GetVersion(Test::kMockEndpoint3, Test::MockClusterId(2), version2) == CHIP_NO_ERROR); + NL_TEST_ASSERT(apSuite, !version2.HasValue()); + delegate.mNumAttributeResponse = 0; + } + + // Eighth read of E2C3A* and E3C2A2, and inject a large amount of event path list, then it would try to apply previous cache + // latest data version and construct data version list but no enough memory, finally fully rollback data version filter. Expect + // E2C3A* attributes in report, and E3C2A2 attribute in report + { + testId++; + ChipLogProgress(DataManagement, "\t -- Running Read with AttributeCache Test ID %d", testId); + app::ReadClient readClient(chip::app::InteractionModelEngine::GetInstance(), &ctx.GetExchangeManager(), + cache.GetBufferedCallback(), chip::app::ReadClient::InteractionType::Read); + chip::app::AttributePathParams attributePathParams2[2]; + attributePathParams2[0].mEndpointId = Test::kMockEndpoint2; + attributePathParams2[0].mClusterId = Test::MockClusterId(3); + attributePathParams2[0].mAttributeId = kInvalidAttributeId; + + attributePathParams2[1].mEndpointId = Test::kMockEndpoint3; + attributePathParams2[1].mClusterId = Test::MockClusterId(2); + attributePathParams2[1].mAttributeId = Test::MockAttributeId(2); + readPrepareParams.mpAttributePathParamsList = attributePathParams2; + readPrepareParams.mAttributePathParamsListSize = 2; + + readPrepareParams.mpEventPathParamsList = eventPathParams; + readPrepareParams.mEventPathParamsListSize = 64; + + err = readClient.SendRequest(readPrepareParams); + NL_TEST_ASSERT(apSuite, err == CHIP_NO_ERROR); + + ctx.DrainAndServiceIO(); + NL_TEST_ASSERT(apSuite, delegate.mNumAttributeResponse == 6); + NL_TEST_ASSERT(apSuite, !delegate.mReadError); + Optional version1; + NL_TEST_ASSERT(apSuite, cache.GetVersion(Test::kMockEndpoint2, Test::MockClusterId(3), version1) == CHIP_NO_ERROR); + NL_TEST_ASSERT(apSuite, version1.HasValue() && (version1.Value() == 1)); + Optional version2; + NL_TEST_ASSERT(apSuite, cache.GetVersion(Test::kMockEndpoint3, Test::MockClusterId(2), version2) == CHIP_NO_ERROR); + NL_TEST_ASSERT(apSuite, !version2.HasValue()); + delegate.mNumAttributeResponse = 0; + readPrepareParams.mpEventPathParamsList = nullptr; + readPrepareParams.mEventPathParamsListSize = 0; + } + + Test::BumpVersion(); + + // Ninth read of E1C2A* and E2C3A* and E2C2A*, it would use C1 cached version to construct DataVersionFilter, but version has + // changed in server. Expect E1C2A* and C2C3A* and E2C2A* attributes in report, and cache their versions + { + testId++; + ChipLogProgress(DataManagement, "\t -- Running Read with AttributeCache Test ID %d", testId); + app::ReadClient readClient(chip::app::InteractionModelEngine::GetInstance(), &ctx.GetExchangeManager(), + cache.GetBufferedCallback(), chip::app::ReadClient::InteractionType::Read); + + chip::app::AttributePathParams attributePathParams3[3]; + attributePathParams3[0].mEndpointId = Test::kMockEndpoint1; + attributePathParams3[0].mClusterId = Test::MockClusterId(2); + attributePathParams3[0].mAttributeId = kInvalidAttributeId; + + attributePathParams3[1].mEndpointId = Test::kMockEndpoint2; + attributePathParams3[1].mClusterId = Test::MockClusterId(3); + attributePathParams3[1].mAttributeId = kInvalidAttributeId; + + attributePathParams3[2].mEndpointId = Test::kMockEndpoint2; + attributePathParams3[2].mClusterId = Test::MockClusterId(2); + attributePathParams3[2].mAttributeId = kInvalidAttributeId; + readPrepareParams.mpAttributePathParamsList = attributePathParams3; + readPrepareParams.mAttributePathParamsListSize = 3; + + err = readClient.SendRequest(readPrepareParams); + NL_TEST_ASSERT(apSuite, err == CHIP_NO_ERROR); + + ctx.DrainAndServiceIO(); + // E1C2A* has 3 attributes and E2C3A* has 5 attributes and E2C2A* has 4 attributes + NL_TEST_ASSERT(apSuite, delegate.mNumAttributeResponse == 12); + NL_TEST_ASSERT(apSuite, !delegate.mReadError); + Optional version1; + NL_TEST_ASSERT(apSuite, cache.GetVersion(Test::kMockEndpoint2, Test::MockClusterId(3), version1) == CHIP_NO_ERROR); + NL_TEST_ASSERT(apSuite, version1.HasValue() && (version1.Value() == 2)); + Optional version2; + NL_TEST_ASSERT(apSuite, cache.GetVersion(Test::kMockEndpoint2, Test::MockClusterId(2), version2) == CHIP_NO_ERROR); + NL_TEST_ASSERT(apSuite, version2.HasValue() && (version2.Value() == 2)); + Optional version3; + NL_TEST_ASSERT(apSuite, cache.GetVersion(Test::kMockEndpoint1, Test::MockClusterId(2), version3) == CHIP_NO_ERROR); + NL_TEST_ASSERT(apSuite, version3.HasValue() && (version3.Value() == 2)); + delegate.mNumAttributeResponse = 0; + } + + // Tenth read of E1C2A*(3 attributes) and E2C3A*(5 attributes) and E2C2A*(4 attributes), and inject a large amount of event path + // list, then it would try to apply previous cache latest data version and construct data version list with the ordering from + // largest cluster size to smallest cluster size(C2, C3, C1) but no enough memory, finally partially rollback data version + // filter with only C2. Expect E1C2A*, E2C2A* attributes(7 attributes) in report, + { + testId++; + ChipLogProgress(DataManagement, "\t -- Running Read with AttributeCache Test ID %d", testId); + app::ReadClient readClient(chip::app::InteractionModelEngine::GetInstance(), &ctx.GetExchangeManager(), + cache.GetBufferedCallback(), chip::app::ReadClient::InteractionType::Read); + + chip::app::AttributePathParams attributePathParams3[3]; + attributePathParams3[0].mEndpointId = Test::kMockEndpoint1; + attributePathParams3[0].mClusterId = Test::MockClusterId(2); + attributePathParams3[0].mAttributeId = kInvalidAttributeId; + + attributePathParams3[1].mEndpointId = Test::kMockEndpoint2; + attributePathParams3[1].mClusterId = Test::MockClusterId(3); + attributePathParams3[1].mAttributeId = kInvalidAttributeId; + + attributePathParams3[2].mEndpointId = Test::kMockEndpoint2; + attributePathParams3[2].mClusterId = Test::MockClusterId(2); + attributePathParams3[2].mAttributeId = kInvalidAttributeId; + readPrepareParams.mpAttributePathParamsList = attributePathParams3; + readPrepareParams.mAttributePathParamsListSize = 3; + readPrepareParams.mpEventPathParamsList = eventPathParams; + readPrepareParams.mEventPathParamsListSize = 62; + err = readClient.SendRequest(readPrepareParams); + NL_TEST_ASSERT(apSuite, err == CHIP_NO_ERROR); + + ctx.DrainAndServiceIO(); + NL_TEST_ASSERT(apSuite, delegate.mNumAttributeResponse == 7); + NL_TEST_ASSERT(apSuite, !delegate.mReadError); + Optional version1; + NL_TEST_ASSERT(apSuite, cache.GetVersion(Test::kMockEndpoint2, Test::MockClusterId(3), version1) == CHIP_NO_ERROR); + NL_TEST_ASSERT(apSuite, version1.HasValue() && (version1.Value() == 2)); + Optional version2; + NL_TEST_ASSERT(apSuite, cache.GetVersion(Test::kMockEndpoint2, Test::MockClusterId(2), version2) == CHIP_NO_ERROR); + NL_TEST_ASSERT(apSuite, version2.HasValue() && (version2.Value() == 2)); + Optional version3; + NL_TEST_ASSERT(apSuite, cache.GetVersion(Test::kMockEndpoint1, Test::MockClusterId(2), version3) == CHIP_NO_ERROR); + NL_TEST_ASSERT(apSuite, version3.HasValue() && (version3.Value() == 2)); + delegate.mNumAttributeResponse = 0; + readPrepareParams.mpEventPathParamsList = nullptr; + readPrepareParams.mEventPathParamsListSize = 0; + } + + NL_TEST_ASSERT(apSuite, engine->GetNumActiveReadClients() == 0); + engine->Shutdown(); + NL_TEST_ASSERT(apSuite, ctx.GetExchangeManager().GetNumActiveExchanges() == 0); +} + void TestReadInteraction::TestReadEventResponse(nlTestSuite * apSuite, void * apContext) { TestContext & ctx = *static_cast(apContext); @@ -1010,6 +1480,7 @@ const nlTest sTests[] = NL_TEST_DEF("TestReadHandlerResourceExhaustion_MultipleReads", TestReadInteraction::TestReadHandlerResourceExhaustion_MultipleReads), NL_TEST_DEF("TestReadAttributeTimeout", TestReadInteraction::TestReadAttributeTimeout), NL_TEST_DEF("TestReadHandler_SubscriptionAlteredReportingIntervals", TestReadInteraction::TestReadHandler_SubscriptionAlteredReportingIntervals), + NL_TEST_DEF("TestReadSubscribeAttributeResponseWithCache", TestReadInteraction::TestReadSubscribeAttributeResponseWithCache), NL_TEST_SENTINEL() }; // clang-format on