diff --git a/examples/fabric-admin/BUILD.gn b/examples/fabric-admin/BUILD.gn index 7fbd13e07183ed..77e851a53c36d2 100644 --- a/examples/fabric-admin/BUILD.gn +++ b/examples/fabric-admin/BUILD.gn @@ -69,8 +69,12 @@ static_library("fabric-admin-utils") { "commands/common/Commands.h", "commands/common/CredentialIssuerCommands.h", "commands/common/HexConversion.h", + "commands/common/IcdManager.cpp", + "commands/common/IcdManager.h", "commands/common/RemoteDataModelLogger.cpp", "commands/common/RemoteDataModelLogger.h", + "commands/common/StayActiveSender.cpp", + "commands/common/StayActiveSender.h", "commands/fabric-sync/FabricSyncCommand.cpp", "commands/pairing/DeviceSynchronization.cpp", "commands/pairing/DeviceSynchronization.h", diff --git a/examples/fabric-admin/commands/common/CHIPCommand.cpp b/examples/fabric-admin/commands/common/CHIPCommand.cpp index 982c857d206136..0c5455439b22d9 100644 --- a/examples/fabric-admin/commands/common/CHIPCommand.cpp +++ b/examples/fabric-admin/commands/common/CHIPCommand.cpp @@ -18,6 +18,7 @@ #include "CHIPCommand.h" +#include "IcdManager.h" #include #include #include @@ -52,7 +53,6 @@ chip::Credentials::GroupDataProviderImpl CHIPCommand::sGroupDataProvider{ kMaxGr // All fabrics share the same ICD client storage. chip::app::DefaultICDClientStorage CHIPCommand::sICDClientStorage; chip::Crypto::RawKeySessionKeystore CHIPCommand::sSessionKeystore; -chip::app::DefaultCheckInDelegate CHIPCommand::sCheckInDelegate; chip::app::CheckInHandler CHIPCommand::sCheckInHandler; namespace { @@ -148,9 +148,9 @@ CHIP_ERROR CHIPCommand::MaybeSetUpStack() auto engine = chip::app::InteractionModelEngine::GetInstance(); VerifyOrReturnError(engine != nullptr, CHIP_ERROR_INCORRECT_STATE); - ReturnLogErrorOnFailure(sCheckInDelegate.Init(&sICDClientStorage, engine)); + ReturnLogErrorOnFailure(IcdManager::Instance().Init(&sICDClientStorage, engine)); ReturnLogErrorOnFailure(sCheckInHandler.Init(DeviceControllerFactory::GetInstance().GetSystemState()->ExchangeMgr(), - &sICDClientStorage, &sCheckInDelegate, engine)); + &sICDClientStorage, &IcdManager::Instance(), engine)); CommissionerIdentity nullIdentity{ kIdentityNull, chip::kUndefinedNodeId }; ReturnLogErrorOnFailure(InitializeCommissioner(nullIdentity, kIdentityNullFabricId)); diff --git a/examples/fabric-admin/commands/common/CHIPCommand.h b/examples/fabric-admin/commands/common/CHIPCommand.h index 6711b3e4f9aa3d..abd68f6344ea44 100644 --- a/examples/fabric-admin/commands/common/CHIPCommand.h +++ b/examples/fabric-admin/commands/common/CHIPCommand.h @@ -168,7 +168,6 @@ class CHIPCommand : public Command static chip::Credentials::GroupDataProviderImpl sGroupDataProvider; static chip::app::DefaultICDClientStorage sICDClientStorage; - static chip::app::DefaultCheckInDelegate sCheckInDelegate; static chip::app::CheckInHandler sCheckInHandler; CredentialIssuerCommands * mCredIssuerCmds; diff --git a/examples/fabric-admin/commands/common/IcdManager.cpp b/examples/fabric-admin/commands/common/IcdManager.cpp new file mode 100644 index 00000000000000..201d7325541982 --- /dev/null +++ b/examples/fabric-admin/commands/common/IcdManager.cpp @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2024 Project CHIP Authors + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +#include "IcdManager.h" + +IcdManager IcdManager::sInstance; + +IcdManager & IcdManager::Instance() +{ + return sInstance; +} + +void IcdManager::OnCheckInComplete(const chip::app::ICDClientInfo & clientInfo) +{ + DefaultCheckInDelegate::OnCheckInComplete(clientInfo); + if (mDelegate) + { + mDelegate->OnCheckInCompleted(clientInfo); + } +} + +void IcdManager::SetDelegate(Delegate * delegate) +{ + // To keep IcdManager simple, there is an assumption that there is only ever + // one delegate set and it's lifetime is identical to IcdManager. In the + // future this assumption can change should there be a need, but that will + // require code changes to IcdManager. For now we will crash if someone tries + // to call SetDelegate for a second time or if delegate is non-null. + VerifyOrDie(delegate); + VerifyOrDie(!mDelegate); + mDelegate = delegate; +} diff --git a/examples/fabric-admin/commands/common/IcdManager.h b/examples/fabric-admin/commands/common/IcdManager.h new file mode 100644 index 00000000000000..8993f24e531e83 --- /dev/null +++ b/examples/fabric-admin/commands/common/IcdManager.h @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2024 Project CHIP Authors + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +#pragma once + +#include + +/** + * @brief Manages check-ins from ICD devices. + * + * Intended to be used as a thin CheckInDelegate. This allows a delegate register + * themselves so they can be aware when ICD device checks-in allowing the + * delegate to interact with the ICD device during the short window that it is + * awake. + */ +class IcdManager : public chip::app::DefaultCheckInDelegate +{ +public: + class Delegate + { + public: + virtual ~Delegate() = default; + virtual void OnCheckInCompleted(const chip::app::ICDClientInfo & clientInfo) = 0; + }; + + static IcdManager & Instance(); + void OnCheckInComplete(const chip::app::ICDClientInfo & clientInfo) override; + + // There is an assumption delegate assigned only happens once and that it lives + // for the entirety of the lifetime of fabric admin. + void SetDelegate(Delegate * delegate); + +private: + static IcdManager sInstance; + Delegate * mDelegate = nullptr; +}; diff --git a/examples/fabric-admin/commands/common/StayActiveSender.cpp b/examples/fabric-admin/commands/common/StayActiveSender.cpp new file mode 100644 index 00000000000000..16f1d5cd132f81 --- /dev/null +++ b/examples/fabric-admin/commands/common/StayActiveSender.cpp @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2024 Project CHIP Authors + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "StayActiveSender.h" + +#include +#include +#include +#include + +CHIP_ERROR StayActiveSender::SendStayActiveCommand(uint32_t stayActiveDurationMs, const chip::ScopedNodeId & peerNode, + chip::app::InteractionModelEngine * engine, OnDoneCallbackType onDone) +{ + ConstructorOnlyInternallyCallable internal; + auto stayActiveSender = chip::Platform::New(internal, stayActiveDurationMs, peerNode, + chip::app::InteractionModelEngine::GetInstance(), onDone); + VerifyOrReturnError(stayActiveSender != nullptr, CHIP_ERROR_NO_MEMORY); + CHIP_ERROR err = stayActiveSender->EstablishSessionToPeer(); + if (CHIP_NO_ERROR != err) + { + chip::Platform::Delete(stayActiveSender); + } + return err; +} + +StayActiveSender::StayActiveSender(const ConstructorOnlyInternallyCallable & _, uint32_t stayActiveDurationMs, + const chip::ScopedNodeId & peerNode, chip::app::InteractionModelEngine * engine, + OnDoneCallbackType onDone) : + mStayActiveDurationMs(stayActiveDurationMs), + mPeerNode(peerNode), mpImEngine(engine), mOnDone(onDone), mOnConnectedCallback(HandleDeviceConnected, this), + mOnConnectionFailureCallback(HandleDeviceConnectionFailure, this) +{} + +CHIP_ERROR StayActiveSender::SendStayActiveCommand(chip::Messaging::ExchangeManager & exchangeMgr, + const chip::SessionHandle & sessionHandle) +{ + auto onSuccess = [&](const chip::app::ConcreteCommandPath & commandPath, const chip::app::StatusIB & status, + const auto & dataResponse) { + uint32_t promisedActiveDurationMs = dataResponse.promisedActiveDuration; + ChipLogProgress(ICD, "StayActive command succeeded with promised duration %u", promisedActiveDurationMs); + mOnDone(promisedActiveDurationMs); + chip::Platform::Delete(this); + }; + + auto onFailure = [&](CHIP_ERROR error) { + ChipLogError(ICD, "StayActive command failed: %" CHIP_ERROR_FORMAT, error.Format()); + chip::Platform::Delete(this); + }; + + chip::EndpointId endpointId = 0; + chip::app::Clusters::IcdManagement::Commands::StayActiveRequest::Type request; + request.stayActiveDuration = mStayActiveDurationMs; + return chip::Controller::InvokeCommandRequest(&exchangeMgr, sessionHandle, endpointId, request, onSuccess, onFailure); +} + +CHIP_ERROR StayActiveSender::EstablishSessionToPeer() +{ + ChipLogProgress(ICD, "Trying to establish a CASE session to extend the active period for lit icd device"); + auto * caseSessionManager = mpImEngine->GetCASESessionManager(); + VerifyOrReturnError(caseSessionManager != nullptr, CHIP_ERROR_INVALID_CASE_PARAMETER); + caseSessionManager->FindOrEstablishSession(mPeerNode, &mOnConnectedCallback, &mOnConnectionFailureCallback); + return CHIP_NO_ERROR; +} + +void StayActiveSender::HandleDeviceConnected(void * context, chip::Messaging::ExchangeManager & exchangeMgr, + const chip::SessionHandle & sessionHandle) +{ + StayActiveSender * const _this = static_cast(context); + VerifyOrDie(_this != nullptr); + + CHIP_ERROR err = _this->SendStayActiveCommand(exchangeMgr, sessionHandle); + if (CHIP_NO_ERROR != err) + { + ChipLogError(ICD, "Failed to send stay active command"); + chip::Platform::Delete(_this); + } +} + +void StayActiveSender::HandleDeviceConnectionFailure(void * context, const chip::ScopedNodeId & peerId, CHIP_ERROR err) +{ + StayActiveSender * const _this = static_cast(context); + VerifyOrDie(_this != nullptr); + ChipLogError(ICD, "Failed to establish CASE for stay active command with error '%" CHIP_ERROR_FORMAT "'", err.Format()); + chip::Platform::Delete(_this); +} diff --git a/examples/fabric-admin/commands/common/StayActiveSender.h b/examples/fabric-admin/commands/common/StayActiveSender.h new file mode 100644 index 00000000000000..c0caa380e6f912 --- /dev/null +++ b/examples/fabric-admin/commands/common/StayActiveSender.h @@ -0,0 +1,107 @@ +/* + * + * Copyright (c) 2024 Project CHIP Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +#include +#include +#include + +/** + * @brief StayActiveSender contains all the data and methods needed for active period extension of an ICD client. + * + * Lifetime of instance of StayActiveSender is entirely self managed. + */ +class StayActiveSender +{ +private: + // Ideally StayActiveSender would be a private constructor, unfortunately that is not possible as Platform::New + // does not have access to private constructors. As a workaround we have defined this private struct that can + // be forwarded by Platform::New that allows us to enforce that the only way StayActiveSender is constructed is + // if SendStayActiveCommand is called. + struct ConstructorOnlyInternallyCallable + { + }; + +public: + using OnDoneCallbackType = std::function; + + /** + * @brief Attempts to send a StayActiveRequest command + * + * @param[in] stayActiveDurationMs StayActiveRequest command parameter. + * @param[in] peerNode Peer node we sending StayActiveRequest command to + * @param[in] engine Interaction Model Engine instance for sending command. + * @param[in] onDone Upon this function returning success, it is expected that onDone will be called after we + * have successfully recieved a response + * + * @return CHIP_ERROR CHIP_NO_ERROR on success, or corresponding error code. + */ + static CHIP_ERROR SendStayActiveCommand(uint32_t stayActiveDurationMs, const chip::ScopedNodeId & peerNode, + chip::app::InteractionModelEngine * engine, OnDoneCallbackType onDone); + + // Ideally this would be a private constructor, unfortunately that is not possible as Platform::New does not + // have access to private constructors. As a workaround we have defined a private struct that can be forwarded + // by Platform::New that allows us to enforce that the only way this is constructed is if SendStayActiveCommand + // is called. + StayActiveSender(const ConstructorOnlyInternallyCallable & _, uint32_t stayActiveDurationMs, + const chip::ScopedNodeId & peerNode, chip::app::InteractionModelEngine * engine, OnDoneCallbackType onDone); + +private: + /** + * @brief Sets up a CASE session with the peer to extend the client's active period with that peer. + * Returns error if we did not even manage to kick off a CASE attempt. + */ + CHIP_ERROR EstablishSessionToPeer(); + + // CASE session callbacks + /** + *@brief Callback received on successfully establishing a CASE session in order to keep the 'lit icd device' active + * + * @param[in] context - context of the client establishing the CASE session + * @param[in] exchangeMgr - exchange manager to use for the re-registration + * @param[in] sessionHandle - session handle to use for the re-registration + */ + static void HandleDeviceConnected(void * context, chip::Messaging::ExchangeManager & exchangeMgr, + const chip::SessionHandle & sessionHandle); + /** + * @brief Callback received on failure to establish a CASE session + * + * @param[in] context - context of the client establishing the CASE session + * @param[in] peerId - Scoped Node ID of the peer node + * @param[in] err - failure reason + */ + static void HandleDeviceConnectionFailure(void * context, const chip::ScopedNodeId & peerId, CHIP_ERROR err); + + /** + * @brief Used to send a stayActive command to the peer + * + * @param[in] exchangeMgr - exchange manager to use for the re-registration + * @param[in] sessionHandle - session handle to use for the re-registration + */ + CHIP_ERROR SendStayActiveCommand(chip::Messaging::ExchangeManager & exchangeMgr, const chip::SessionHandle & sessionHandle); + + uint32_t mStayActiveDurationMs = 0; + chip::ScopedNodeId mPeerNode; + chip::app::InteractionModelEngine * mpImEngine = nullptr; + OnDoneCallbackType mOnDone; + + chip::Callback::Callback mOnConnectedCallback; + chip::Callback::Callback mOnConnectionFailureCallback; +}; diff --git a/examples/fabric-admin/commands/pairing/PairingCommand.cpp b/examples/fabric-admin/commands/pairing/PairingCommand.cpp index 21b023f423ed84..aecd259d373d3f 100644 --- a/examples/fabric-admin/commands/pairing/PairingCommand.cpp +++ b/examples/fabric-admin/commands/pairing/PairingCommand.cpp @@ -464,7 +464,7 @@ void PairingCommand::OnICDRegistrationComplete(ScopedNodeId nodeId, uint32_t icd sizeof(icdSymmetricKeyHex), chip::Encoding::HexFlags::kNullTerminate); app::ICDClientInfo clientInfo; - clientInfo.peer_node = chip::ScopedNodeId(mICDCheckInNodeId.Value(), nodeId.GetFabricIndex()); + clientInfo.peer_node = nodeId; clientInfo.monitored_subject = mICDMonitoredSubject.Value(); clientInfo.start_icd_counter = icdCounter; diff --git a/examples/fabric-admin/rpc/RpcServer.cpp b/examples/fabric-admin/rpc/RpcServer.cpp index 16ff695e159712..86d90ad294beb7 100644 --- a/examples/fabric-admin/rpc/RpcServer.cpp +++ b/examples/fabric-admin/rpc/RpcServer.cpp @@ -20,11 +20,16 @@ #include "pw_rpc_system_server/rpc_server.h" #include "pw_rpc_system_server/socket.h" +#include +#include + +#include "RpcClient.h" +#include +#include #include #include #include #include -#include #if defined(PW_RPC_FABRIC_ADMIN_SERVICE) && PW_RPC_FABRIC_ADMIN_SERVICE #include "pigweed/rpc_services/FabricAdmin.h" @@ -35,9 +40,33 @@ using namespace ::chip; namespace { #if defined(PW_RPC_FABRIC_ADMIN_SERVICE) && PW_RPC_FABRIC_ADMIN_SERVICE -class FabricAdmin final : public rpc::FabricAdmin + +class FabricAdmin final : public rpc::FabricAdmin, public IcdManager::Delegate { public: + void OnCheckInCompleted(const chip::app::ICDClientInfo & clientInfo) override + { + chip::NodeId nodeId = clientInfo.peer_node.GetNodeId(); + auto it = mPendingKeepActiveTimesMs.find(nodeId); + VerifyOrReturn(it != mPendingKeepActiveTimesMs.end()); + // TODO(#33221): We also need a mechanism here to drop KeepActive + // request if they were recieved over 60 mins ago. + uint32_t stayActiveDurationMs = it->second; + + // TODO(#33221): If there is a failure in sending the message this request just gets dropped. + // Work to see if there should be update to spec on whether some sort of failure later on + // Should be indicated in some manner, or identify a better recovery mechanism here. + mPendingKeepActiveTimesMs.erase(nodeId); + + auto onDone = [=](uint32_t promisedActiveDuration) { ActiveChanged(nodeId, promisedActiveDuration); }; + CHIP_ERROR err = StayActiveSender::SendStayActiveCommand(stayActiveDurationMs, clientInfo.peer_node, + chip::app::InteractionModelEngine::GetInstance(), onDone); + if (err != CHIP_NO_ERROR) + { + ChipLogError(NotSpecified, "Failed to send StayActive command %s", err.AsString()); + } + } + pw::Status OpenCommissioningWindow(const chip_rpc_DeviceCommissioningWindowInfo & request, chip_rpc_OperationStatus & response) override { @@ -65,14 +94,41 @@ class FabricAdmin final : public rpc::FabricAdmin pw::Status KeepActive(const chip_rpc_KeepActiveParameters & request, pw_protobuf_Empty & response) override { ChipLogProgress(NotSpecified, "Received KeepActive request: 0x%lx, %u", request.node_id, request.stay_active_duration_ms); - // TODO(#33221): When we get this command hopefully we are already registered with an ICD device to be - // notified when it wakes up. We will need to add in hooks there to make sure we send the StayActiveRequest - // Important thing to note: - // * If we get this call multiple times before we get a wakeup from ICD, we only send out one StayActiveRequest command - // * After 60 mins from last exipry we no longer will send out a StayActiveRequest. - + // TODO(#33221): We should really be using ScopedNode, but that requires larger fix in communication between + // fabric-admin and fabric-bridge. For now we make the assumption that there is only one fabric used by + // fabric-admin. + KeepActiveWorkData * data = chip::Platform::New(this, request.node_id, request.stay_active_duration_ms); + VerifyOrReturnValue(data, pw::Status::Internal()); + chip::DeviceLayer::PlatformMgr().ScheduleWork(KeepActiveWork, reinterpret_cast(data)); return pw::OkStatus(); } + + void ScheduleSendingKeepActiveOnCheckIn(chip::NodeId nodeId, uint32_t stayActiveDurationMs) + { + mPendingKeepActiveTimesMs[nodeId] = stayActiveDurationMs; + } + +private: + struct KeepActiveWorkData + { + KeepActiveWorkData(FabricAdmin * fabricAdmin, chip::NodeId nodeId, uint32_t stayActiveDurationMs) : + mFabricAdmin(fabricAdmin), mNodeId(nodeId), mStayActiveDurationMs(stayActiveDurationMs) + {} + + FabricAdmin * mFabricAdmin; + chip::NodeId mNodeId; + uint32_t mStayActiveDurationMs; + }; + + static void KeepActiveWork(intptr_t arg) + { + KeepActiveWorkData * data = reinterpret_cast(arg); + data->mFabricAdmin->ScheduleSendingKeepActiveOnCheckIn(data->mNodeId, data->mStayActiveDurationMs); + chip::Platform::Delete(data); + } + + // Modifications to mPendingKeepActiveTimesMs should be done on the MatterEventLoop thread + std::map mPendingKeepActiveTimesMs; }; FabricAdmin fabric_admin_service; @@ -82,6 +138,7 @@ void RegisterServices(pw::rpc::Server & server) { #if defined(PW_RPC_FABRIC_ADMIN_SERVICE) && PW_RPC_FABRIC_ADMIN_SERVICE server.RegisterService(fabric_admin_service); + IcdManager::Instance().SetDelegate(&fabric_admin_service); #endif }