Line data Source code
1 : /*
2 : *
3 : * Copyright (c) 2021 Project CHIP Authors
4 : * All rights reserved.
5 : *
6 : * Licensed under the Apache License, Version 2.0 (the "License");
7 : * you may not use this file except in compliance with the License.
8 : * You may obtain a copy of the License at
9 : *
10 : * http://www.apache.org/licenses/LICENSE-2.0
11 : *
12 : * Unless required by applicable law or agreed to in writing, software
13 : * distributed under the License is distributed on an "AS IS" BASIS,
14 : * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 : * See the License for the specific language governing permissions and
16 : * limitations under the License.
17 : */
18 :
19 : #include "system/SystemPacketBuffer.h"
20 : #include <app/ClusterStateCache.h>
21 : #include <app/InteractionModelEngine.h>
22 : #include <tuple>
23 :
24 : namespace chip {
25 : namespace app {
26 :
27 : namespace {
28 :
29 : // Determine how much space a StatusIB takes up on the wire.
30 36 : size_t SizeOfStatusIB(const StatusIB & aStatus)
31 : {
32 : // 1 byte: anonymous tag control byte for struct.
33 : // 1 byte: control byte for uint8 value.
34 : // 1 byte: context-specific tag for uint8 value.
35 : // 1 byte: the uint8 value.
36 : // 1 byte: end of container.
37 36 : size_t size = 5;
38 :
39 36 : if (aStatus.mClusterStatus.HasValue())
40 : {
41 : // 1 byte: control byte for uint8 value.
42 : // 1 byte: context-specific tag for uint8 value.
43 : // 1 byte: the uint8 value.
44 0 : size += 3;
45 : }
46 :
47 36 : return size;
48 : }
49 :
50 : } // anonymous namespace
51 :
52 80 : CHIP_ERROR ClusterStateCache::GetElementTLVSize(TLV::TLVReader * apData, size_t & aSize)
53 : {
54 80 : Platform::ScopedMemoryBufferWithSize<uint8_t> backingBuffer;
55 : TLV::TLVReader reader;
56 80 : reader.Init(*apData);
57 80 : size_t totalBufSize = reader.GetTotalLength();
58 80 : backingBuffer.Calloc(totalBufSize);
59 80 : VerifyOrReturnError(backingBuffer.Get() != nullptr, CHIP_ERROR_NO_MEMORY);
60 80 : TLV::ScopedBufferTLVWriter writer(std::move(backingBuffer), totalBufSize);
61 80 : ReturnErrorOnFailure(writer.CopyElement(TLV::AnonymousTag(), reader));
62 80 : aSize = writer.GetLengthWritten();
63 80 : ReturnErrorOnFailure(writer.Finalize(backingBuffer));
64 80 : return CHIP_NO_ERROR;
65 80 : }
66 :
67 115 : CHIP_ERROR ClusterStateCache::UpdateCache(const ConcreteDataAttributePath & aPath, TLV::TLVReader * apData,
68 : const StatusIB & aStatus)
69 : {
70 115 : AttributeState state;
71 115 : bool endpointIsNew = false;
72 :
73 115 : if (mCache.find(aPath.mEndpointId) == mCache.end())
74 : {
75 : //
76 : // Since we might potentially be creating a new entry at mCache[aPath.mEndpointId][aPath.mClusterId] that
77 : // wasn't there before, we need to check if an entry didn't exist there previously and remember that so that
78 : // we can appropriately notify our clients of the addition of a new endpoint.
79 : //
80 15 : endpointIsNew = true;
81 : }
82 :
83 115 : if (apData)
84 : {
85 80 : size_t elementSize = 0;
86 80 : ReturnErrorOnFailure(GetElementTLVSize(apData, elementSize));
87 :
88 80 : if (mCacheData)
89 : {
90 74 : Platform::ScopedMemoryBufferWithSize<uint8_t> backingBuffer;
91 74 : backingBuffer.Calloc(elementSize);
92 74 : VerifyOrReturnError(backingBuffer.Get() != nullptr, CHIP_ERROR_NO_MEMORY);
93 74 : TLV::ScopedBufferTLVWriter writer(std::move(backingBuffer), elementSize);
94 74 : ReturnErrorOnFailure(writer.CopyElement(TLV::AnonymousTag(), *apData));
95 74 : ReturnErrorOnFailure(writer.Finalize(backingBuffer));
96 :
97 74 : state.Set<AttributeData>(std::move(backingBuffer));
98 74 : }
99 : else
100 : {
101 6 : state.Set<size_t>(elementSize);
102 : }
103 : //
104 : // Clear out the committed data version and only set it again once we have received all data for this cluster.
105 : // Otherwise, we may have incomplete data that looks like it's complete since it has a valid data version.
106 : //
107 80 : mCache[aPath.mEndpointId][aPath.mClusterId].mCommittedDataVersion.ClearValue();
108 :
109 : // This commits a pending data version if the last report path is valid and it is different from the current path.
110 80 : if (mLastReportDataPath.IsValidConcreteClusterPath() && mLastReportDataPath != aPath)
111 : {
112 12 : CommitPendingDataVersion();
113 : }
114 :
115 80 : bool foundEncompassingWildcardPath = false;
116 102 : for (const auto & path : mRequestPathSet)
117 : {
118 79 : if (path.IncludesAllAttributesInCluster(aPath))
119 : {
120 57 : foundEncompassingWildcardPath = true;
121 57 : break;
122 : }
123 : }
124 :
125 : // if this data item is encompassed by a wildcard path, let's go ahead and update its pending data version.
126 80 : if (foundEncompassingWildcardPath)
127 : {
128 57 : mCache[aPath.mEndpointId][aPath.mClusterId].mPendingDataVersion = aPath.mDataVersion;
129 : }
130 :
131 80 : mLastReportDataPath = aPath;
132 : }
133 : else
134 : {
135 35 : if (mCacheData)
136 : {
137 32 : state.Set<StatusIB>(aStatus);
138 : }
139 : else
140 : {
141 3 : state.Set<size_t>(SizeOfStatusIB(aStatus));
142 : }
143 : }
144 :
145 : //
146 : // if the endpoint didn't exist previously, let's track the insertion
147 : // so that we can inform our callback of a new endpoint being added appropriately.
148 : //
149 115 : if (endpointIsNew)
150 : {
151 15 : mAddedEndpoints.push_back(aPath.mEndpointId);
152 : }
153 :
154 115 : mCache[aPath.mEndpointId][aPath.mClusterId].mAttributes[aPath.mAttributeId] = std::move(state);
155 :
156 115 : if (mCacheData)
157 : {
158 106 : mChangedAttributeSet.insert(aPath);
159 : }
160 :
161 115 : return CHIP_NO_ERROR;
162 115 : }
163 :
164 32 : CHIP_ERROR ClusterStateCache::UpdateEventCache(const EventHeader & aEventHeader, TLV::TLVReader * apData, const StatusIB * apStatus)
165 : {
166 32 : if (apData)
167 : {
168 : //
169 : // If we've already seen this event before, there's no more work to be done.
170 : //
171 32 : if (mHighestReceivedEventNumber.HasValue() && aEventHeader.mEventNumber <= mHighestReceivedEventNumber.Value())
172 : {
173 5 : return CHIP_NO_ERROR;
174 : }
175 27 : if (mCacheData)
176 : {
177 21 : System::PacketBufferHandle handle = System::PacketBufferHandle::New(chip::app::kMaxSecureSduLengthBytes);
178 21 : VerifyOrReturnError(!handle.IsNull(), CHIP_ERROR_NO_MEMORY);
179 :
180 21 : System::PacketBufferTLVWriter writer;
181 21 : writer.Init(std::move(handle), false);
182 :
183 21 : ReturnErrorOnFailure(writer.CopyElement(TLV::AnonymousTag(), *apData));
184 21 : ReturnErrorOnFailure(writer.Finalize(&handle));
185 :
186 : //
187 : // Compact the buffer down to a more reasonably sized packet buffer
188 : // if we can.
189 : //
190 21 : handle.RightSize();
191 :
192 21 : EventData eventData;
193 21 : eventData.first = aEventHeader;
194 21 : eventData.second = std::move(handle);
195 :
196 21 : mEventDataCache.insert(std::move(eventData));
197 21 : }
198 27 : mHighestReceivedEventNumber.SetValue(aEventHeader.mEventNumber);
199 : }
200 0 : else if (apStatus)
201 : {
202 0 : if (mCacheData)
203 : {
204 0 : mEventStatusCache[aEventHeader.mPath] = *apStatus;
205 : }
206 : }
207 :
208 27 : return CHIP_NO_ERROR;
209 : }
210 :
211 29 : void ClusterStateCache::OnReportBegin()
212 : {
213 29 : mLastReportDataPath = ConcreteClusterPath(kInvalidEndpointId, kInvalidClusterId);
214 29 : mChangedAttributeSet.clear();
215 29 : mAddedEndpoints.clear();
216 29 : mCallback.OnReportBegin();
217 29 : }
218 :
219 41 : void ClusterStateCache::CommitPendingDataVersion()
220 : {
221 41 : if (!mLastReportDataPath.IsValidConcreteClusterPath())
222 : {
223 7 : return;
224 : }
225 :
226 34 : auto & lastClusterInfo = mCache[mLastReportDataPath.mEndpointId][mLastReportDataPath.mClusterId];
227 34 : if (lastClusterInfo.mPendingDataVersion.HasValue())
228 : {
229 19 : lastClusterInfo.mCommittedDataVersion = lastClusterInfo.mPendingDataVersion;
230 19 : lastClusterInfo.mPendingDataVersion.ClearValue();
231 : }
232 : }
233 :
234 29 : void ClusterStateCache::OnReportEnd()
235 : {
236 29 : CommitPendingDataVersion();
237 29 : mLastReportDataPath = ConcreteClusterPath(kInvalidEndpointId, kInvalidClusterId);
238 29 : std::set<std::tuple<EndpointId, ClusterId>> changedClusters;
239 :
240 : //
241 : // Add the EndpointId and ClusterId into a set so that we only
242 : // convey unique combinations in the subsequent OnClusterChanged callback.
243 : //
244 131 : for (auto & path : mChangedAttributeSet)
245 : {
246 102 : mCallback.OnAttributeChanged(this, path);
247 102 : changedClusters.insert(std::make_tuple(path.mEndpointId, path.mClusterId));
248 : }
249 :
250 62 : for (auto & item : changedClusters)
251 : {
252 33 : mCallback.OnClusterChanged(this, std::get<0>(item), std::get<1>(item));
253 : }
254 :
255 44 : for (auto endpoint : mAddedEndpoints)
256 : {
257 15 : mCallback.OnEndpointAdded(this, endpoint);
258 : }
259 :
260 29 : mCallback.OnReportEnd();
261 29 : }
262 :
263 74 : CHIP_ERROR ClusterStateCache::Get(const ConcreteAttributePath & path, TLV::TLVReader & reader) const
264 : {
265 : CHIP_ERROR err;
266 74 : auto attributeState = GetAttributeState(path.mEndpointId, path.mClusterId, path.mAttributeId, err);
267 74 : ReturnErrorOnFailure(err);
268 73 : if (attributeState->Is<StatusIB>())
269 : {
270 2 : return CHIP_ERROR_IM_STATUS_CODE_RECEIVED;
271 : }
272 :
273 71 : if (!attributeState->Is<AttributeData>())
274 : {
275 3 : return CHIP_ERROR_KEY_NOT_FOUND;
276 : }
277 :
278 68 : reader.Init(attributeState->Get<AttributeData>().Get(), attributeState->Get<AttributeData>().AllocatedSize());
279 68 : return reader.Next();
280 : }
281 :
282 44 : CHIP_ERROR ClusterStateCache::Get(EventNumber eventNumber, TLV::TLVReader & reader) const
283 : {
284 : CHIP_ERROR err;
285 :
286 44 : auto eventData = GetEventData(eventNumber, err);
287 44 : ReturnErrorOnFailure(err);
288 :
289 44 : System::PacketBufferTLVReader bufReader;
290 :
291 44 : bufReader.Init(eventData->second.Retain());
292 44 : ReturnErrorOnFailure(bufReader.Next());
293 :
294 44 : reader.Init(bufReader);
295 44 : return CHIP_NO_ERROR;
296 44 : }
297 :
298 114 : const ClusterStateCache::EndpointState * ClusterStateCache::GetEndpointState(EndpointId endpointId, CHIP_ERROR & err) const
299 : {
300 114 : auto endpointIter = mCache.find(endpointId);
301 114 : if (endpointIter == mCache.end())
302 : {
303 1 : err = CHIP_ERROR_KEY_NOT_FOUND;
304 1 : return nullptr;
305 : }
306 :
307 113 : err = CHIP_NO_ERROR;
308 113 : return &endpointIter->second;
309 : }
310 :
311 114 : const ClusterStateCache::ClusterState * ClusterStateCache::GetClusterState(EndpointId endpointId, ClusterId clusterId,
312 : CHIP_ERROR & err) const
313 : {
314 114 : auto endpointState = GetEndpointState(endpointId, err);
315 114 : if (err != CHIP_NO_ERROR)
316 : {
317 1 : return nullptr;
318 : }
319 :
320 113 : auto clusterState = endpointState->find(clusterId);
321 113 : if (clusterState == endpointState->end())
322 : {
323 0 : err = CHIP_ERROR_KEY_NOT_FOUND;
324 0 : return nullptr;
325 : }
326 :
327 113 : err = CHIP_NO_ERROR;
328 113 : return &clusterState->second;
329 : }
330 :
331 75 : const ClusterStateCache::AttributeState * ClusterStateCache::GetAttributeState(EndpointId endpointId, ClusterId clusterId,
332 : AttributeId attributeId, CHIP_ERROR & err) const
333 : {
334 75 : auto clusterState = GetClusterState(endpointId, clusterId, err);
335 75 : if (err != CHIP_NO_ERROR)
336 : {
337 1 : return nullptr;
338 : }
339 :
340 74 : auto attributeState = clusterState->mAttributes.find(attributeId);
341 74 : if (attributeState == clusterState->mAttributes.end())
342 : {
343 0 : err = CHIP_ERROR_KEY_NOT_FOUND;
344 0 : return nullptr;
345 : }
346 :
347 74 : err = CHIP_NO_ERROR;
348 74 : return &attributeState->second;
349 : }
350 :
351 88 : const ClusterStateCache::EventData * ClusterStateCache::GetEventData(EventNumber eventNumber, CHIP_ERROR & err) const
352 : {
353 88 : EventData compareKey;
354 :
355 88 : compareKey.first.mEventNumber = eventNumber;
356 88 : auto eventData = mEventDataCache.find(std::move(compareKey));
357 88 : if (eventData == mEventDataCache.end())
358 : {
359 0 : err = CHIP_ERROR_KEY_NOT_FOUND;
360 0 : return nullptr;
361 : }
362 :
363 88 : err = CHIP_NO_ERROR;
364 88 : return &(*eventData);
365 88 : }
366 :
367 115 : void ClusterStateCache::OnAttributeData(const ConcreteDataAttributePath & aPath, TLV::TLVReader * apData, const StatusIB & aStatus)
368 : {
369 : //
370 : // Since the cache itself is a ReadClient::Callback, it may be incorrectly passed in directly when registering with the
371 : // ReadClient. This should be avoided, since that bypasses the built-in buffered reader adapter callback that is needed for
372 : // lists to work correctly.
373 : //
374 : // Instead, the right callback should be retrieved using GetBufferedCallback().
375 : //
376 : // To catch such errors, we validate that the provided concrete path never indicates a raw list item operation (which the
377 : // buffered reader will handle and convert for us).
378 : //
379 : //
380 115 : VerifyOrDie(!aPath.IsListItemOperation());
381 :
382 : // Copy the reader for forwarding
383 : TLV::TLVReader dataSnapshot;
384 115 : if (apData)
385 : {
386 80 : dataSnapshot.Init(*apData);
387 : }
388 :
389 115 : UpdateCache(aPath, apData, aStatus);
390 :
391 : //
392 : // Forward the call through.
393 : //
394 115 : mCallback.OnAttributeData(aPath, apData ? &dataSnapshot : nullptr, aStatus);
395 115 : }
396 :
397 28 : CHIP_ERROR ClusterStateCache::GetVersion(const ConcreteClusterPath & aPath, Optional<DataVersion> & aVersion) const
398 : {
399 28 : VerifyOrReturnError(aPath.IsValidConcreteClusterPath(), CHIP_ERROR_INVALID_ARGUMENT);
400 : CHIP_ERROR err;
401 28 : auto clusterState = GetClusterState(aPath.mEndpointId, aPath.mClusterId, err);
402 28 : ReturnErrorOnFailure(err);
403 28 : aVersion = clusterState->mCommittedDataVersion;
404 28 : return CHIP_NO_ERROR;
405 : }
406 :
407 32 : void ClusterStateCache::OnEventData(const EventHeader & aEventHeader, TLV::TLVReader * apData, const StatusIB * apStatus)
408 : {
409 32 : VerifyOrDie(apData != nullptr || apStatus != nullptr);
410 :
411 : TLV::TLVReader dataSnapshot;
412 32 : if (apData)
413 : {
414 32 : dataSnapshot.Init(*apData);
415 : }
416 :
417 32 : UpdateEventCache(aEventHeader, apData, apStatus);
418 32 : mCallback.OnEventData(aEventHeader, apData ? &dataSnapshot : nullptr, apStatus);
419 32 : }
420 :
421 1 : CHIP_ERROR ClusterStateCache::GetStatus(const ConcreteAttributePath & path, StatusIB & status) const
422 : {
423 : CHIP_ERROR err;
424 :
425 1 : auto attributeState = GetAttributeState(path.mEndpointId, path.mClusterId, path.mAttributeId, err);
426 1 : ReturnErrorOnFailure(err);
427 :
428 1 : if (!attributeState->Is<StatusIB>())
429 : {
430 0 : return CHIP_ERROR_INVALID_ARGUMENT;
431 : }
432 :
433 1 : status = attributeState->Get<StatusIB>();
434 1 : return CHIP_NO_ERROR;
435 : }
436 :
437 0 : CHIP_ERROR ClusterStateCache::GetStatus(const ConcreteEventPath & path, StatusIB & status) const
438 : {
439 0 : auto statusIter = mEventStatusCache.find(path);
440 0 : if (statusIter == mEventStatusCache.end())
441 : {
442 0 : return CHIP_ERROR_KEY_NOT_FOUND;
443 : }
444 :
445 0 : status = statusIter->second;
446 0 : return CHIP_NO_ERROR;
447 : }
448 :
449 545 : void ClusterStateCache::GetSortedFilters(std::vector<std::pair<DataVersionFilter, size_t>> & aVector) const
450 : {
451 1167 : for (auto const & endpointIter : mCache)
452 : {
453 622 : EndpointId endpointId = endpointIter.first;
454 1254 : for (auto const & clusterIter : endpointIter.second)
455 : {
456 632 : if (!clusterIter.second.mCommittedDataVersion.HasValue())
457 : {
458 65 : continue;
459 : }
460 567 : DataVersion dataVersion = clusterIter.second.mCommittedDataVersion.Value();
461 567 : size_t clusterSize = 0;
462 567 : ClusterId clusterId = clusterIter.first;
463 :
464 1263 : for (auto const & attributeIter : clusterIter.second.mAttributes)
465 : {
466 696 : if (attributeIter.second.Is<StatusIB>())
467 : {
468 33 : clusterSize += SizeOfStatusIB(attributeIter.second.Get<StatusIB>());
469 : }
470 663 : else if (attributeIter.second.Is<size_t>())
471 : {
472 0 : clusterSize += attributeIter.second.Get<size_t>();
473 : }
474 : else
475 : {
476 663 : VerifyOrDie(attributeIter.second.Is<AttributeData>());
477 : TLV::TLVReader bufReader;
478 663 : bufReader.Init(attributeIter.second.Get<AttributeData>().Get(),
479 663 : attributeIter.second.Get<AttributeData>().AllocatedSize());
480 663 : ReturnOnFailure(bufReader.Next());
481 : // Skip to the end of the element.
482 663 : ReturnOnFailure(bufReader.Skip());
483 :
484 : // Compute the amount of value data
485 663 : clusterSize += bufReader.GetLengthRead();
486 : }
487 : }
488 :
489 567 : if (clusterSize == 0)
490 : {
491 : // No data in this cluster, so no point in sending a dataVersion
492 : // along at all.
493 0 : continue;
494 : }
495 :
496 567 : DataVersionFilter filter(endpointId, clusterId, dataVersion);
497 :
498 567 : aVector.push_back(std::make_pair(filter, clusterSize));
499 567 : }
500 : }
501 :
502 545 : std::sort(aVector.begin(), aVector.end(),
503 156 : [](const std::pair<DataVersionFilter, size_t> & x, const std::pair<DataVersionFilter, size_t> & y) {
504 156 : return x.second > y.second;
505 : });
506 : }
507 :
508 545 : CHIP_ERROR ClusterStateCache::OnUpdateDataVersionFilterList(DataVersionFilterIBs::Builder & aDataVersionFilterIBsBuilder,
509 : const Span<AttributePathParams> & aAttributePaths,
510 : bool & aEncodedDataVersionList)
511 : {
512 545 : CHIP_ERROR err = CHIP_NO_ERROR;
513 545 : TLV::TLVWriter backup;
514 :
515 : // Only put paths into mRequestPathSet if they cover clusters in their entirety and no other path in our path list
516 : // points to a specific attribute from any of those clusters.
517 : // this would help for data-out-of-sync issue when handling store data version for the particular case on two paths: (E1, C1,
518 : // wildcard), (wildcard, C1, A1)
519 1111 : for (auto & attribute1 : aAttributePaths)
520 : {
521 566 : if (attribute1.HasWildcardAttributeId())
522 : {
523 544 : bool intersected = false;
524 1104 : for (auto & attribute2 : aAttributePaths)
525 : {
526 561 : if (attribute2.HasWildcardAttributeId())
527 : {
528 555 : continue;
529 : }
530 :
531 6 : if (attribute1.Intersects(attribute2))
532 : {
533 1 : intersected = true;
534 1 : break;
535 : }
536 : }
537 :
538 544 : if (!intersected)
539 : {
540 543 : mRequestPathSet.insert(attribute1);
541 : }
542 : }
543 : }
544 :
545 545 : std::vector<std::pair<DataVersionFilter, size_t>> filterVector;
546 545 : GetSortedFilters(filterVector);
547 :
548 545 : aEncodedDataVersionList = false;
549 939 : for (auto & filter : filterVector)
550 : {
551 549 : bool intersected = false;
552 549 : aDataVersionFilterIBsBuilder.Checkpoint(backup);
553 :
554 : // if the particular cached cluster does not intersect with user provided attribute paths, skip the cached one
555 556 : for (const auto & attributePath : aAttributePaths)
556 : {
557 553 : if (attributePath.IncludesAttributesInCluster(filter.first))
558 : {
559 546 : intersected = true;
560 546 : break;
561 : }
562 : }
563 549 : if (!intersected)
564 : {
565 3 : continue;
566 : }
567 :
568 546 : DataVersionFilterIB::Builder & filterIB = aDataVersionFilterIBsBuilder.CreateDataVersionFilter();
569 683 : SuccessOrExit(err = aDataVersionFilterIBsBuilder.GetError());
570 528 : ClusterPathIB::Builder & filterPath = filterIB.CreatePath();
571 528 : SuccessOrExit(err = filterIB.GetError());
572 501 : SuccessOrExit(err = filterPath.Endpoint(filter.first.mEndpointId).Cluster(filter.first.mClusterId).EndOfClusterPathIB());
573 418 : SuccessOrExit(err = filterIB.DataVersion(filter.first.mDataVersion.Value()).EndOfDataVersionFilterIB());
574 391 : ChipLogProgress(DataManagement, "Update DataVersionFilter: Endpoint=%u Cluster=" ChipLogFormatMEI " Version=%" PRIu32,
575 : filter.first.mEndpointId, ChipLogValueMEI(filter.first.mClusterId), filter.first.mDataVersion.Value());
576 :
577 391 : aEncodedDataVersionList = true;
578 : }
579 :
580 545 : exit:
581 545 : if (err == CHIP_ERROR_NO_MEMORY || err == CHIP_ERROR_BUFFER_TOO_SMALL)
582 : {
583 155 : ChipLogProgress(DataManagement, "OnUpdateDataVersionFilterList out of space; rolling back");
584 155 : aDataVersionFilterIBsBuilder.Rollback(backup);
585 155 : err = CHIP_NO_ERROR;
586 : }
587 545 : return err;
588 545 : }
589 :
590 0 : CHIP_ERROR ClusterStateCache::GetLastReportDataPath(ConcreteClusterPath & aPath)
591 : {
592 0 : if (mLastReportDataPath.IsValidConcreteClusterPath())
593 : {
594 0 : aPath = mLastReportDataPath;
595 0 : return CHIP_NO_ERROR;
596 : }
597 0 : return CHIP_ERROR_INCORRECT_STATE;
598 : }
599 : } // namespace app
600 : } // namespace chip
|