diff --git a/src/app/InteractionModelEngine.cpp b/src/app/InteractionModelEngine.cpp index 48ff3ec3fbaa58..34e2c2055e36f2 100644 --- a/src/app/InteractionModelEngine.cpp +++ b/src/app/InteractionModelEngine.cpp @@ -995,6 +995,22 @@ void InteractionModelEngine::OnActiveModeNotification(ScopedNodeId aPeer) } } +void InteractionModelEngine::OnPeerTypeChange(ScopedNodeId aPeer, ReadClient::PeerType aType) +{ + // TODO: Follow up to use a iterator function to avoid copy/paste here. + for (ReadClient * pListItem = mpActiveReadClientList; pListItem != nullptr;) + { + // It is possible that pListItem is destroyed by the app in OnPeerTypeChange. + // Get the next item before invoking `OnPeerTypeChange`. + auto pNextItem = pListItem->GetNextClient(); + if (ScopedNodeId(pListItem->GetPeerNodeId(), pListItem->GetFabricIndex()) == aPeer) + { + pListItem->OnPeerTypeChange(aType); + } + pListItem = pNextItem; + } +} + void InteractionModelEngine::AddReadClient(ReadClient * apReadClient) { apReadClient->SetNextClient(mpActiveReadClientList); diff --git a/src/app/InteractionModelEngine.h b/src/app/InteractionModelEngine.h index e850b493cc3bfb..5f130d819f008a 100644 --- a/src/app/InteractionModelEngine.h +++ b/src/app/InteractionModelEngine.h @@ -247,6 +247,14 @@ class InteractionModelEngine : public Messaging::UnsolicitedMessageHandler, */ void OnActiveModeNotification(ScopedNodeId aPeer); + /** + * Used to notify when a peer becomes LIT ICD or vice versa. + * + * ReadClient will call this function when it finds any updates of the OperatingMode attribute from ICD management + * cluster. The application doesn't need to call this function, usually. + */ + void OnPeerTypeChange(ScopedNodeId aPeer, ReadClient::PeerType aType); + /** * Add a read client to the internally tracked list of weak references. This list is used to * correctly dispatch unsolicited reports to the right matching handler by subscription ID. diff --git a/src/app/ReadClient.cpp b/src/app/ReadClient.cpp index a25359040e4fde..77a8e4c826ad6b 100644 --- a/src/app/ReadClient.cpp +++ b/src/app/ReadClient.cpp @@ -34,6 +34,10 @@ #include #include +#include +#include +#include + namespace chip { namespace app { @@ -451,12 +455,27 @@ void ReadClient::OnActiveModeNotification() TriggerResubscriptionForLivenessTimeout(CHIP_ERROR_TIMEOUT); } +void ReadClient::OnPeerTypeChange(PeerType aType) +{ + VerifyOrDie(mpImEngine->InActiveReadClientList(this)); + + mIsPeerLIT = (aType == PeerType::kLITICD); + + ChipLogProgress(DataManagement, "Peer is now %s LIT ICD.", mIsPeerLIT ? "a" : "not a"); + + // If the peer is no longer LIT, try to wake up the subscription and do resubscribe when necessary. + if (!mIsPeerLIT) + { + OnActiveModeNotification(); + } +} + CHIP_ERROR ReadClient::OnMessageReceived(Messaging::ExchangeContext * apExchangeContext, const PayloadHeader & aPayloadHeader, System::PacketBufferHandle && aPayload) { CHIP_ERROR err = CHIP_NO_ERROR; Status status = Status::InvalidAction; - VerifyOrExit(!IsIdle(), err = CHIP_ERROR_INCORRECT_STATE); + VerifyOrExit(!IsIdle() && !IsInactiveICDSubscription(), err = CHIP_ERROR_INCORRECT_STATE); if (aPayloadHeader.HasMessageType(Protocols::InteractionModel::MsgType::ReportData)) { @@ -663,6 +682,29 @@ void ReadClient::OnResponseTimeout(Messaging::ExchangeContext * apExchangeContex Close(CHIP_ERROR_TIMEOUT); } +CHIP_ERROR ReadClient::ReadICDOperatingModeFromAttributeDataIB(TLV::TLVReader && aReader, PeerType & aType) +{ + Clusters::IcdManagement::Attributes::OperatingMode::TypeInfo::DecodableType operatingMode; + + CHIP_ERROR err = DataModel::Decode(aReader, operatingMode); + ReturnErrorOnFailure(err); + + switch (operatingMode) + { + case Clusters::IcdManagement::OperatingModeEnum::kSit: + aType = PeerType::kNormal; + break; + case Clusters::IcdManagement::OperatingModeEnum::kLit: + aType = PeerType::kLITICD; + break; + default: + err = CHIP_ERROR_INVALID_ARGUMENT; + break; + } + + return err; +} + CHIP_ERROR ReadClient::ProcessAttributePath(AttributePathIB::Parser & aAttributePathParser, ConcreteDataAttributePath & aAttributePath) { @@ -757,6 +799,26 @@ CHIP_ERROR ReadClient::ProcessAttributeReportIBs(TLV::TLVReader & aAttributeRepo attributePath.mListOp = ConcreteDataAttributePath::ListOperation::ReplaceAll; } + if (attributePath == + ConcreteDataAttributePath(kRootEndpointId, Clusters::IcdManagement::Id, + Clusters::IcdManagement::Attributes::OperatingMode::Id)) + { + PeerType peerType; + TLV::TLVReader operatingModeTlvReader; + operatingModeTlvReader.Init(dataReader); + if (CHIP_NO_ERROR == ReadICDOperatingModeFromAttributeDataIB(std::move(operatingModeTlvReader), peerType)) + { + // It is safe to call `OnPeerTypeChange` since we are in the middle of parsing the attribute data, And + // the subscription should be active so `OnActiveModeNotification` is a no-op in this case. + InteractionModelEngine::GetInstance()->OnPeerTypeChange(mPeer, peerType); + } + else + { + ChipLogError(DataManagement, "Failed to get ICD state from attribute data with error'%" CHIP_ERROR_FORMAT "'", + err.Format()); + } + } + NoteReportingData(); mpCallback.OnAttributeData(attributePath, &dataReader, statusIB); } diff --git a/src/app/ReadClient.h b/src/app/ReadClient.h index 46e895f6c16783..d3fb370febe25c 100644 --- a/src/app/ReadClient.h +++ b/src/app/ReadClient.h @@ -292,6 +292,12 @@ class ReadClient : public Messaging::ExchangeDelegate Subscribe, }; + enum class PeerType : uint8_t + { + kNormal, + kLITICD, + }; + /** * * Constructor. @@ -524,6 +530,15 @@ class ReadClient : public Messaging::ExchangeDelegate System::PacketBufferHandle && aPayload) override; void OnResponseTimeout(Messaging::ExchangeContext * apExchangeContext) override; + /** + * Updates the type (LIT ICD or not) of the peer. + * + * When the subscription is active, this function will just set the flag. When the subscription is an InactiveICDSubscription, + * setting the peer type to SIT or normal devices will also trigger a resubscription attempt. + * + */ + void OnPeerTypeChange(PeerType aType); + /** * Check if current read client is being used * @@ -544,6 +559,7 @@ class ReadClient : public Messaging::ExchangeDelegate CHIP_ERROR BuildDataVersionFilterList(DataVersionFilterIBs::Builder & aDataVersionFilterIBsBuilder, const Span & aAttributePaths, const Span & aDataVersionFilters, bool & aEncodedDataVersionList); + CHIP_ERROR ReadICDOperatingModeFromAttributeDataIB(TLV::TLVReader && aReader, PeerType & aType); CHIP_ERROR ProcessAttributeReportIBs(TLV::TLVReader & aAttributeDataIBsReader); CHIP_ERROR ProcessEventReportIBs(TLV::TLVReader & aEventReportIBsReader); diff --git a/src/controller/tests/data_model/TestRead.cpp b/src/controller/tests/data_model/TestRead.cpp index fcaaa308851fd6..917c3a1251e344 100644 --- a/src/controller/tests/data_model/TestRead.cpp +++ b/src/controller/tests/data_model/TestRead.cpp @@ -81,6 +81,8 @@ ResponseDirective responseDirective; // Every read will increment this count by 1 and return the new value. uint16_t totalReadCount = 0; +bool isLitIcd = false; + } // namespace namespace chip { @@ -169,6 +171,17 @@ CHIP_ERROR ReadSingleClusterData(const Access::SubjectDescriptor & aSubjectDescr return err; } } + if (aPath.mClusterId == app::Clusters::IcdManagement::Id && + aPath.mAttributeId == app::Clusters::IcdManagement::Attributes::OperatingMode::Id) + { + AttributeValueEncoder::AttributeEncodeState state = + (apEncoderState == nullptr ? AttributeValueEncoder::AttributeEncodeState() : *apEncoderState); + AttributeValueEncoder valueEncoder(aAttributeReports, aSubjectDescriptor.fabricIndex, aPath, + kDataVersion /* data version */, aIsFabricFiltered, state); + + return valueEncoder.Encode(isLitIcd ? Clusters::IcdManagement::OperatingModeEnum::kLit + : Clusters::IcdManagement::OperatingModeEnum::kSit); + } AttributeReportIB::Builder & attributeReport = aAttributeReports.CreateAttributeReport(); ReturnErrorOnFailure(aAttributeReports.GetError()); @@ -298,6 +311,7 @@ class TestReadInteraction : public app::ReadHandler::ApplicationCallback static void TestReadHandler_KeepSubscriptionTest(nlTestSuite * apSuite, void * apContext); static void TestSubscribe_OnActiveModeNotification(nlTestSuite * apSuite, void * apContext); static void TestSubscribe_ImmediatelyResubscriptionForLIT(nlTestSuite * apSuite, void * apContext); + static void TestSubscribe_DynamicLITSubscription(nlTestSuite * apSuite, void * apContext); private: static uint16_t mMaxInterval; @@ -2722,6 +2736,123 @@ void TestReadInteraction::TestSubscribe_OnActiveModeNotification(nlTestSuite * a NL_TEST_ASSERT(apSuite, ctx.GetExchangeManager().GetNumActiveExchanges() == 0); } +/** + * When the liveness timeout of a subscription to ICD is reached, the subscription will enter "InactiveICDSubscription" state, the + * client should call "OnActiveModeNotification" to re-activate it again when the check-in message is received from the ICD. + */ +void TestReadInteraction::TestSubscribe_DynamicLITSubscription(nlTestSuite * apSuite, void * apContext) +{ + TestContext & ctx = *static_cast(apContext); + auto sessionHandle = ctx.GetSessionBobToAlice(); + + ctx.SetMRPMode(chip::Test::MessagingContext::MRPMode::kResponsive); + + { + TestResubscriptionCallback callback; + app::ReadClient readClient(app::InteractionModelEngine::GetInstance(), &ctx.GetExchangeManager(), callback, + app::ReadClient::InteractionType::Subscribe); + + responseDirective = kSendDataResponse; + callback.mScheduleLITResubscribeImmediately = false; + callback.SetReadClient(&readClient); + isLitIcd = false; + + app::ReadPrepareParams readPrepareParams(ctx.GetSessionBobToAlice()); + + // Read full wildcard paths, repeat twice to ensure chunking. + app::AttributePathParams attributePathParams[1]; + readPrepareParams.mpAttributePathParamsList = attributePathParams; + readPrepareParams.mAttributePathParamsListSize = ArraySize(attributePathParams); + attributePathParams[0].mEndpointId = kRootEndpointId; + attributePathParams[0].mClusterId = app::Clusters::IcdManagement::Id; + attributePathParams[0].mAttributeId = app::Clusters::IcdManagement::Attributes::OperatingMode::Id; + + constexpr uint16_t maxIntervalCeilingSeconds = 1; + + readPrepareParams.mMaxIntervalCeilingSeconds = maxIntervalCeilingSeconds; + readPrepareParams.mIsPeerLIT = true; + + auto err = readClient.SendAutoResubscribeRequest(std::move(readPrepareParams)); + NL_TEST_ASSERT(apSuite, err == CHIP_NO_ERROR); + + // + // Drive servicing IO till we have established a subscription. + // + ctx.GetIOContext().DriveIOUntil(System::Clock::Milliseconds32(2000), + [&]() { return callback.mOnSubscriptionEstablishedCount >= 1; }); + NL_TEST_ASSERT(apSuite, callback.mOnSubscriptionEstablishedCount == 1); + NL_TEST_ASSERT(apSuite, callback.mOnError == 0); + NL_TEST_ASSERT(apSuite, callback.mOnResubscriptionsAttempted == 0); + chip::app::ReadHandler * readHandler = app::InteractionModelEngine::GetInstance()->ActiveHandlerAt(0); + + uint16_t minInterval; + uint16_t maxInterval; + readHandler->GetReportingIntervals(minInterval, maxInterval); + + // Part 1. LIT -> SIT + + // + // Disable packet transmission, and drive IO till timeout. + // We won't actually request resubscription, since the device is not active, the resubscription will be deferred until + // WakeUp() is called. + // + // Even if we set the peer type to LIT before, the report indicates that the peer is a SIT now, it will just bahve as + // normal, non-LIT subscriptions. + ctx.GetLoopback().mNumMessagesToDrop = chip::Test::LoopbackTransport::kUnlimitedMessageCount; + ctx.GetIOContext().DriveIOUntil(ComputeSubscriptionTimeout(System::Clock::Seconds16(maxInterval)), + [&]() { return callback.mOnResubscriptionsAttempted != 0; }); + NL_TEST_ASSERT(apSuite, callback.mOnResubscriptionsAttempted == 1); + NL_TEST_ASSERT(apSuite, callback.mLastError == CHIP_ERROR_TIMEOUT); + + ctx.GetLoopback().mNumMessagesToDrop = 0; + callback.ClearCounters(); + + // + // Drive servicing IO till we have established a subscription. + // + ctx.GetIOContext().DriveIOUntil(System::Clock::Milliseconds32(2000), + [&]() { return callback.mOnSubscriptionEstablishedCount == 1; }); + NL_TEST_ASSERT(apSuite, callback.mOnSubscriptionEstablishedCount == 1); + + // + // With re-sub enabled, we shouldn't have encountered any errors + // + NL_TEST_ASSERT(apSuite, callback.mOnError == 0); + NL_TEST_ASSERT(apSuite, callback.mOnDone == 0); + + // Part 2. SIT -> LIT + + isLitIcd = true; + { + app::AttributePathParams path; + path.mEndpointId = kRootEndpointId; + path.mClusterId = Clusters::IcdManagement::Id; + path.mAttributeId = Clusters::IcdManagement::Attributes::OperatingMode::Id; + app::InteractionModelEngine::GetInstance()->GetReportingEngine().SetDirty(path); + } + callback.ClearCounters(); + ctx.GetIOContext().DriveIOUntil(System::Clock::Seconds16(60), [&]() { + return app::InteractionModelEngine::GetInstance()->GetNumDirtySubscriptions() == 0; + }); + + // When we received the update that OperatingMode becomes LIT, we automatically set the inner peer type to LIT ICD. + ctx.GetLoopback().mNumMessagesToDrop = chip::Test::LoopbackTransport::kUnlimitedMessageCount; + ctx.GetIOContext().DriveIOUntil(ComputeSubscriptionTimeout(System::Clock::Seconds16(maxInterval)), [&]() { return false; }); + NL_TEST_ASSERT(apSuite, callback.mOnResubscriptionsAttempted == 1); + NL_TEST_ASSERT(apSuite, callback.mLastError == CHIP_ERROR_LIT_SUBSCRIBE_INACTIVE_TIMEOUT); + + ctx.GetLoopback().mNumMessagesToDrop = 0; + callback.ClearCounters(); + } + + ctx.SetMRPMode(chip::Test::MessagingContext::MRPMode::kDefault); + + app::InteractionModelEngine::GetInstance()->ShutdownActiveReads(); + NL_TEST_ASSERT(apSuite, ctx.GetExchangeManager().GetNumActiveExchanges() == 0); + + isLitIcd = false; +} + /** * When the liveness timeout of a subscription to ICD is reached, the app can issue resubscription immediately * if they know the peer is active. @@ -4920,6 +5051,7 @@ const nlTest sTests[] = NL_TEST_DEF("TestReadHandler_KeepSubscriptionTest", TestReadInteraction::TestReadHandler_KeepSubscriptionTest), NL_TEST_DEF("TestSubscribe_OnActiveModeNotification", TestReadInteraction::TestSubscribe_OnActiveModeNotification), NL_TEST_DEF("TestSubscribe_ImmediatelyResubscriptionForLIT", TestReadInteraction::TestSubscribe_ImmediatelyResubscriptionForLIT), + NL_TEST_DEF("TestSubscribe_DynamicLITSubscription", TestReadInteraction::TestSubscribe_DynamicLITSubscription), NL_TEST_SENTINEL() }; // clang-format on