diff --git a/src/app/chip_data_model.gni b/src/app/chip_data_model.gni index a68d193d241541..d1a8e43a2495ec 100644 --- a/src/app/chip_data_model.gni +++ b/src/app/chip_data_model.gni @@ -390,6 +390,12 @@ template("chip_data_model") { "${_app_root}/clusters/${cluster}/thread-network-diagnostics-provider.cpp", "${_app_root}/clusters/${cluster}/thread-network-diagnostics-provider.h", ] + } else if (cluster == "thread-border-router-management-server") { + sources += [ + "${_app_root}/clusters/${cluster}/thread-border-router-management-server.cpp", + "${_app_root}/clusters/${cluster}/thread-border-router-management-server.h", + "${_app_root}/clusters/${cluster}/thread-br-delegate.h", + ] } else { sources += [ "${_app_root}/clusters/${cluster}/${cluster}.cpp" ] } diff --git a/src/app/clusters/thread-border-router-management-server/thread-border-router-management-server.cpp b/src/app/clusters/thread-border-router-management-server/thread-border-router-management-server.cpp new file mode 100644 index 00000000000000..09f2ed510ae4fe --- /dev/null +++ b/src/app/clusters/thread-border-router-management-server/thread-border-router-management-server.cpp @@ -0,0 +1,356 @@ +/* + * + * 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. + */ + +#include "thread-border-router-management-server.h" + +#include "app-common/zap-generated/cluster-objects.h" +#include "app-common/zap-generated/ids/Attributes.h" +#include "app-common/zap-generated/ids/Clusters.h" +#include "app-common/zap-generated/ids/Commands.h" +#include "app/AttributeAccessInterfaceRegistry.h" +#include "app/AttributeValueEncoder.h" +#include "app/CommandHandler.h" +#include "app/CommandHandlerInterface.h" +#include "app/InteractionModelEngine.h" +#include "app/MessageDef/StatusIB.h" +#include "app/clusters/general-commissioning-server/general-commissioning-server.h" +#include "app/data-model/Nullable.h" +#include "lib/core/CHIPError.h" +#include "lib/core/Optional.h" +#include "lib/support/CodeUtils.h" +#include "lib/support/Span.h" +#include "lib/support/ThreadOperationalDataset.h" +#include "platform/CHIPDeviceEvent.h" +#include "platform/PlatformManager.h" +#include "protocols/interaction_model/StatusCode.h" + +namespace chip { +namespace app { +namespace Clusters { +namespace ThreadBorderRouterManagement { + +using Protocols::InteractionModel::Status; + +static bool IsCommandOverCASESession(CommandHandlerInterface::HandlerContext & ctx) +{ + Messaging::ExchangeContext * exchangeCtx = ctx.mCommandHandler.GetExchangeContext(); + return exchangeCtx && exchangeCtx->HasSessionHandle() && exchangeCtx->GetSessionHandle()->IsSecureSession() && + exchangeCtx->GetSessionHandle()->AsSecureSession()->GetSecureSessionType() == Transport::SecureSession::Type::kCASE; +} + +Status ServerInstance::HandleGetDatasetRequest(bool isOverCASESession, Delegate::DatasetType type, + Thread::OperationalDataset & dataset) +{ + VerifyOrDie(mDelegate); + if (!isOverCASESession) + { + return Status::UnsupportedAccess; + } + + CHIP_ERROR err = mDelegate->GetDataset(dataset, type); + if (err != CHIP_NO_ERROR) + { + return err == CHIP_IM_GLOBAL_STATUS(NotFound) ? StatusIB(err).mStatus : Status::Failure; + } + return Status::Success; +} + +Status ServerInstance::HandleSetActiveDatasetRequest(CommandHandler * commandHandler, + const Commands::SetActiveDatasetRequest::DecodableType & req) +{ + // The SetActiveDatasetRequest command SHALL be FailSafeArmed. Upon receiving this command, the Thread BR will set its + // active dataset. If the dataset is set successfully, OnActivateDatasetComplete will be called with CHIP_NO_ERROR, prompting + // the Thread BR to respond with a success status. If an error occurs while setting the active dataset, the Thread BR should + // respond with a failure status. In this case, when the FailSafe timer expires, the active dataset set by this command will be + // reverted. If the FailSafe timer expires before the Thread BR responds, the Thread BR will respond with a timeout status and + // the active dataset should also be reverted. + VerifyOrDie(mDelegate); + VerifyOrReturnValue(mFailsafeContext.IsFailSafeArmed(commandHandler->GetAccessingFabricIndex()), Status::FailsafeRequired); + + Thread::OperationalDataset activeDataset; + Thread::OperationalDataset currentActiveDataset; + uint64_t currentActiveDatasetTimestamp = 0; + // If any of the parameters in the ActiveDataset is invalid, the command SHALL fail with a status code + // of INVALID_COMMAND. + VerifyOrReturnValue(activeDataset.Init(req.activeDataset) == CHIP_NO_ERROR, Status::InvalidCommand); + + // If this command is invoked when the ActiveDatasetTimestamp attribute is not null, the command SHALL + // fail with a status code of INVALID_IN_STATE. + if ((mDelegate->GetDataset(currentActiveDataset, Delegate::DatasetType::kActive) == CHIP_NO_ERROR) && + (currentActiveDataset.GetActiveTimestamp(currentActiveDatasetTimestamp) == CHIP_NO_ERROR)) + { + return Status::InvalidInState; + } + // If there is a back end command process, return status BUSY. + if (mAsyncCommandHandle.Get()) + { + return Status::Busy; + } + commandHandler->FlushAcksRightAwayOnSlowCommand(); + mAsyncCommandHandle = CommandHandler::Handle(commandHandler); + mBreadcrumb = req.breadcrumb; + mSetActiveDatasetSequenceNumber++; + mDelegate->SetActiveDataset(activeDataset, mSetActiveDatasetSequenceNumber, this); + return Status::Success; +} + +Status ServerInstance::HandleSetPendingDatasetRequest(const Commands::SetPendingDatasetRequest::DecodableType & req) +{ + VerifyOrDie(mDelegate); + if (!mDelegate->GetPanChangeSupported()) + { + return Status::UnsupportedCommand; + } + Thread::OperationalDataset pendingDataset; + // If any of the parameters in the PendingDataset is invalid, the command SHALL fail with a status code + // of INVALID_COMMAND. + ReturnErrorCodeIf(pendingDataset.Init(req.pendingDataset) != CHIP_NO_ERROR, Status::InvalidCommand); + CHIP_ERROR err = mDelegate->SetPendingDataset(pendingDataset); + return StatusIB(err).mStatus; +} + +void AddDatasetResponse(CommandHandlerInterface::HandlerContext & ctx, Status status, const Thread::OperationalDataset & dataset) +{ + if (status != Status::Success) + { + ctx.mCommandHandler.AddStatus(ctx.mRequestPath, status); + return; + } + Commands::DatasetResponse::Type response; + response.dataset = dataset.AsByteSpan(); + ctx.mCommandHandler.AddResponse(ctx.mRequestPath, response); +} + +void ServerInstance::InvokeCommand(HandlerContext & ctxt) +{ + switch (ctxt.mRequestPath.mCommandId) + { + case Commands::GetActiveDatasetRequest::Id: + HandleCommand(ctxt, [this](HandlerContext & ctx, const auto & req) { + Thread::OperationalDataset dataset; + Status status = HandleGetActiveDatasetRequest(IsCommandOverCASESession(ctx), dataset); + AddDatasetResponse(ctx, status, dataset); + }); + break; + case Commands::GetPendingDatasetRequest::Id: + HandleCommand(ctxt, [this](HandlerContext & ctx, const auto & req) { + Thread::OperationalDataset dataset; + Status status = HandleGetPendingDatasetRequest(IsCommandOverCASESession(ctx), dataset); + AddDatasetResponse(ctx, status, dataset); + }); + break; + case Commands::SetActiveDatasetRequest::Id: + HandleCommand(ctxt, [this](HandlerContext & ctx, const auto & req) { + mPath = ctx.mRequestPath; + Status status = HandleSetActiveDatasetRequest(&ctx.mCommandHandler, req); + if (status != Status::Success) + { + // If status is not Success, we should immediately report the status. Otherwise the async work will report the + // status to the client. + ctx.mCommandHandler.AddStatus(ctx.mRequestPath, status); + } + }); + break; + case Commands::SetPendingDatasetRequest::Id: + HandleCommand(ctxt, [this](HandlerContext & ctx, const auto & req) { + ctx.mCommandHandler.AddStatus(ctx.mRequestPath, HandleSetPendingDatasetRequest(req)); + }); + break; + default: + break; + } +} + +void ServerInstance::ReadFeatureMap(BitFlags & outFeatureMap) +{ + if (mDelegate->GetPanChangeSupported()) + { + outFeatureMap.Set(Feature::kPANChange); + } +} + +CHIP_ERROR ServerInstance::ReadBorderRouterName(MutableCharSpan & outBorderRouterName) +{ + mDelegate->GetBorderRouterName(outBorderRouterName); + VerifyOrReturnValue(outBorderRouterName.size() <= kBorderRouterNameMaxLength, CHIP_IM_GLOBAL_STATUS(Failure)); + return CHIP_NO_ERROR; +} + +CHIP_ERROR ServerInstance::ReadBorderAgentID(MutableByteSpan & outBorderAgentId) +{ + VerifyOrReturnValue((mDelegate->GetBorderAgentId(outBorderAgentId) == CHIP_NO_ERROR) && + (outBorderAgentId.size() == kBorderAgentIdLength), + CHIP_IM_GLOBAL_STATUS(Failure)); + return CHIP_NO_ERROR; +} + +Optional ServerInstance::ReadActiveDatasetTimestamp() +{ + uint64_t activeDatasetTimestampValue = 0; + Thread::OperationalDataset activeDataset; + if ((mDelegate->GetDataset(activeDataset, Delegate::DatasetType::kActive) == CHIP_NO_ERROR) && + (activeDataset.GetActiveTimestamp(activeDatasetTimestampValue) == CHIP_NO_ERROR)) + { + return MakeOptional(activeDatasetTimestampValue); + } + return NullOptional; +} + +CHIP_ERROR ServerInstance::Read(const ConcreteReadAttributePath & aPath, AttributeValueEncoder & aEncoder) +{ + if (aPath.mClusterId != ThreadBorderRouterManagement::Id) + { + return CHIP_ERROR_INVALID_ARGUMENT; + } + VerifyOrDie(mDelegate); + CHIP_ERROR status = CHIP_NO_ERROR; + switch (aPath.mAttributeId) + { + case Globals::Attributes::FeatureMap::Id: { + BitFlags featureMap; + ReadFeatureMap(featureMap); + status = aEncoder.Encode(featureMap); + break; + } + case Attributes::BorderRouterName::Id: { + char borderRouterNameBuf[kBorderRouterNameMaxLength] = { 0 }; + MutableCharSpan borderRouterName(borderRouterNameBuf); + status = ReadBorderRouterName(borderRouterName); + // If there are any internal errors, the status will be returned and the client will get an error report. + if (status == CHIP_NO_ERROR) + { + status = aEncoder.Encode(borderRouterName); + } + break; + } + case Attributes::BorderAgentID::Id: { + uint8_t borderAgentIDBuf[kBorderAgentIdLength] = { 0 }; + MutableByteSpan borderAgentID(borderAgentIDBuf); + status = ReadBorderAgentID(borderAgentID); + if (status == CHIP_NO_ERROR) + { + status = aEncoder.Encode(borderAgentID); + } + break; + } + case Attributes::ThreadVersion::Id: { + uint16_t threadVersion = mDelegate->GetThreadVersion(); + status = aEncoder.Encode(threadVersion); + break; + } + case Attributes::InterfaceEnabled::Id: { + bool interfaceEnabled = mDelegate->GetInterfaceEnabled(); + status = aEncoder.Encode(interfaceEnabled); + break; + } + case Attributes::ActiveDatasetTimestamp::Id: { + Optional activeDatasetTimestamp = ReadActiveDatasetTimestamp(); + status = activeDatasetTimestamp.HasValue() ? aEncoder.Encode(DataModel::MakeNullable(activeDatasetTimestamp.Value())) + : aEncoder.EncodeNull(); + break; + } + default: + break; + } + return status; +} + +void ServerInstance::CommitSavedBreadcrumb() +{ + if (mBreadcrumb.HasValue()) + { + GeneralCommissioning::SetBreadcrumb(mBreadcrumb.Value()); + } + mBreadcrumb.ClearValue(); +} + +void ServerInstance::OnActivateDatasetComplete(uint32_t sequenceNum, CHIP_ERROR error) +{ + auto commandHandleRef = std::move(mAsyncCommandHandle); + auto commandHandle = commandHandleRef.Get(); + if (commandHandle == nullptr) + { + return; + } + if (mSetActiveDatasetSequenceNumber != sequenceNum) + { + // Previous SetActiveDatasetRequest was handled. + return; + } + if (error == CHIP_NO_ERROR) + { + // TODO: SPEC Issue #10022 + CommitSavedBreadcrumb(); + } + else + { + ChipLogError(Zcl, "Failed on activating the active dataset for Thread BR: %" CHIP_ERROR_FORMAT, error.Format()); + } + commandHandle->AddStatus(mPath, StatusIB(error).mStatus); +} + +void ServerInstance::ReportAttributeChanged(AttributeId attributeId) +{ + MatterReportingAttributeChangeCallback(mServerEndpointId, Id, attributeId); +} + +void ServerInstance::OnFailSafeTimerExpired() +{ + if (mDelegate) + { + mDelegate->RevertActiveDataset(); + } + auto commandHandleRef = std::move(mAsyncCommandHandle); + auto commandHandle = commandHandleRef.Get(); + if (commandHandle == nullptr) + { + return; + } + commandHandle->AddStatus(mPath, Status::Timeout); +} + +void ServerInstance::OnPlatformEventHandler(const DeviceLayer::ChipDeviceEvent * event, intptr_t arg) +{ + ServerInstance * _this = reinterpret_cast(arg); + if (event->Type == DeviceLayer::DeviceEventType::kFailSafeTimerExpired) + { + _this->OnFailSafeTimerExpired(); + } + else if (event->Type == DeviceLayer::DeviceEventType::kCommissioningComplete) + { + _this->mDelegate->CommitActiveDataset(); + } +} + +CHIP_ERROR ServerInstance::Init() +{ + ReturnErrorCodeIf(!mDelegate, CHIP_ERROR_INVALID_ARGUMENT); + ReturnErrorOnFailure(InteractionModelEngine::GetInstance()->RegisterCommandHandler(this)); + VerifyOrReturnError(registerAttributeAccessOverride(this), CHIP_ERROR_INCORRECT_STATE); + ReturnErrorOnFailure(DeviceLayer::PlatformMgrImpl().AddEventHandler(OnPlatformEventHandler, reinterpret_cast(this))); + return mDelegate->Init(this); +} + +} // namespace ThreadBorderRouterManagement +} // namespace Clusters +} // namespace app +} // namespace chip + +void MatterThreadBorderRouterManagementPluginServerInitCallback() +{ + // Nothing to do, the server init routine will be done in Instance::Init() +} diff --git a/src/app/clusters/thread-border-router-management-server/thread-border-router-management-server.h b/src/app/clusters/thread-border-router-management-server/thread-border-router-management-server.h new file mode 100644 index 00000000000000..a2b6d7949ffdff --- /dev/null +++ b/src/app/clusters/thread-border-router-management-server/thread-border-router-management-server.h @@ -0,0 +1,106 @@ +/* + * + * 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 "thread-br-delegate.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace chip { +namespace app { +namespace Clusters { +namespace ThreadBorderRouterManagement { + +class ServerInstance : public CommandHandlerInterface, + public AttributeAccessInterface, + public Delegate::ActivateDatasetCallback, + public Delegate::AttributeChangeCallback +{ +public: + using Status = Protocols::InteractionModel::Status; + ServerInstance(EndpointId endpointId, Delegate * delegate, FailSafeContext & failSafeContext) : + CommandHandlerInterface(Optional(endpointId), Id), + AttributeAccessInterface(Optional(endpointId), Id), mDelegate(delegate), mServerEndpointId(endpointId), + mFailsafeContext(failSafeContext) + {} + virtual ~ServerInstance() = default; + + CHIP_ERROR Init(); + + // CommandHanlerInterface + void InvokeCommand(HandlerContext & ctx) override; + + // AttributeAccessInterface + CHIP_ERROR Read(const ConcreteReadAttributePath & aPath, AttributeValueEncoder & aEncoder) override; + + // ActivateDatasetCallback + void OnActivateDatasetComplete(uint32_t sequenceNum, CHIP_ERROR error) override; + + // AttributeChangeCallback + void ReportAttributeChanged(AttributeId attributeId) override; + +private: + // TODO: Split the business logic from the unit test class + friend class TestThreadBorderRouterManagementCluster; + // Command Handlers + Status HandleGetActiveDatasetRequest(bool isOverCASESession, Thread::OperationalDataset & dataset) + { + return HandleGetDatasetRequest(isOverCASESession, Delegate::DatasetType::kActive, dataset); + } + Status HandleGetPendingDatasetRequest(bool isOverCASESession, Thread::OperationalDataset & dataset) + { + return HandleGetDatasetRequest(isOverCASESession, Delegate::DatasetType::kPending, dataset); + } + Status HandleSetActiveDatasetRequest(CommandHandler * commandHandler, + const Commands::SetActiveDatasetRequest::DecodableType & req); + Status HandleSetPendingDatasetRequest(const Commands::SetPendingDatasetRequest::DecodableType & req); + Status HandleGetDatasetRequest(bool isOverCASESession, Delegate::DatasetType type, Thread::OperationalDataset & dataset); + + // Attribute Read handlers + void ReadFeatureMap(BitFlags & feature); + Optional ReadActiveDatasetTimestamp(); + CHIP_ERROR ReadBorderRouterName(MutableCharSpan & borderRouterName); + CHIP_ERROR ReadBorderAgentID(MutableByteSpan & borderAgentId); + + static void OnPlatformEventHandler(const DeviceLayer::ChipDeviceEvent * event, intptr_t arg); + void OnFailSafeTimerExpired(); + void CommitSavedBreadcrumb(); + + Delegate * mDelegate; + app::CommandHandler::Handle mAsyncCommandHandle; + ConcreteCommandPath mPath = ConcreteCommandPath(0, 0, 0); + Optional mBreadcrumb; + uint32_t mSetActiveDatasetSequenceNumber = 0; + EndpointId mServerEndpointId; + FailSafeContext & mFailsafeContext; +}; + +} // namespace ThreadBorderRouterManagement +} // namespace Clusters +} // namespace app +} // namespace chip diff --git a/src/app/clusters/thread-border-router-management-server/thread-br-delegate.h b/src/app/clusters/thread-border-router-management-server/thread-br-delegate.h new file mode 100644 index 00000000000000..201540af96ecff --- /dev/null +++ b/src/app/clusters/thread-border-router-management-server/thread-br-delegate.h @@ -0,0 +1,115 @@ +/* + * + * 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 + +namespace chip { +namespace app { +namespace Clusters { +namespace ThreadBorderRouterManagement { + +constexpr size_t kBorderRouterNameMaxLength = 63; +constexpr size_t kBorderAgentIdLength = 16; + +class Delegate +{ +public: + Delegate() = default; + virtual ~Delegate() = default; + + class ActivateDatasetCallback + { + public: + ActivateDatasetCallback() = default; + virtual ~ActivateDatasetCallback() = default; + // If the dataset is set successfully, OnActivateDatasetComplete should be called with CHIP_NO_ERROR when the + // Border Router is attached to the Thread network. + // If an error occurs while setting the active dataset, this callback should be called with the error. + // The error input of this function could be SDK-range error for CHIP error or OpenThread-range error for Thread error. + virtual void OnActivateDatasetComplete(uint32_t sequenceNum, CHIP_ERROR error) = 0; + }; + + class AttributeChangeCallback + { + public: + AttributeChangeCallback() = default; + virtual ~AttributeChangeCallback() = default; + // If the attributes of the Thread Border Router Management is changed, ReportAttributeChanged should be called. + virtual void ReportAttributeChanged(AttributeId attributeId) = 0; + }; + + enum class DatasetType : uint8_t + { + kActive, + kPending, + }; + + virtual CHIP_ERROR Init(AttributeChangeCallback * attributeChangeCallback) = 0; + + // Get whether PanChange feature is supported for the Thread BR. + virtual bool GetPanChangeSupported() = 0; + + // Get the BorderRouterName of the Thread BR, which will also be the service name of Thread BR's MeshCOP service. + virtual void GetBorderRouterName(MutableCharSpan & borderRouterName) = 0; + + // Get the BorderAgentId of the Thread BR. + // @return + // -IncorrectState When Thread stack is not initialized. + // -InvalidArgument When the size of borderAgentId is not 16 bytes. + // -ThreadErrors When failing to get BorderAgentId. + virtual CHIP_ERROR GetBorderAgentId(MutableByteSpan & borderAgentId) = 0; + + // Get the Thread version which matches the value mapping defined in the "Version TLV" section of the Thread specification. + virtual uint16_t GetThreadVersion() = 0; + + // Get whether the associated IEEE 802.15.4 Thread interface is enabled or disabled. + virtual bool GetInterfaceEnabled() = 0; + + // Get the active dataset or the pending dataset. + // @return + // -IncorrectState When Thread stack is not initialized. + // -NotFound when failing to get the dataset. + virtual CHIP_ERROR GetDataset(Thread::OperationalDataset & dataset, DatasetType type) = 0; + + // There should be no active dataset configured when calling this API, otherwise we should use SetPendingDataset. + // The Delegate implementation must store the sequence number and pass it to OnActivateDatasetComplete. + virtual void SetActiveDataset(const Thread::OperationalDataset & activeDataset, uint32_t sequenceNum, + ActivateDatasetCallback * callback) = 0; + + // This function will check save whether there is active dataset configured. + virtual CHIP_ERROR CommitActiveDataset() = 0; + + // The function is called when Failsafe timer is triggered or when the Border Router reboots with a previous Failsafe timer + // started but not disarmed before reboot. The delegate implementation should check whether there is a previous SetActiveDataset + // request and revert the active dataset set by the previous SetActiveDataset. Since there should be no configured dataset when + // calling SetActiveDataset, this function will clear the active dataset to allow trying again a new SetActiveDataset operation. + // The delegate is allowed to call OnActivateDatasetComplete for the previous SetActiveDataset request even after this function + // is called as the sequence number passed to OnActivateDatasetComplete will be different. + virtual CHIP_ERROR RevertActiveDataset() = 0; + + virtual CHIP_ERROR SetPendingDataset(const Thread::OperationalDataset & pendingDataset) = 0; +}; + +} // namespace ThreadBorderRouterManagement +} // namespace Clusters +} // namespace app +} // namespace chip diff --git a/src/app/common/templates/config-data.yaml b/src/app/common/templates/config-data.yaml index 83d1b682153ed7..432a7a5593e209 100644 --- a/src/app/common/templates/config-data.yaml +++ b/src/app/common/templates/config-data.yaml @@ -41,6 +41,7 @@ CommandHandlerInterfaceOnlyClusters: - Electrical Power Measurement - Electrical Energy Measurement - Wi-Fi Network Management + - Thread Border Router Management # We need a more configurable way of deciding which clusters have which init functions.... # See https://github.com/project-chip/connectedhomeip/issues/4369 diff --git a/src/app/tests/BUILD.gn b/src/app/tests/BUILD.gn index c1826884923af4..713cf0f8d87be9 100644 --- a/src/app/tests/BUILD.gn +++ b/src/app/tests/BUILD.gn @@ -220,6 +220,9 @@ chip_test_suite("tests") { "${chip_root}/src/app/server", "${chip_root}/src/messaging/tests/echo:common", ] + } else if (!chip_fake_platform) { + test_sources += [ "TestThreadBorderRouterManagementCluster.cpp" ] + public_deps += [ ":thread-border-router-management-test-srcs" ] } if (!chip_fake_platform) { diff --git a/src/app/tests/TestThreadBorderRouterManagementCluster.cpp b/src/app/tests/TestThreadBorderRouterManagementCluster.cpp new file mode 100644 index 00000000000000..a6915824fb2ea2 --- /dev/null +++ b/src/app/tests/TestThreadBorderRouterManagementCluster.cpp @@ -0,0 +1,336 @@ +/* + * + * 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. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace chip { +namespace app { +namespace Clusters { + +namespace GeneralCommissioning { +// Mock function +void SetBreadcrumb(Attributes::Breadcrumb::TypeInfo::Type breadcrumb) {} +} // namespace GeneralCommissioning + +namespace ThreadBorderRouterManagement { + +class TestDelegate : public Delegate +{ +public: + TestDelegate() = default; + ~TestDelegate() = default; + + CHIP_ERROR Init(AttributeChangeCallback * callback) override { return CHIP_NO_ERROR; } + + bool GetPanChangeSupported() override { return mPanChangeSupported; } + + void GetBorderRouterName(MutableCharSpan & borderRouterName) override + { + size_t nameIndex = mUseInvalidBorderRouterName ? 1 : 0; + if (borderRouterName.size() >= strlen(kTestName[nameIndex])) + { + CopyCharSpanToMutableCharSpan(CharSpan(kTestName[nameIndex], strlen(kTestName[nameIndex])), borderRouterName); + } + } + + CHIP_ERROR GetBorderAgentId(MutableByteSpan & borderAgentId) override + { + if (borderAgentId.size() >= mTestBorderAgentIdLen) + { + CopySpanToMutableSpan(ByteSpan(kTestBorderAgentId, mTestBorderAgentIdLen), borderAgentId); + return CHIP_NO_ERROR; + } + return CHIP_ERROR_BUFFER_TOO_SMALL; + } + + uint16_t GetThreadVersion() override { return kTestThreadVersion; } + + bool GetInterfaceEnabled() override { return mInterfaceEnabled; } + + CHIP_ERROR GetDataset(Thread::OperationalDataset & dataset, DatasetType type) override + { + if (type == DatasetType::kActive && mStoredActiveDatasetLen) + { + dataset.Init(ByteSpan(mStoredActiveDataset, mStoredActiveDatasetLen)); + return CHIP_NO_ERROR; + } + if (type == DatasetType::kPending && mPendingDatasetLen) + { + dataset.Init(ByteSpan(mPendingDataset, mPendingDatasetLen)); + return CHIP_NO_ERROR; + } + return CHIP_IM_GLOBAL_STATUS(NotFound); + } + + void SetActiveDataset(const Thread::OperationalDataset & activeDataset, uint32_t sequenceNumber, + ActivateDatasetCallback * callback) override + { + memcpy(mActiveDataset, activeDataset.AsByteSpan().data(), activeDataset.AsByteSpan().size()); + mActiveDatasetLen = activeDataset.AsByteSpan().size(); + mCallback = callback; + mSetActiveDatasetCommandSequenceNum = sequenceNumber; + } + + CHIP_ERROR CommitActiveDataset() override { return CHIP_NO_ERROR; } + + CHIP_ERROR RevertActiveDataset() override + { + mStoredActiveDatasetLen = 0; + mInterfaceEnabled = false; + mCallback = nullptr; + return CHIP_NO_ERROR; + } + + CHIP_ERROR SetPendingDataset(const Thread::OperationalDataset & pendingDataset) override + { + memcpy(mPendingDataset, pendingDataset.AsByteSpan().data(), pendingDataset.AsByteSpan().size()); + mPendingDatasetLen = pendingDataset.AsByteSpan().size(); + return CHIP_NO_ERROR; + } + + void ActivateActiveDataset() + { + memcpy(mStoredActiveDataset, mActiveDataset, Thread::kSizeOperationalDataset); + mStoredActiveDatasetLen = mActiveDatasetLen; + mInterfaceEnabled = true; + if (mCallback) + { + mCallback->OnActivateDatasetComplete(mSetActiveDatasetCommandSequenceNum, CHIP_NO_ERROR); + } + mCallback = nullptr; + } + + bool mPanChangeSupported = true; + const char * kTestName[2] = { "TestName", "TestNameLength64________________________________________________" }; + const uint8_t kTestBorderAgentId[kBorderAgentIdLength] = { 0x0, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, + 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF }; + const uint16_t kTestThreadVersion = 4; + uint8_t mActiveDataset[Thread::kSizeOperationalDataset] = { 0 }; + size_t mActiveDatasetLen = 0; + uint8_t mStoredActiveDataset[Thread::kSizeOperationalDataset] = { 0 }; + size_t mStoredActiveDatasetLen = 0; + uint8_t mPendingDataset[Thread::kSizeOperationalDataset] = { 0 }; + size_t mPendingDatasetLen = 0; + bool mUseInvalidBorderRouterName = true; + size_t mTestBorderAgentIdLen = kBorderAgentIdLength - 1; + bool mInterfaceEnabled = false; + uint32_t mSetActiveDatasetCommandSequenceNum = 0; + ActivateDatasetCallback * mCallback = nullptr; +}; + +constexpr EndpointId kTestEndpointId = 1; +constexpr FabricIndex kTestAccessingFabricIndex = 1; +static FailSafeContext sTestFailsafeContext; +static TestDelegate sTestDelegate; +static ServerInstance sTestSeverInstance(kTestEndpointId, &sTestDelegate, sTestFailsafeContext); + +class TestSetActiveDatasetCommandHandler : public CommandHandler +{ +public: + TestSetActiveDatasetCommandHandler() : mClusterStatus(Protocols::InteractionModel::Status::Success) {} + CHIP_ERROR FallibleAddStatus(const ConcreteCommandPath & aRequestCommandPath, + const Protocols::InteractionModel::ClusterStatusCode & aStatus, const char * context = nullptr) + { + return CHIP_NO_ERROR; + } + + void AddStatus(const ConcreteCommandPath & aRequestCommandPath, const Protocols::InteractionModel::ClusterStatusCode & aStatus, + const char * context = nullptr) + { + mClusterStatus = aStatus; + } + + FabricIndex GetAccessingFabricIndex() const { return kTestAccessingFabricIndex; } + + CHIP_ERROR AddResponseData(const ConcreteCommandPath & aRequestCommandPath, CommandId aResponseCommandId, + const DataModel::EncodableToTLV & aEncodable) + { + return CHIP_NO_ERROR; + } + + void AddResponse(const ConcreteCommandPath & aRequestCommandPath, CommandId aResponseCommandId, + const DataModel::EncodableToTLV & aEncodable) + {} + + bool IsTimedInvoke() const { return false; } + + void FlushAcksRightAwayOnSlowCommand() {} + + Access::SubjectDescriptor GetSubjectDescriptor() const + { + Access::SubjectDescriptor subjectDescriptor = { kUndefinedFabricIndex, Access::AuthMode::kNone, kUndefinedNodeId, + kUndefinedCATs }; + return subjectDescriptor; + } + + Messaging::ExchangeContext * GetExchangeContext() const { return nullptr; } + + Protocols::InteractionModel::ClusterStatusCode mClusterStatus; +}; + +TestSetActiveDatasetCommandHandler sTestCommandHandler; + +class TestThreadBorderRouterManagementCluster : public ::testing::Test +{ +public: + static void SetUpTestSuite() + { + ASSERT_EQ(Platform::MemoryInit(), CHIP_NO_ERROR); + ASSERT_EQ(DeviceLayer::PlatformMgr().InitChipStack(), CHIP_NO_ERROR); + } + + static void TearDownTestSuite() + { + DeviceLayer::PlatformMgr().Shutdown(); + Platform::MemoryShutdown(); + } + + void TestAttributeRead(); + void TestCommandHandle(); +}; + +// Test ReadXX functions in ThreadBorderRouterManagement ServerInstance +TEST_F_FROM_FIXTURE(TestThreadBorderRouterManagementCluster, TestAttributeRead) +{ + // FeatureMap attribute + BitFlags featureMap = BitFlags(); + // Make the PAN change feature supported in Test delegate. + sTestDelegate.mPanChangeSupported = true; + sTestSeverInstance.ReadFeatureMap(featureMap); + EXPECT_TRUE(featureMap.Has(Feature::kPANChange)); + // Make the PAN change feature unsupported in Test delegate. + sTestDelegate.mPanChangeSupported = false; + featureMap.ClearAll(); + sTestSeverInstance.ReadFeatureMap(featureMap); + EXPECT_FALSE(featureMap.Has(Feature::kPANChange)); + // BorderRouterName attribute + // Use invalid BR name + sTestDelegate.mUseInvalidBorderRouterName = true; + char borderRouterName[kBorderRouterNameMaxLength + 10] = { 0 }; + MutableCharSpan nameSpan(borderRouterName); + EXPECT_EQ(sTestSeverInstance.ReadBorderRouterName(nameSpan), CHIP_IM_GLOBAL_STATUS(Failure)); + nameSpan = MutableCharSpan(borderRouterName); + // Use valid BR name + sTestDelegate.mUseInvalidBorderRouterName = false; + EXPECT_EQ(sTestSeverInstance.ReadBorderRouterName(nameSpan), CHIP_NO_ERROR); + EXPECT_TRUE(nameSpan.data_equal(CharSpan("TestName", strlen("TestName")))); + // BorderAgentId attribute + uint8_t borderAgentId[kBorderAgentIdLength] = { 0 }; + MutableByteSpan agentIdSpan(borderAgentId); + // Use invalid border agent id + sTestDelegate.mTestBorderAgentIdLen = kBorderAgentIdLength - 1; + EXPECT_EQ(sTestSeverInstance.ReadBorderAgentID(agentIdSpan), CHIP_IM_GLOBAL_STATUS(Failure)); + agentIdSpan = MutableByteSpan(borderAgentId); + // Use valid border agent id + sTestDelegate.mTestBorderAgentIdLen = kBorderAgentIdLength; + EXPECT_EQ(sTestSeverInstance.ReadBorderAgentID(agentIdSpan), CHIP_NO_ERROR); + EXPECT_TRUE(agentIdSpan.data_equal(ByteSpan(sTestDelegate.kTestBorderAgentId))); + // ActiveDatasetTimestamp attribute + // The active dataset timestamp should be null when no active dataset is configured + Optional timestamp = sTestSeverInstance.ReadActiveDatasetTimestamp(); + EXPECT_FALSE(timestamp.HasValue()); +} + +TEST_F_FROM_FIXTURE(TestThreadBorderRouterManagementCluster, TestCommandHandle) +{ + // Test GetActiveDatasetRequest and GetPendingDatasetRequest commands + Thread::OperationalDataset dataset; + using DatasetType = Delegate::DatasetType; + using Status = Protocols::InteractionModel::Status; + // The GetDataset requests should over CASE session. + EXPECT_EQ(sTestSeverInstance.HandleGetDatasetRequest(false /* isOverCASESession */, DatasetType::kActive, dataset), + Status::UnsupportedAccess); + EXPECT_EQ(sTestSeverInstance.HandleGetDatasetRequest(false, DatasetType::kPending, dataset), Status::UnsupportedAccess); + // The GetDataset should return NotFound when no dataset is configured. + EXPECT_EQ(sTestSeverInstance.HandleGetDatasetRequest(true, DatasetType::kActive, dataset), Status::NotFound); + EXPECT_EQ(sTestSeverInstance.HandleGetDatasetRequest(true, DatasetType::kPending, dataset), Status::NotFound); + // Test SetActiveDatasetRequest + ThreadBorderRouterManagement::Commands::SetActiveDatasetRequest::DecodableType req1; + uint8_t invalidDataset[] = { 0x11, 0x22, 0x33, 0x44, 0x55, 0x66 }; + uint8_t validDataset[] = { 0x0e, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x0b, 0x35, 0x06, + 0x00, 0x04, 0x00, 0x1f, 0xff, 0xe0, 0x02, 0x08, 0xde, 0xaa, 0x00, 0xbe, 0xef, 0x00, 0xca, 0xef, 0x07, + 0x08, 0xfd, 0xde, 0xad, 0x00, 0xbe, 0xef, 0x00, 0x00, 0x05, 0x10, 0xb7, 0x28, 0x08, 0x04, 0x85, 0xcf, + 0xc5, 0x25, 0x7f, 0x68, 0x4c, 0x54, 0x9d, 0x6a, 0x57, 0x5e, 0x03, 0x0a, 0x4f, 0x70, 0x65, 0x6e, 0x54, + 0x68, 0x72, 0x65, 0x61, 0x64, 0x01, 0x02, 0xc1, 0x15, 0x04, 0x10, 0xcb, 0x13, 0x47, 0xeb, 0x0c, 0xd4, + 0xb3, 0x5c, 0xd1, 0x42, 0xda, 0x5e, 0x6d, 0xf1, 0x8b, 0x88, 0x0c, 0x04, 0x02, 0xa0, 0xf7, 0xf8 }; + Optional activeDatasetTimestamp = chip::NullOptional; + activeDatasetTimestamp = sTestSeverInstance.ReadActiveDatasetTimestamp(); + EXPECT_FALSE(activeDatasetTimestamp.HasValue()); + req1.activeDataset = ByteSpan(invalidDataset); + // SetActiveDatasetRequest is FailsafeRequired. + EXPECT_EQ(sTestSeverInstance.HandleSetActiveDatasetRequest(&sTestCommandHandler, req1), Status::FailsafeRequired); + EXPECT_EQ(sTestFailsafeContext.ArmFailSafe(kTestAccessingFabricIndex, System::Clock::Seconds16(1)), CHIP_NO_ERROR); + // SetActiveDatasetRequest should return InvalidCommand when dataset is invalid. + EXPECT_EQ(sTestSeverInstance.HandleSetActiveDatasetRequest(&sTestCommandHandler, req1), Status::InvalidCommand); + req1.activeDataset = ByteSpan(validDataset); + EXPECT_EQ(sTestSeverInstance.HandleSetActiveDatasetRequest(&sTestCommandHandler, req1), Status::Success); + // When the Server is handling a SetActiveDatasetRequest command, it should return Busy after receiving another one. + EXPECT_EQ(sTestSeverInstance.HandleSetActiveDatasetRequest(&sTestCommandHandler, req1), Status::Busy); + EXPECT_FALSE(sTestDelegate.mInterfaceEnabled); + EXPECT_EQ(sTestDelegate.mSetActiveDatasetCommandSequenceNum, static_cast(1)); + // Activate the dataset. + sTestDelegate.ActivateActiveDataset(); + EXPECT_EQ(sTestCommandHandler.mClusterStatus, + Protocols::InteractionModel::ClusterStatusCode(Protocols::InteractionModel::Status::Success)); + sTestFailsafeContext.DisarmFailSafe(); + // The Dataset should be updated. + EXPECT_EQ(sTestSeverInstance.HandleGetDatasetRequest(true, DatasetType::kActive, dataset), Status::Success); + EXPECT_TRUE(dataset.AsByteSpan().data_equal(ByteSpan(validDataset))); + EXPECT_TRUE(sTestDelegate.mInterfaceEnabled); + activeDatasetTimestamp = sTestSeverInstance.ReadActiveDatasetTimestamp(); + // activeDatasetTimestamp should have value. + EXPECT_TRUE(activeDatasetTimestamp.HasValue()); + EXPECT_EQ(sTestFailsafeContext.ArmFailSafe(kTestAccessingFabricIndex, System::Clock::Seconds16(1)), CHIP_NO_ERROR); + // When ActiveDatasetTimestamp is not null, the set active dataset request should return InvalidInState. + EXPECT_EQ(sTestSeverInstance.HandleSetActiveDatasetRequest(&sTestCommandHandler, req1), Status::InvalidInState); + sTestFailsafeContext.DisarmFailSafe(); + // Test SetPendingDatasetRequest command + Commands::SetPendingDatasetRequest::DecodableType req2; + sTestDelegate.mPanChangeSupported = false; + req2.pendingDataset = ByteSpan(validDataset); + // SetPendingDatasetRequest is supported when PANChange feature is enabled. + EXPECT_EQ(sTestSeverInstance.HandleSetPendingDatasetRequest(req2), Status::UnsupportedCommand); + sTestDelegate.mPanChangeSupported = true; + req2.pendingDataset = ByteSpan(invalidDataset); + // SetPendingDatasetRequest should return InvalidCommand when dataset is invalid. + EXPECT_EQ(sTestSeverInstance.HandleSetPendingDatasetRequest(req2), Status::InvalidCommand); + req2.pendingDataset = ByteSpan(validDataset); + // Success SetPendingDatasetRequest + EXPECT_EQ(sTestSeverInstance.HandleSetPendingDatasetRequest(req2), Status::Success); + EXPECT_EQ(sTestSeverInstance.HandleGetDatasetRequest(true, DatasetType::kPending, dataset), Status::Success); + EXPECT_TRUE(dataset.AsByteSpan().data_equal(ByteSpan(validDataset))); +} + +} // namespace ThreadBorderRouterManagement +} // namespace Clusters +} // namespace app +} // namespace chip diff --git a/src/app/zap-templates/zcl/zcl-with-test-extensions.json b/src/app/zap-templates/zcl/zcl-with-test-extensions.json index c6b39c837a1e00..fed647bf301ab7 100644 --- a/src/app/zap-templates/zcl/zcl-with-test-extensions.json +++ b/src/app/zap-templates/zcl/zcl-with-test-extensions.json @@ -298,6 +298,14 @@ "general_error_boolean", "cluster_error_boolean" ], + "Thread Border Router Management": [ + "BorderRouterName", + "BorderAgentID", + "ThreadVersion", + "InterfaceEnabled", + "ActiveDatasetTimestamp", + "FeatureMap" + ], "Thread Network Diagnostics": [ "Channel", "RoutingRole", diff --git a/src/app/zap-templates/zcl/zcl.json b/src/app/zap-templates/zcl/zcl.json index 18afa7eacc2f8a..5e7a529f2ae35a 100644 --- a/src/app/zap-templates/zcl/zcl.json +++ b/src/app/zap-templates/zcl/zcl.json @@ -296,6 +296,14 @@ "general_error_boolean", "cluster_error_boolean" ], + "Thread Border Router Management": [ + "BorderRouterName", + "BorderAgentID", + "ThreadVersion", + "InterfaceEnabled", + "ActiveDatasetTimestamp", + "FeatureMap" + ], "Thread Network Diagnostics": [ "Channel", "RoutingRole", diff --git a/src/app/zap_cluster_list.json b/src/app/zap_cluster_list.json index dc324968991adc..a8aa4ce320b990 100644 --- a/src/app/zap_cluster_list.json +++ b/src/app/zap_cluster_list.json @@ -289,7 +289,9 @@ "THERMOSTAT_USER_INTERFACE_CONFIGURATION_CLUSTER": [ "thermostat-user-interface-configuration-server" ], - "THREAD_BORDER_ROUTER_MANAGEMENT_CLUSTER": [], + "THREAD_BORDER_ROUTER_MANAGEMENT_CLUSTER": [ + "thread-border-router-management-server" + ], "THREAD_NETWORK_DIAGNOSTICS_CLUSTER": [ "thread-network-diagnostics-server" ], diff --git a/src/platform/OpenThread/GenericThreadBorderRouterDelegate.cpp b/src/platform/OpenThread/GenericThreadBorderRouterDelegate.cpp new file mode 100644 index 00000000000000..26bb08a39fa296 --- /dev/null +++ b/src/platform/OpenThread/GenericThreadBorderRouterDelegate.cpp @@ -0,0 +1,224 @@ +/* + * + * 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. + */ + +#include "GenericThreadBorderRouterDelegate.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace chip { +namespace app { +namespace Clusters { + +namespace ThreadBorderRouterManagement { + +class ScopedThreadLock +{ +public: + ScopedThreadLock() { DeviceLayer::ThreadStackMgr().LockThreadStack(); } + ~ScopedThreadLock() { DeviceLayer::ThreadStackMgr().UnlockThreadStack(); } +}; + +CHIP_ERROR GenericOpenThreadBorderRouterDelegate::Init(AttributeChangeCallback * callback) +{ + mpActivateDatasetCallback = nullptr; + mpAttributeChangeCallback = callback; + ReturnErrorOnFailure(DeviceLayer::PlatformMgrImpl().AddEventHandler(OnPlatformEventHandler, reinterpret_cast(this))); + // When the Thread Border Router is reboot during SetActiveDataset, we need to revert the active dateset. + RevertActiveDataset(); + return CHIP_NO_ERROR; +} + +CHIP_ERROR GenericOpenThreadBorderRouterDelegate::GetBorderAgentId(MutableByteSpan & borderAgentIdSpan) +{ + otInstance * otInst = DeviceLayer::ThreadStackMgrImpl().OTInstance(); + VerifyOrReturnError(otInst, CHIP_ERROR_INCORRECT_STATE); + otBorderAgentId borderAgentId; + if (borderAgentIdSpan.size() != sizeof(borderAgentId.mId)) + { + return CHIP_ERROR_INVALID_ARGUMENT; + } + otError otErr = OT_ERROR_NONE; + { + ScopedThreadLock threadLock; + otErr = otBorderAgentGetId(otInst, &borderAgentId); + } + if (otErr == OT_ERROR_NONE) + { + CopySpanToMutableSpan(ByteSpan(borderAgentId.mId), borderAgentIdSpan); + return CHIP_NO_ERROR; + } + return DeviceLayer::Internal::MapOpenThreadError(otErr); +} + +uint16_t GenericOpenThreadBorderRouterDelegate::GetThreadVersion() +{ + return otThreadGetVersion(); +} + +bool GenericOpenThreadBorderRouterDelegate::GetInterfaceEnabled() +{ + otInstance * otInst = DeviceLayer::ThreadStackMgrImpl().OTInstance(); + VerifyOrReturnValue(otInst, false); + ScopedThreadLock threadLock; + return otIp6IsEnabled(otInst); +} + +CHIP_ERROR GenericOpenThreadBorderRouterDelegate::GetDataset(Thread::OperationalDataset & dataset, DatasetType type) +{ + otInstance * otInst = DeviceLayer::ThreadStackMgrImpl().OTInstance(); + VerifyOrReturnError(otInst, CHIP_ERROR_INCORRECT_STATE); + + otError otErr = OT_ERROR_NONE; + otOperationalDatasetTlvs datasetTlvs; + { + ScopedThreadLock threadLock; + if (type == DatasetType::kActive) + { + otErr = otDatasetGetActiveTlvs(otInst, &datasetTlvs); + } + else + { + otErr = otDatasetGetPendingTlvs(otInst, &datasetTlvs); + } + } + if (otErr == OT_ERROR_NONE) + { + return dataset.Init(ByteSpan(datasetTlvs.mTlvs, datasetTlvs.mLength)); + } + return DeviceLayer::Internal::MapOpenThreadError(otErr); +} + +void GenericOpenThreadBorderRouterDelegate::SetActiveDataset(const Thread::OperationalDataset & activeDataset, uint32_t sequenceNum, + ActivateDatasetCallback * callback) +{ + // This function will never be invoked when there is an Active Dataset already configured. + CHIP_ERROR err = SaveActiveDatasetConfigured(false); + if (err == CHIP_NO_ERROR) + { + err = DeviceLayer::ThreadStackMgrImpl().AttachToThreadNetwork(activeDataset, nullptr); + } + if (err != CHIP_NO_ERROR) + { + callback->OnActivateDatasetComplete(sequenceNum, err); + return; + } + mSequenceNum = sequenceNum; + mpActivateDatasetCallback = callback; +} + +void GenericOpenThreadBorderRouterDelegate::OnPlatformEventHandler(const DeviceLayer::ChipDeviceEvent * event, intptr_t arg) +{ + GenericOpenThreadBorderRouterDelegate * delegate = reinterpret_cast(arg); + if (delegate) + { + if ((event->Type == DeviceLayer::DeviceEventType::kThreadConnectivityChange) && + (event->ThreadConnectivityChange.Result == DeviceLayer::kConnectivity_Established) && + delegate->mpActivateDatasetCallback) + { + delegate->mpActivateDatasetCallback->OnActivateDatasetComplete(delegate->mSequenceNum, CHIP_NO_ERROR); + delegate->mpActivateDatasetCallback = nullptr; + } + } + if (event->Type == DeviceLayer::DeviceEventType::kThreadStateChange) + { + if (event->ThreadStateChange.OpenThread.Flags & OT_CHANGED_THREAD_NETIF_STATE) + { + DeviceLayer::SystemLayer().ScheduleLambda( + [delegate]() { delegate->mpAttributeChangeCallback->ReportAttributeChanged(Attributes::InterfaceEnabled::Id); }); + } + if (event->ThreadStateChange.OpenThread.Flags & OT_CHANGED_ACTIVE_DATASET) + { + DeviceLayer::SystemLayer().ScheduleLambda([delegate]() { + delegate->mpAttributeChangeCallback->ReportAttributeChanged(Attributes::ActiveDatasetTimestamp::Id); + }); + } + } +} + +CHIP_ERROR GenericOpenThreadBorderRouterDelegate::SaveActiveDatasetConfigured(bool configured) +{ + VerifyOrReturnError(mStorage, CHIP_ERROR_INTERNAL); + return mStorage->SyncSetKeyValue(kFailsafeActiveDatasetConfigured, &configured, sizeof(bool)); +} + +CHIP_ERROR GenericOpenThreadBorderRouterDelegate::CommitActiveDataset() +{ + return SaveActiveDatasetConfigured(DeviceLayer::ThreadStackMgrImpl().IsThreadAttached()); +} + +CHIP_ERROR GenericOpenThreadBorderRouterDelegate::RevertActiveDataset() +{ + // The FailSafe Timer is triggered and the previous command request should be handled, so reset the callback. + mpActivateDatasetCallback = nullptr; + bool activeDatasetConfigured = true; + uint16_t activeDatasetConfiguredLen = sizeof(bool); + VerifyOrReturnError(mStorage, CHIP_ERROR_INTERNAL); + mStorage->SyncGetKeyValue(kFailsafeActiveDatasetConfigured, &activeDatasetConfigured, activeDatasetConfiguredLen); + VerifyOrDie(activeDatasetConfiguredLen == sizeof(bool)); + if (!activeDatasetConfigured) + { + // The active dataset should be no configured after calling this function, so we will try to attach an empty Thread dataset + // and that will clear the one stored in the Thread stack since the SetActiveDataset operation fails and FailSafe timer is + // triggered. + Thread::OperationalDataset emptyDataset = {}; + CHIP_ERROR err = DeviceLayer::ThreadStackMgrImpl().AttachToThreadNetwork(emptyDataset, nullptr); + SaveActiveDatasetConfigured(false); + return err; + } + return CHIP_NO_ERROR; +} + +CHIP_ERROR GenericOpenThreadBorderRouterDelegate::SetPendingDataset(const Thread::OperationalDataset & pendingDataset) +{ + otInstance * otInst = DeviceLayer::ThreadStackMgrImpl().OTInstance(); + VerifyOrReturnError(otInst, CHIP_ERROR_INCORRECT_STATE); + + otOperationalDatasetTlvs datasetTlvs; + memcpy(datasetTlvs.mTlvs, pendingDataset.AsByteSpan().data(), pendingDataset.AsByteSpan().size()); + datasetTlvs.mLength = pendingDataset.AsByteSpan().size(); + { + ScopedThreadLock threadLock; + ReturnErrorCodeIf(otDatasetSetPendingTlvs(otInst, &datasetTlvs) != OT_ERROR_NONE, CHIP_ERROR_INTERNAL); + } + return CHIP_NO_ERROR; +} + +} // namespace ThreadBorderRouterManagement +} // namespace Clusters +} // namespace app +} // namespace chip diff --git a/src/platform/OpenThread/GenericThreadBorderRouterDelegate.h b/src/platform/OpenThread/GenericThreadBorderRouterDelegate.h new file mode 100644 index 00000000000000..079f1bd6e38edd --- /dev/null +++ b/src/platform/OpenThread/GenericThreadBorderRouterDelegate.h @@ -0,0 +1,104 @@ +/* + * + * 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 +#include +#include +#include +#include +#include + +namespace chip { +namespace app { +namespace Clusters { + +namespace ThreadBorderRouterManagement { + +class GenericOpenThreadBorderRouterDelegate : public Delegate +{ +public: + static constexpr char kFailsafeActiveDatasetConfigured[] = "g/fs/tbradc"; + GenericOpenThreadBorderRouterDelegate(PersistentStorageDelegate * storage) : mStorage(storage) {} + ~GenericOpenThreadBorderRouterDelegate() = default; + + CHIP_ERROR Init(AttributeChangeCallback * callback) override; + + bool GetPanChangeSupported() override { return true; } + + void GetBorderRouterName(MutableCharSpan & borderRouterName) override + { + CopyCharSpanToMutableCharSpan(CharSpan(mThreadBorderRouterName, strlen(mThreadBorderRouterName)), borderRouterName); + } + + CHIP_ERROR GetBorderAgentId(MutableByteSpan & borderAgentId) override; + + uint16_t GetThreadVersion() override; + + bool GetInterfaceEnabled() override; + + CHIP_ERROR GetDataset(Thread::OperationalDataset & dataset, DatasetType type) override; + + void SetActiveDataset(const Thread::OperationalDataset & activeDataset, uint32_t sequenceNum, + ActivateDatasetCallback * callback) override; + + CHIP_ERROR CommitActiveDataset() override; + + CHIP_ERROR RevertActiveDataset() override; + + CHIP_ERROR SetPendingDataset(const Thread::OperationalDataset & pendingDataset) override; + + static void OnPlatformEventHandler(const DeviceLayer::ChipDeviceEvent * event, intptr_t arg); + + void SetThreadBorderRouterName(const CharSpan & name) + { + MutableCharSpan borderRouterName(mThreadBorderRouterName); + CopyCharSpanToMutableCharSpan(name, borderRouterName); + if (mpAttributeChangeCallback) + { + DeviceLayer::SystemLayer().ScheduleLambda( + [this]() { mpAttributeChangeCallback->ReportAttributeChanged(Attributes::BorderRouterName::Id); }); + } + } + + void NotifyBorderAgentIdChanged() + { + if (mpAttributeChangeCallback) + { + // OpenThread doesn't have callback or event for BorderAgentId change, we can only change the BorderAgentId with + // otBorderAgentSetId(). Please call this function with otBorderAgentSetId(). + DeviceLayer::SystemLayer().ScheduleLambda( + [this]() { mpAttributeChangeCallback->ReportAttributeChanged(Attributes::BorderAgentID::Id); }); + } + } + +private: + CHIP_ERROR SaveActiveDatasetConfigured(bool configured); + ActivateDatasetCallback * mpActivateDatasetCallback = nullptr; + uint32_t mSequenceNum = 0; + char mThreadBorderRouterName[kBorderRouterNameMaxLength + 1]; + PersistentStorageDelegate * mStorage; + AttributeChangeCallback * mpAttributeChangeCallback = nullptr; +}; +} // namespace ThreadBorderRouterManagement +} // namespace Clusters +} // namespace app +} // namespace chip