diff --git a/examples/fabric-admin/device_manager/CommissionerControl.cpp b/examples/fabric-admin/device_manager/CommissionerControl.cpp index 80c9437450cc80..45c7bc02da69a5 100644 --- a/examples/fabric-admin/device_manager/CommissionerControl.cpp +++ b/examples/fabric-admin/device_manager/CommissionerControl.cpp @@ -1,3 +1,21 @@ +/* + * 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 "CommissionerControl.h" #include "DeviceManager.h" diff --git a/examples/fabric-sync/admin/BUILD.gn b/examples/fabric-sync/admin/BUILD.gn index c2ab7152305204..2ccc30ca2f7976 100644 --- a/examples/fabric-sync/admin/BUILD.gn +++ b/examples/fabric-sync/admin/BUILD.gn @@ -15,16 +15,44 @@ import("//build_overrides/chip.gni") import("${chip_root}/src/app/chip_data_model.gni") +config("config") { + include_dirs = [ + ".", + "${chip_root}/examples/fabric-sync", + "${chip_root}/examples/platform/linux", + "${chip_root}/src/lib", + ] +} + source_set("fabric-admin-lib") { + public_configs = [ ":config" ] + sources = [ + "BridgeSubscription.cpp", + "BridgeSubscription.h", + "CommissionerControl.cpp", + "CommissionerControl.h", "DeviceManager.cpp", "DeviceManager.h", + "DeviceSubscription.cpp", + "DeviceSubscription.h", + "DeviceSubscriptionManager.cpp", + "DeviceSubscriptionManager.h", + "DeviceSynchronization.cpp", + "DeviceSynchronization.h", + "FabricAdmin.cpp", + "FabricAdmin.h", + "FabricSyncGetter.cpp", + "FabricSyncGetter.h", "PairingManager.cpp", "PairingManager.h", + "UniqueIdGetter.cpp", + "UniqueIdGetter.h", ] deps = [ "${chip_root}/examples/fabric-sync/bridge:fabric-bridge-lib", + "${chip_root}/examples/platform/linux:app-main", "${chip_root}/src/lib", ] } diff --git a/examples/fabric-sync/admin/BridgeSubscription.cpp b/examples/fabric-sync/admin/BridgeSubscription.cpp new file mode 100644 index 00000000000000..e7052d7df08a96 --- /dev/null +++ b/examples/fabric-sync/admin/BridgeSubscription.cpp @@ -0,0 +1,163 @@ +/* + * 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 "BridgeSubscription.h" +#include "DeviceManager.h" + +using namespace ::chip; +using namespace ::chip::app; +using chip::app::ReadClient; + +namespace admin { + +namespace { + +constexpr uint16_t kSubscribeMinInterval = 0; +constexpr uint16_t kSubscribeMaxInterval = 60; + +void OnDeviceConnectedWrapper(void * context, Messaging::ExchangeManager & exchangeMgr, const SessionHandle & sessionHandle) +{ + reinterpret_cast(context)->OnDeviceConnected(exchangeMgr, sessionHandle); +} + +void OnDeviceConnectionFailureWrapper(void * context, const ScopedNodeId & peerId, CHIP_ERROR error) +{ + reinterpret_cast(context)->OnDeviceConnectionFailure(peerId, error); +} + +} // namespace + +BridgeSubscription::BridgeSubscription() : + mOnDeviceConnectedCallback(OnDeviceConnectedWrapper, this), + mOnDeviceConnectionFailureCallback(OnDeviceConnectionFailureWrapper, this) +{} + +CHIP_ERROR BridgeSubscription::StartSubscription(Controller::DeviceController & controller, NodeId nodeId, EndpointId endpointId) +{ + assertChipStackLockedByCurrentThread(); + + VerifyOrDie(!subscriptionStarted); // Ensure it's not called multiple times. + + // Mark as started + subscriptionStarted = true; + + mEndpointId = endpointId; + + CHIP_ERROR err = controller.GetConnectedDevice(nodeId, &mOnDeviceConnectedCallback, &mOnDeviceConnectionFailureCallback); + if (err != CHIP_NO_ERROR) + { + ChipLogError(NotSpecified, "Failed to connect to remote fabric sync bridge %" CHIP_ERROR_FORMAT, err.Format()); + } + return err; +} + +void BridgeSubscription::OnAttributeData(const ConcreteDataAttributePath & path, TLV::TLVReader * data, const StatusIB & status) +{ + if (!status.IsSuccess()) + { + ChipLogError(NotSpecified, "Response Failure: %" CHIP_ERROR_FORMAT, status.ToChipError().Format()); + return; + } + + if (data == nullptr) + { + ChipLogError(NotSpecified, "Response Failure: No Data"); + return; + } + + DeviceMgr().HandleAttributeData(path, *data); +} + +void BridgeSubscription::OnEventData(const app::EventHeader & eventHeader, TLV::TLVReader * data, const app::StatusIB * status) +{ + if (status != nullptr) + { + CHIP_ERROR error = status->ToChipError(); + if (CHIP_NO_ERROR != error) + { + ChipLogError(NotSpecified, "Response Failure: %" CHIP_ERROR_FORMAT, error.Format()); + return; + } + } + + if (data == nullptr) + { + ChipLogError(NotSpecified, "Response Failure: No Data"); + return; + } + + DeviceMgr().HandleEventData(eventHeader, *data); +} + +void BridgeSubscription::OnError(CHIP_ERROR error) +{ + ChipLogProgress(NotSpecified, "Error on remote fabric sync bridge subscription: %" CHIP_ERROR_FORMAT, error.Format()); +} + +void BridgeSubscription::OnDone(ReadClient * apReadClient) +{ + mClient.reset(); + ChipLogProgress(NotSpecified, "The remote fabric sync bridge subscription is terminated"); + + // Reset the subscription state to allow retry + subscriptionStarted = false; + + // TODO:(#36092) Fabric-Admin should attempt to re-subscribe when the subscription to the remote bridge is terminated. +} + +void BridgeSubscription::OnDeviceConnected(Messaging::ExchangeManager & exchangeMgr, const SessionHandle & sessionHandle) +{ + mClient = std::make_unique(app::InteractionModelEngine::GetInstance(), &exchangeMgr /* echangeMgr */, + *this /* callback */, ReadClient::InteractionType::Subscribe); + VerifyOrDie(mClient); + + AttributePathParams readPaths[1]; + readPaths[0] = AttributePathParams(mEndpointId, Clusters::Descriptor::Id, Clusters::Descriptor::Attributes::PartsList::Id); + + EventPathParams eventPaths[1]; + eventPaths[0] = EventPathParams(mEndpointId, Clusters::CommissionerControl::Id, + Clusters::CommissionerControl::Events::CommissioningRequestResult::Id); + eventPaths[0].mIsUrgentEvent = true; + + ReadPrepareParams readParams(sessionHandle); + + readParams.mpAttributePathParamsList = readPaths; + readParams.mAttributePathParamsListSize = 1; + readParams.mpEventPathParamsList = eventPaths; + readParams.mEventPathParamsListSize = 1; + readParams.mMinIntervalFloorSeconds = kSubscribeMinInterval; + readParams.mMaxIntervalCeilingSeconds = kSubscribeMaxInterval; + readParams.mKeepSubscriptions = true; + + CHIP_ERROR err = mClient->SendRequest(readParams); + + if (err != CHIP_NO_ERROR) + { + ChipLogError(NotSpecified, "Failed to issue subscription to the Descriptor Cluster of the remote bridged device."); + OnDone(nullptr); + return; + } +} + +void BridgeSubscription::OnDeviceConnectionFailure(const ScopedNodeId & peerId, CHIP_ERROR error) +{ + ChipLogError(NotSpecified, "BridgeSubscription failed to connect to " ChipLogFormatX64, ChipLogValueX64(peerId.GetNodeId())); + OnDone(nullptr); +} + +} // namespace admin diff --git a/examples/fabric-sync/admin/BridgeSubscription.h b/examples/fabric-sync/admin/BridgeSubscription.h new file mode 100644 index 00000000000000..e5f8c73fc14257 --- /dev/null +++ b/examples/fabric-sync/admin/BridgeSubscription.h @@ -0,0 +1,81 @@ +/* + * 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 +#include + +#include +#include + +namespace admin { + +/** + * @brief Class used to subscribe to attributes and events from the remote bridged device. + * + * The Descriptor Cluster contains attributes such as the Parts List, which provides a list + * of endpoints or devices that are part of a composite device or bridge. The CommissionerControl + * Cluster generates events related to commissioning requests, which can be monitored to track + * device commissioning status. + * + * When subscribing to attributes and events of a bridged device from another fabric, the class: + * - Establishes a secure session with the device (if needed) via CASE (Chip over + * Authenticated Session Establishment) session. + * - Subscribes to the specified attributes in the Descriptor Cluster (e.g., Parts List) and + * events in the CommissionerControl Cluster (e.g., CommissioningRequestResult) of the remote + * device on the specified node and endpoint. + * - Invokes the provided callback upon successful or unsuccessful subscription, allowing + * further handling of data or errors. + * + * This class also implements the necessary callbacks to handle attribute data reports, event data, + * errors, and session establishment procedures. + */ +class BridgeSubscription : public chip::app::ReadClient::Callback +{ +public: + BridgeSubscription(); + + CHIP_ERROR StartSubscription(chip::Controller::DeviceController & controller, chip::NodeId nodeId, chip::EndpointId endpointId); + + /////////////////////////////////////////////////////////////// + // ReadClient::Callback implementation + /////////////////////////////////////////////////////////////// + void OnAttributeData(const chip::app::ConcreteDataAttributePath & path, chip::TLV::TLVReader * data, + const chip::app::StatusIB & status) override; + void OnEventData(const chip::app::EventHeader & eventHeader, chip::TLV::TLVReader * data, + const chip::app::StatusIB * status) override; + void OnError(CHIP_ERROR error) override; + void OnDone(chip::app::ReadClient * apReadClient) override; + + /////////////////////////////////////////////////////////////// + // callbacks for CASE session establishment + /////////////////////////////////////////////////////////////// + void OnDeviceConnected(chip::Messaging::ExchangeManager & exchangeMgr, const chip::SessionHandle & sessionHandle); + void OnDeviceConnectionFailure(const chip::ScopedNodeId & peerId, CHIP_ERROR error); + +private: + std::unique_ptr mClient; + + chip::Callback::Callback mOnDeviceConnectedCallback; + chip::Callback::Callback mOnDeviceConnectionFailureCallback; + chip::EndpointId mEndpointId; + bool subscriptionStarted = false; +}; + +} // namespace admin diff --git a/examples/fabric-sync/admin/CommissionerControl.cpp b/examples/fabric-sync/admin/CommissionerControl.cpp new file mode 100644 index 00000000000000..45c7bc02da69a5 --- /dev/null +++ b/examples/fabric-sync/admin/CommissionerControl.cpp @@ -0,0 +1,166 @@ +/* + * 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 "CommissionerControl.h" +#include "DeviceManager.h" + +using namespace ::chip; + +namespace admin { + +void CommissionerControl::Init(Controller::DeviceCommissioner & commissioner, NodeId nodeId, EndpointId endpointId) +{ + // Ensure that mCommissioner is not already initialized + VerifyOrDie(mCommissioner == nullptr); + + ChipLogProgress(NotSpecified, "Initilize CommissionerControl"); + mCommissioner = &commissioner; + mDestinationId = nodeId; + mEndpointId = endpointId; +} + +CHIP_ERROR CommissionerControl::RequestCommissioningApproval(uint64_t requestId, uint16_t vendorId, uint16_t productId, + Optional label) +{ + VerifyOrReturnError(mCommissioner != nullptr, CHIP_ERROR_INCORRECT_STATE); + + ChipLogProgress(NotSpecified, "Sending RequestCommissioningApproval to node " ChipLogFormatX64, + ChipLogValueX64(mDestinationId)); + + mRequestCommissioningApproval.requestID = requestId; + mRequestCommissioningApproval.vendorID = static_cast(vendorId); + mRequestCommissioningApproval.productID = productId; + + if (label.HasValue()) + { + VerifyOrReturnError(label.Value().size() <= kMaxDeviceLabelLength, CHIP_ERROR_BUFFER_TOO_SMALL); + memcpy(mLabelBuffer, label.Value().data(), label.Value().size()); + mRequestCommissioningApproval.label = Optional>(CharSpan(mLabelBuffer, label.Value().size())); + } + + mCommandType = CommandType::kRequestCommissioningApproval; + return mCommissioner->GetConnectedDevice(mDestinationId, &mOnDeviceConnectedCallback, &mOnDeviceConnectionFailureCallback); +} + +CHIP_ERROR CommissionerControl::CommissionNode(uint64_t requestId, uint16_t responseTimeoutSeconds) +{ + VerifyOrReturnError(mCommissioner != nullptr, CHIP_ERROR_INCORRECT_STATE); + + ChipLogProgress(NotSpecified, "Sending CommissionNode to node " ChipLogFormatX64, ChipLogValueX64(mDestinationId)); + + mCommissionNode.requestID = requestId; + mCommissionNode.responseTimeoutSeconds = responseTimeoutSeconds; + + mCommandType = CommandType::kCommissionNode; + return mCommissioner->GetConnectedDevice(mDestinationId, &mOnDeviceConnectedCallback, &mOnDeviceConnectionFailureCallback); +} + +void CommissionerControl::OnResponse(app::CommandSender * client, const app::ConcreteCommandPath & path, + const app::StatusIB & status, TLV::TLVReader * data) +{ + ChipLogProgress(NotSpecified, "CommissionerControl: OnResponse."); + + CHIP_ERROR error = status.ToChipError(); + if (CHIP_NO_ERROR != error) + { + ChipLogError(NotSpecified, "Response Failure: %s", ErrorStr(error)); + return; + } + + if (data != nullptr) + { + DeviceMgr().HandleCommandResponse(path, *data); + } +} + +void CommissionerControl::OnError(const app::CommandSender * client, CHIP_ERROR error) +{ + // Handle the error, then reset mCommandSender + ChipLogProgress(NotSpecified, "CommissionerControl: OnError: Error: %s", ErrorStr(error)); +} + +void CommissionerControl::OnDone(app::CommandSender * client) +{ + ChipLogProgress(NotSpecified, "CommissionerControl: OnDone."); + + switch (mCommandType) + { + case CommandType::kRequestCommissioningApproval: + ChipLogProgress(NotSpecified, "CommissionerControl: Command RequestCommissioningApproval has been successfully processed."); + break; + + case CommandType::kCommissionNode: + ChipLogProgress(NotSpecified, "CommissionerControl: Command CommissionNode has been successfully processed."); + break; + + default: + ChipLogError(NotSpecified, "CommissionerControl: Unknown or unhandled command type in OnDone."); + break; + } + + // Reset command type to undefined after processing is done + mCommandType = CommandType::kUndefined; + + // Ensure that mCommandSender is cleaned up after it is done + mCommandSender.reset(); +} + +CHIP_ERROR CommissionerControl::SendCommandForType(CommandType commandType, DeviceProxy * device) +{ + ChipLogProgress(AppServer, "Sending command with Endpoint ID: %d, Command Type: %d", mEndpointId, + static_cast(commandType)); + + switch (commandType) + { + case CommandType::kRequestCommissioningApproval: + return SendCommand(device, mEndpointId, app::Clusters::CommissionerControl::Id, + app::Clusters::CommissionerControl::Commands::RequestCommissioningApproval::Id, + mRequestCommissioningApproval); + case CommandType::kCommissionNode: + return SendCommand(device, mEndpointId, app::Clusters::CommissionerControl::Id, + app::Clusters::CommissionerControl::Commands::CommissionNode::Id, mCommissionNode); + default: + return CHIP_ERROR_INVALID_ARGUMENT; + } +} + +void CommissionerControl::OnDeviceConnectedFn(void * context, Messaging::ExchangeManager & exchangeMgr, + const SessionHandle & sessionHandle) +{ + CommissionerControl * self = reinterpret_cast(context); + VerifyOrReturn(self != nullptr, ChipLogError(NotSpecified, "OnDeviceConnectedFn: context is null")); + + OperationalDeviceProxy device(&exchangeMgr, sessionHandle); + + CHIP_ERROR err = self->SendCommandForType(self->mCommandType, &device); + if (err != CHIP_NO_ERROR) + { + ChipLogError(NotSpecified, "Failed to send CommissionerControl command."); + self->OnDone(nullptr); + } +} + +void CommissionerControl::OnDeviceConnectionFailureFn(void * context, const ScopedNodeId & peerId, CHIP_ERROR err) +{ + LogErrorOnFailure(err); + CommissionerControl * self = reinterpret_cast(context); + VerifyOrReturn(self != nullptr, ChipLogError(NotSpecified, "OnDeviceConnectedFn: context is null")); + self->OnDone(nullptr); +} + +} // namespace admin diff --git a/examples/fabric-sync/admin/CommissionerControl.h b/examples/fabric-sync/admin/CommissionerControl.h new file mode 100644 index 00000000000000..392367d6c04367 --- /dev/null +++ b/examples/fabric-sync/admin/CommissionerControl.h @@ -0,0 +1,129 @@ +/* + * 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 +#include + +namespace admin { + +/** + * @class CommissionerControl + * @brief This class handles sending CHIP commands related to commissioning, including sending + * commissioning approval requests and commissioning nodes. + * + * The class acts as a command sender and implements the `chip::app::CommandSender::Callback` interface + * to handle responses, errors, and completion events for the commands it sends. It relies on external + * CCTRL delegate and server mechanisms to manage the overall protocol and state transitions, including + * processing the CommissioningRequestResult and invoking CommissionNode. + */ +class CommissionerControl : public chip::app::CommandSender::Callback +{ +public: + CommissionerControl() : + mOnDeviceConnectedCallback(OnDeviceConnectedFn, this), mOnDeviceConnectionFailureCallback(OnDeviceConnectionFailureFn, this) + {} + + /** + * @brief Initializes the CommissionerControl with a DeviceCommissioner, NodeId, and EndpointId. + * + * @param commissioner The DeviceCommissioner to use for the commissioning process. + * @param nodeId The node ID of the remote fabric bridge. + * @param endpointId The endpoint on which to send CommissionerControl commands. + */ + void Init(chip::Controller::DeviceCommissioner & commissioner, chip::NodeId nodeId, chip::EndpointId endpointId); + + /** + * @brief Sends a RequestCommissioningApproval command to the device. + * + * @param requestId The unique request ID. + * @param vendorId The vendor ID of the device. + * @param productId The product ID of the device. + * @param label Optional label for the device. + * @return CHIP_ERROR CHIP_NO_ERROR on success, or an appropriate error code on failure. + */ + CHIP_ERROR RequestCommissioningApproval(uint64_t requestId, uint16_t vendorId, uint16_t productId, + chip::Optional label); + /** + * @brief Sends a CommissionNode command to the device. + * + * @param requestId The unique request ID. + * @param responseTimeoutSeconds Timeout for the response in seconds. + * @return CHIP_ERROR CHIP_NO_ERROR on success, or an appropriate error code on failure. + */ + CHIP_ERROR CommissionNode(uint64_t requestId, uint16_t responseTimeoutSeconds); + + /////////// CommandSender Callback Interface ///////// + virtual void OnResponse(chip::app::CommandSender * client, const chip::app::ConcreteCommandPath & path, + const chip::app::StatusIB & status, chip::TLV::TLVReader * data) override; + + virtual void OnError(const chip::app::CommandSender * client, CHIP_ERROR error) override; + + virtual void OnDone(chip::app::CommandSender * client) override; + +private: + static constexpr uint16_t kMaxDeviceLabelLength = 64; + + enum class CommandType : uint8_t + { + kUndefined = 0, + kRequestCommissioningApproval = 1, + kCommissionNode = 2, + }; + + template + CHIP_ERROR SendCommand(chip::DeviceProxy * device, chip::EndpointId endpointId, chip::ClusterId clusterId, + chip::CommandId commandId, const T & value) + { + chip::app::CommandPathParams commandPath = { endpointId, clusterId, commandId, + (chip::app::CommandPathFlags::kEndpointIdValid) }; + mCommandSender = std::make_unique(this, device->GetExchangeManager(), false, false, + device->GetSecureSession().Value()->AllowsLargePayload()); + + VerifyOrReturnError(mCommandSender != nullptr, CHIP_ERROR_NO_MEMORY); + + chip::app::CommandSender::AddRequestDataParameters addRequestDataParams(chip::NullOptional); + ReturnErrorOnFailure(mCommandSender->AddRequestData(commandPath, value, addRequestDataParams)); + ReturnErrorOnFailure(mCommandSender->SendCommandRequest(device->GetSecureSession().Value())); + + return CHIP_NO_ERROR; + } + + CHIP_ERROR SendCommandForType(CommandType commandType, chip::DeviceProxy * device); + + static void OnDeviceConnectedFn(void * context, chip::Messaging::ExchangeManager & exchangeMgr, + const chip::SessionHandle & sessionHandle); + static void OnDeviceConnectionFailureFn(void * context, const chip::ScopedNodeId & peerId, CHIP_ERROR error); + + // Private data members + chip::Controller::DeviceCommissioner * mCommissioner = nullptr; + std::unique_ptr mCommandSender; + chip::NodeId mDestinationId = chip::kUndefinedNodeId; + chip::EndpointId mEndpointId = chip::kRootEndpointId; + CommandType mCommandType = CommandType::kUndefined; + char mLabelBuffer[kMaxDeviceLabelLength]; + + chip::Callback::Callback mOnDeviceConnectedCallback; + chip::Callback::Callback mOnDeviceConnectionFailureCallback; + + chip::app::Clusters::CommissionerControl::Commands::RequestCommissioningApproval::Type mRequestCommissioningApproval; + chip::app::Clusters::CommissionerControl::Commands::CommissionNode::Type mCommissionNode; +}; + +} // namespace admin diff --git a/examples/fabric-sync/admin/DeviceManager.cpp b/examples/fabric-sync/admin/DeviceManager.cpp index 5c41bbb56f7b83..79b47446c3f860 100644 --- a/examples/fabric-sync/admin/DeviceManager.cpp +++ b/examples/fabric-sync/admin/DeviceManager.cpp @@ -1,5 +1,4 @@ /* - * * Copyright (c) 2024 Project CHIP Authors * All rights reserved. * @@ -18,6 +17,9 @@ #include "DeviceManager.h" +#include +#include +#include #include #include @@ -27,9 +29,18 @@ using namespace chip; namespace admin { + +namespace { + +constexpr EndpointId kAggregatorEndpointId = 1; + +} // namespace + // Define the static member DeviceManager DeviceManager::sInstance; +LinuxCommissionableDataProvider sCommissionableDataProvider; + void DeviceManager::Init() { // TODO: (#34113) Init mLastUsedNodeId from chip config file @@ -60,6 +71,68 @@ void DeviceManager::UpdateLastUsedNodeId(NodeId nodeId) void DeviceManager::SetRemoteBridgeNodeId(chip::NodeId nodeId) { mRemoteBridgeNodeId = nodeId; + + if (mRemoteBridgeNodeId != kUndefinedNodeId) + { + mCommissionerControl.Init(PairingManager::Instance().CurrentCommissioner(), mRemoteBridgeNodeId, kAggregatorEndpointId); + } +} + +SyncedDevice * DeviceManager::FindDeviceByEndpoint(EndpointId endpointId) +{ + for (auto & device : mSyncedDevices) + { + if (device.GetEndpointId() == endpointId) + { + return const_cast(&device); + } + } + return nullptr; +} + +SyncedDevice * DeviceManager::FindDeviceByNode(NodeId nodeId) +{ + for (auto & device : mSyncedDevices) + { + if (device.GetNodeId() == nodeId) + { + return const_cast(&device); + } + } + return nullptr; +} + +void DeviceManager::OpenLocalBridgeCommissioningWindow(uint32_t iterations, uint16_t commissioningTimeoutSec, + uint16_t discriminator, const ByteSpan & salt, const ByteSpan & verifier) +{ + ChipLogProgress(NotSpecified, "Opening commissioning window of the local bridge"); + + auto & commissionMgr = Server::GetInstance().GetCommissioningWindowManager(); + auto commissioningTimeout = System::Clock::Seconds16(commissioningTimeoutSec); + + Optional> spake2pVerifier = verifier.empty() + ? Optional>::Missing() + : Optional>(std::vector(verifier.begin(), verifier.end())); + + Optional> spake2pSalt = salt.empty() + ? Optional>::Missing() + : Optional>(std::vector(salt.begin(), salt.end())); + + CHIP_ERROR err = + sCommissionableDataProvider.Init(spake2pVerifier, spake2pSalt, iterations, Optional(), discriminator); + if (err != CHIP_NO_ERROR) + { + ChipLogError(NotSpecified, "Failed to initialize the commissionable data provider of the local bridge: %s", ErrorStr(err)); + return; + } + + DeviceLayer::SetCommissionableDataProvider(&sCommissionableDataProvider); + + err = commissionMgr.OpenBasicCommissioningWindow(commissioningTimeout); + if (err != CHIP_NO_ERROR) + { + ChipLogError(NotSpecified, "Failed to open commissioning window of the local bridge: %s", ErrorStr(err)); + } } CHIP_ERROR DeviceManager::PairRemoteFabricBridge(NodeId nodeId, uint32_t setupPINCode, const char * deviceRemoteIp, @@ -143,4 +216,265 @@ CHIP_ERROR DeviceManager::UnpairRemoteDevice(NodeId nodeId) return CHIP_NO_ERROR; } +void DeviceManager::SubscribeRemoteFabricBridge() +{ + ChipLogProgress(NotSpecified, "Start subscription to the remote bridge."); + + CHIP_ERROR error = mBridgeSubscriber.StartSubscription(PairingManager::Instance().CurrentCommissioner(), mRemoteBridgeNodeId, + kAggregatorEndpointId); + + if (error != CHIP_NO_ERROR) + { + ChipLogError(NotSpecified, "Failed to subscribe to the remote bridge (NodeId: %lu). Error: %" CHIP_ERROR_FORMAT, + mRemoteBridgeNodeId, error.Format()); + return; + } +} + +void DeviceManager::ReadSupportedDeviceCategories() +{ + if (!IsFabricSyncReady()) + { + // print to console + fprintf(stderr, "Remote Fabric Bridge is not configured yet.\n"); + return; + } + + ChipLogProgress(NotSpecified, "Read SupportedDeviceCategories from the remote bridge."); + + CHIP_ERROR error = mFabricSyncGetter.GetFabricSynchronizationData( + [this](TLV::TLVReader & data) { this->HandleReadSupportedDeviceCategories(data); }, + PairingManager::Instance().CurrentCommissioner(), this->GetRemoteBridgeNodeId(), kAggregatorEndpointId); + + if (error != CHIP_NO_ERROR) + { + ChipLogError(NotSpecified, + "Failed to read SupportedDeviceCategories from the remote bridge (NodeId: %lu). Error: %" CHIP_ERROR_FORMAT, + mRemoteBridgeNodeId, error.Format()); + } +} + +void DeviceManager::HandleReadSupportedDeviceCategories(TLV::TLVReader & data) +{ + ChipLogProgress(NotSpecified, "Attribute SupportedDeviceCategories detected."); + + BitMask value; + CHIP_ERROR error = app::DataModel::Decode(data, value); + if (error != CHIP_NO_ERROR) + { + ChipLogError(NotSpecified, "Failed to decode attribute value. Error: %" CHIP_ERROR_FORMAT, error.Format()); + return; + } + + if (value.Has(app::Clusters::CommissionerControl::SupportedDeviceCategoryBitmap::kFabricSynchronization)) + { + ChipLogProgress(NotSpecified, "Remote Fabric-Bridge supports Fabric Synchronization, start reverse commissioning."); + RequestCommissioningApproval(); + } + else + { + ChipLogProgress(NotSpecified, "Remote Fabric-Bridge does not support Fabric Synchronization."); + } +} + +void DeviceManager::RequestCommissioningApproval() +{ + ChipLogProgress(NotSpecified, "Starting reverse commissioning for bridge device: NodeId: " ChipLogFormatX64, + ChipLogValueX64(mRemoteBridgeNodeId)); + + uint64_t requestId = Crypto::GetRandU64(); + uint16_t vendorId = static_cast(CHIP_DEVICE_CONFIG_DEVICE_VENDOR_ID); + uint16_t productId = static_cast(CHIP_DEVICE_CONFIG_DEVICE_PRODUCT_ID); + + CHIP_ERROR error = mCommissionerControl.RequestCommissioningApproval(requestId, vendorId, productId, NullOptional); + + if (error != CHIP_NO_ERROR) + { + ChipLogError(NotSpecified, + "Failed to request commissioning-approval to the remote bridge (NodeId: %lu). Error: %" CHIP_ERROR_FORMAT, + mRemoteBridgeNodeId, error.Format()); + return; + } + + mRequestId = requestId; +} + +void DeviceManager::HandleCommissioningRequestResult(TLV::TLVReader & data) +{ + ChipLogProgress(NotSpecified, "CommissioningRequestResult event received."); + + app::Clusters::CommissionerControl::Events::CommissioningRequestResult::DecodableType value; + CHIP_ERROR error = app::DataModel::Decode(data, value); + if (error != CHIP_NO_ERROR) + { + ChipLogError(NotSpecified, "Failed to decode event value. Error: %" CHIP_ERROR_FORMAT, error.Format()); + return; + } + + if (value.requestID != mRequestId) + { + ChipLogError(NotSpecified, "The RequestId does not match the RequestId provided to RequestCommissioningApproval"); + return; + } + + if (value.statusCode != static_cast(Protocols::InteractionModel::Status::Success)) + { + ChipLogError(NotSpecified, "The server is not ready to begin commissioning the requested device"); + return; + } + + // The server is ready to begin commissioning the requested device, request the Commissioner Control Server to begin + // commissioning a previously approved request. + SendCommissionNodeRequest(value.requestID, kResponseTimeoutSeconds); +} + +void DeviceManager::HandleAttributePartsListUpdate(TLV::TLVReader & data) +{ + ChipLogProgress(NotSpecified, "Attribute PartsList change detected:"); + + app::DataModel::DecodableList value; + CHIP_ERROR error = app::DataModel::Decode(data, value); + if (error != CHIP_NO_ERROR) + { + ChipLogError(NotSpecified, "Failed to decode attribute value. Error: %" CHIP_ERROR_FORMAT, error.Format()); + return; + } + + std::set newEndpoints; + + // Populate the newEndpoints set from the decoded value using an iterator + auto iter = value.begin(); + while (iter.Next()) + { + newEndpoints.insert(iter.GetValue()); + } + + if (iter.GetStatus() != CHIP_NO_ERROR) + { + ChipLogError(NotSpecified, "Failed to iterate over decoded attribute value."); + return; + } + + // Compare newEndpoints with mSyncedDevices to determine added and removed endpoints + std::vector addedEndpoints; + std::vector removedEndpoints; + + // Note: We're using vectors and manual searches instead of set operations + // because we need to work with the SyncedDevice objects in mSyncedDevices, + // not just their EndpointIds. This approach allows us to access the full + // Device information when processing changes. + + // Find added endpoints + for (const auto & endpoint : newEndpoints) + { + if (FindDeviceByEndpoint(endpoint) == nullptr) + { + addedEndpoints.push_back(endpoint); + } + } + + // Find removed endpoints + for (auto & device : mSyncedDevices) + { + EndpointId endpointId = device.GetEndpointId(); + if (newEndpoints.find(endpointId) == newEndpoints.end()) + { + removedEndpoints.push_back(endpointId); + } + } + + // Process added endpoints + for (const auto & endpoint : addedEndpoints) + { + // print to console + fprintf(stderr, "A new device is added on Endpoint: %u\n", endpoint); + } + + // Process removed endpoints + for (const auto & endpoint : removedEndpoints) + { + ChipLogProgress(NotSpecified, "Endpoint removed: %u", endpoint); + + SyncedDevice * device = FindDeviceByEndpoint(endpoint); + + if (device == nullptr) + { + ChipLogProgress(NotSpecified, "No device on Endpoint: %u", endpoint); + continue; + } + } +} + +void DeviceManager::SendCommissionNodeRequest(uint64_t requestId, uint16_t responseTimeoutSeconds) +{ + ChipLogProgress(NotSpecified, "Request the Commissioner Control Server to begin commissioning a previously approved request."); + + CHIP_ERROR error = mCommissionerControl.CommissionNode(requestId, responseTimeoutSeconds); + + if (error != CHIP_NO_ERROR) + { + ChipLogError(NotSpecified, + "Failed to send CommissionNode command to the remote bridge (NodeId: %lu). Error: %" CHIP_ERROR_FORMAT, + mRemoteBridgeNodeId, error.Format()); + return; + } +} + +void DeviceManager::HandleReverseOpenCommissioningWindow(TLV::TLVReader & data) +{ + ChipLogProgress(NotSpecified, "Handle ReverseOpenCommissioningWindow command."); + + app::Clusters::CommissionerControl::Commands::ReverseOpenCommissioningWindow::DecodableType value; + CHIP_ERROR error = app::DataModel::Decode(data, value); + + if (error != CHIP_NO_ERROR) + { + ChipLogError(NotSpecified, "Failed to decode command response value. Error: %" CHIP_ERROR_FORMAT, error.Format()); + return; + } + + // Log all fields + ChipLogProgress(NotSpecified, "DecodableType fields:"); + ChipLogProgress(NotSpecified, " commissioningTimeout: %u", value.commissioningTimeout); + ChipLogProgress(NotSpecified, " discriminator: %u", value.discriminator); + ChipLogProgress(NotSpecified, " iterations: %u", value.iterations); + ChipLogProgress(NotSpecified, " PAKEPasscodeVerifier size: %lu", value.PAKEPasscodeVerifier.size()); + ChipLogProgress(NotSpecified, " salt size: %lu", value.salt.size()); + + OpenLocalBridgeCommissioningWindow(value.iterations, value.commissioningTimeout, value.discriminator, + ByteSpan(value.salt.data(), value.salt.size()), + ByteSpan(value.PAKEPasscodeVerifier.data(), value.PAKEPasscodeVerifier.size())); +} + +void DeviceManager::HandleAttributeData(const app::ConcreteDataAttributePath & path, TLV::TLVReader & data) +{ + if (path.mClusterId == app::Clusters::Descriptor::Id && + path.mAttributeId == app::Clusters::Descriptor::Attributes::PartsList::Id) + { + HandleAttributePartsListUpdate(data); + return; + } +} + +void DeviceManager::HandleEventData(const app::EventHeader & header, TLV::TLVReader & data) +{ + if (header.mPath.mClusterId == app::Clusters::CommissionerControl::Id && + header.mPath.mEventId == app::Clusters::CommissionerControl::Events::CommissioningRequestResult::Id) + { + HandleCommissioningRequestResult(data); + } +} + +void DeviceManager::HandleCommandResponse(const app::ConcreteCommandPath & path, TLV::TLVReader & data) +{ + ChipLogProgress(NotSpecified, "Command Response received."); + + if (path.mClusterId == app::Clusters::CommissionerControl::Id && + path.mCommandId == app::Clusters::CommissionerControl::Commands::ReverseOpenCommissioningWindow::Id) + { + VerifyOrDie(path.mEndpointId == kAggregatorEndpointId); + HandleReverseOpenCommissioningWindow(data); + } +} + } // namespace admin diff --git a/examples/fabric-sync/admin/DeviceManager.h b/examples/fabric-sync/admin/DeviceManager.h index db6ad9b528ade4..b92154c1e6e008 100644 --- a/examples/fabric-sync/admin/DeviceManager.h +++ b/examples/fabric-sync/admin/DeviceManager.h @@ -1,5 +1,4 @@ /* - * * Copyright (c) 2024 Project CHIP Authors * All rights reserved. * @@ -18,13 +17,38 @@ #pragma once +#include "BridgeSubscription.h" +#include "CommissionerControl.h" +#include "FabricSyncGetter.h" #include "PairingManager.h" #include #include +#include namespace admin { +constexpr uint32_t kDefaultSetupPinCode = 20202021; +constexpr uint16_t kResponseTimeoutSeconds = 30; + +class SyncedDevice +{ +public: + SyncedDevice(chip::NodeId nodeId, chip::EndpointId endpointId) : mNodeId(nodeId), mEndpointId(endpointId) {} + + chip::NodeId GetNodeId() const { return mNodeId; } + chip::EndpointId GetEndpointId() const { return mEndpointId; } + + bool operator<(const SyncedDevice & other) const + { + return mNodeId < other.mNodeId || (mNodeId == other.mNodeId && mEndpointId < other.mEndpointId); + } + +private: + chip::NodeId mNodeId; + chip::EndpointId mEndpointId; +}; + class DeviceManager { public: @@ -51,6 +75,22 @@ class DeviceManager */ bool IsCurrentBridgeDevice(chip::NodeId nodeId) const { return nodeId == mRemoteBridgeNodeId; } + /** + * @brief Open the commissioning window of the local bridge. + * + * @param iterations The number of PBKDF (Password-Based Key Derivation Function) iterations to use + * for deriving the PAKE (Password Authenticated Key Exchange) verifier. + * @param commissioningTimeoutSec The time in seconds before the commissioning window closes. This value determines + * how long the commissioning window remains open for incoming connections. + * @param discriminator The device-specific discriminator, determined during commissioning, which helps + * to uniquely identify the device among others. + * @param salt The salt used in the cryptographic operations for commissioning. + * @param verifier The PAKE verifier used to authenticate the commissioning process. + * + */ + void OpenLocalBridgeCommissioningWindow(uint32_t iterations, uint16_t commissioningTimeoutSec, uint16_t discriminator, + const chip::ByteSpan & salt, const chip::ByteSpan & verifier); + /** * @brief Pair a remote fabric bridge with a given node ID. * @@ -116,9 +156,34 @@ class DeviceManager */ CHIP_ERROR UnpairRemoteDevice(chip::NodeId nodeId); + void SubscribeRemoteFabricBridge(); + + void ReadSupportedDeviceCategories(); + + void HandleAttributeData(const chip::app::ConcreteDataAttributePath & path, chip::TLV::TLVReader & data); + + void HandleEventData(const chip::app::EventHeader & header, chip::TLV::TLVReader & data); + + void HandleCommandResponse(const chip::app::ConcreteCommandPath & path, chip::TLV::TLVReader & data); + + SyncedDevice * FindDeviceByEndpoint(chip::EndpointId endpointId); + SyncedDevice * FindDeviceByNode(chip::NodeId nodeId); + private: friend DeviceManager & DeviceMgr(); + void RequestCommissioningApproval(); + + void HandleReadSupportedDeviceCategories(chip::TLV::TLVReader & data); + + void HandleCommissioningRequestResult(chip::TLV::TLVReader & data); + + void HandleAttributePartsListUpdate(chip::TLV::TLVReader & data); + + void SendCommissionNodeRequest(uint64_t requestId, uint16_t responseTimeoutSeconds); + + void HandleReverseOpenCommissioningWindow(chip::TLV::TLVReader & data); + static DeviceManager sInstance; chip::NodeId mLastUsedNodeId = 0; @@ -127,7 +192,13 @@ class DeviceManager // This represents the bridge on the other ecosystem. chip::NodeId mRemoteBridgeNodeId = chip::kUndefinedNodeId; - bool mInitialized = false; + std::set mSyncedDevices; + bool mInitialized = false; + uint64_t mRequestId = 0; + + BridgeSubscription mBridgeSubscriber; + CommissionerControl mCommissionerControl; + FabricSyncGetter mFabricSyncGetter; }; /** diff --git a/examples/fabric-sync/admin/DeviceSubscription.cpp b/examples/fabric-sync/admin/DeviceSubscription.cpp new file mode 100644 index 00000000000000..9379df6910f84a --- /dev/null +++ b/examples/fabric-sync/admin/DeviceSubscription.cpp @@ -0,0 +1,280 @@ +/* + * 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 "DeviceSubscription.h" +#include "DeviceManager.h" + +#include +#include +#include +#include + +using namespace ::chip; +using namespace ::chip::app; +using chip::app::ReadClient; + +namespace admin { + +namespace { + +void OnDeviceConnectedWrapper(void * context, Messaging::ExchangeManager & exchangeMgr, const SessionHandle & sessionHandle) +{ + reinterpret_cast(context)->OnDeviceConnected(exchangeMgr, sessionHandle); +} + +void OnDeviceConnectionFailureWrapper(void * context, const ScopedNodeId & peerId, CHIP_ERROR error) +{ + reinterpret_cast(context)->OnDeviceConnectionFailure(peerId, error); +} + +} // namespace + +DeviceSubscription::DeviceSubscription() : + mOnDeviceConnectedCallback(OnDeviceConnectedWrapper, this), + mOnDeviceConnectionFailureCallback(OnDeviceConnectionFailureWrapper, this) +{} + +void DeviceSubscription::OnAttributeData(const ConcreteDataAttributePath & path, TLV::TLVReader * data, const StatusIB & status) +{ + VerifyOrDie(path.mEndpointId == kRootEndpointId); + VerifyOrDie(path.mClusterId == Clusters::AdministratorCommissioning::Id); + + switch (path.mAttributeId) + { + case Clusters::AdministratorCommissioning::Attributes::WindowStatus::Id: { + Clusters::AdministratorCommissioning::CommissioningWindowStatusEnum windowStatus; + CHIP_ERROR err = data->Get(windowStatus); + VerifyOrReturn(err == CHIP_NO_ERROR, ChipLogError(NotSpecified, "Failed to read WindowStatus")); + VerifyOrReturn(windowStatus != Clusters::AdministratorCommissioning::CommissioningWindowStatusEnum::kUnknownEnumValue); + mCurrentAdministratorCommissioningAttributes.windowStatus = windowStatus; + mChangeDetected = true; + break; + } + case Clusters::AdministratorCommissioning::Attributes::AdminFabricIndex::Id: { + FabricIndex fabricIndex; + CHIP_ERROR err = data->Get(fabricIndex); + if (err == CHIP_NO_ERROR) + { + mCurrentAdministratorCommissioningAttributes.openerFabricIndex = fabricIndex; + } + else + { + mCurrentAdministratorCommissioningAttributes.openerFabricIndex.reset(); + } + + mChangeDetected = true; + break; + } + case Clusters::AdministratorCommissioning::Attributes::AdminVendorId::Id: { + VendorId vendorId; + CHIP_ERROR err = data->Get(vendorId); + if (err == CHIP_NO_ERROR) + { + mCurrentAdministratorCommissioningAttributes.openerVendorId = vendorId; + } + else + { + mCurrentAdministratorCommissioningAttributes.openerVendorId.reset(); + } + + mChangeDetected = true; + break; + } + default: + break; + } +} + +void DeviceSubscription::OnReportEnd() +{ + // Report end is at the end of all attributes (success) + if (mChangeDetected) + { + CHIP_ERROR err = + bridge::FabricBridge::Instance().AdminCommissioningAttributeChanged(mCurrentAdministratorCommissioningAttributes); + if (err != CHIP_NO_ERROR) + { + ChipLogError(NotSpecified, "Cannot forward Administrator Commissioning Attribute to fabric bridge %" CHIP_ERROR_FORMAT, + err.Format()); + } + mChangeDetected = false; + } +} + +void DeviceSubscription::OnDone(ReadClient * apReadClient) +{ + // After calling mOnDoneCallback we are indicating that `this` is deleted and we shouldn't do anything else with + // DeviceSubscription. + MoveToState(State::AwaitingDestruction); + mOnDoneCallback(mScopedNodeId); +} + +void DeviceSubscription::OnError(CHIP_ERROR error) +{ + if (error == CHIP_ERROR_TIMEOUT && mState == State::SubscriptionStarted) + { + if (bridge::FabricBridge::Instance().DeviceReachableChanged(mCurrentAdministratorCommissioningAttributes.id, false) != + CHIP_NO_ERROR) + { + ChipLogError(NotSpecified, "Failed to update the device reachability state"); + } + } + + ChipLogProgress(NotSpecified, "Error subscribing: %" CHIP_ERROR_FORMAT, error.Format()); +} + +void DeviceSubscription::OnDeviceConnected(Messaging::ExchangeManager & exchangeMgr, const SessionHandle & sessionHandle) +{ + if (mState == State::Stopping) + { + // After calling mOnDoneCallback we are indicating that `this` is deleted and we shouldn't do anything else with + // DeviceSubscription. + MoveToState(State::AwaitingDestruction); + mOnDoneCallback(mScopedNodeId); + return; + } + VerifyOrDie(mState == State::Connecting); + mClient = std::make_unique(app::InteractionModelEngine::GetInstance(), &exchangeMgr /* echangeMgr */, + *this /* callback */, ReadClient::InteractionType::Subscribe); + VerifyOrDie(mClient); + + AttributePathParams readPaths[1]; + readPaths[0] = AttributePathParams(kRootEndpointId, Clusters::AdministratorCommissioning::Id); + + ReadPrepareParams readParams(sessionHandle); + + readParams.mpAttributePathParamsList = readPaths; + readParams.mAttributePathParamsListSize = 1; + readParams.mMaxIntervalCeilingSeconds = 5 * 60; + + CHIP_ERROR err = mClient->SendRequest(readParams); + + if (err != CHIP_NO_ERROR) + { + ChipLogError(NotSpecified, "Failed to issue subscription to AdministratorCommissioning data"); + // After calling mOnDoneCallback we are indicating that `this` is deleted and we shouldn't do anything else with + // DeviceSubscription. + MoveToState(State::AwaitingDestruction); + mOnDoneCallback(mScopedNodeId); + return; + } + MoveToState(State::SubscriptionStarted); +} + +void DeviceSubscription::MoveToState(const State aTargetState) +{ + mState = aTargetState; + ChipLogDetail(NotSpecified, "DeviceSubscription moving to [%10.10s]", GetStateStr()); +} + +const char * DeviceSubscription::GetStateStr() const +{ + switch (mState) + { + case State::Idle: + return "Idle"; + + case State::Connecting: + return "Connecting"; + + case State::Stopping: + return "Stopping"; + + case State::SubscriptionStarted: + return "SubscriptionStarted"; + + case State::AwaitingDestruction: + return "AwaitingDestruction"; + } + return "N/A"; +} + +void DeviceSubscription::OnDeviceConnectionFailure(const ScopedNodeId & peerId, CHIP_ERROR error) +{ + VerifyOrDie(mState == State::Connecting || mState == State::Stopping); + ChipLogError(NotSpecified, "DeviceSubscription failed to connect to " ChipLogFormatX64, ChipLogValueX64(peerId.GetNodeId())); + + if (mState == State::Connecting) + { + if (bridge::FabricBridge::Instance().DeviceReachableChanged(mCurrentAdministratorCommissioningAttributes.id, false) != + CHIP_NO_ERROR) + { + ChipLogError(NotSpecified, "Failed to update the device reachability state"); + } + } + + // After calling mOnDoneCallback we are indicating that `this` is deleted and we shouldn't do anything else with + // DeviceSubscription. + MoveToState(State::AwaitingDestruction); + mOnDoneCallback(mScopedNodeId); +} + +CHIP_ERROR DeviceSubscription::StartSubscription(OnDoneCallback onDoneCallback, Controller::DeviceController & controller, + ScopedNodeId scopedNodeId) +{ + assertChipStackLockedByCurrentThread(); + VerifyOrDie(mState == State::Idle); + VerifyOrReturnError(controller.GetFabricIndex() == scopedNodeId.GetFabricIndex(), CHIP_ERROR_INVALID_ARGUMENT); + + mScopedNodeId = scopedNodeId; + + mCurrentAdministratorCommissioningAttributes = AdministratorCommissioningChanged_init_default; + mCurrentAdministratorCommissioningAttributes.id = scopedNodeId; + mCurrentAdministratorCommissioningAttributes.windowStatus = + Clusters::AdministratorCommissioning::CommissioningWindowStatusEnum::kWindowNotOpen; + + mOnDoneCallback = onDoneCallback; + MoveToState(State::Connecting); + CHIP_ERROR err = + controller.GetConnectedDevice(scopedNodeId.GetNodeId(), &mOnDeviceConnectedCallback, &mOnDeviceConnectionFailureCallback); + if (err != CHIP_NO_ERROR) + { + MoveToState(State::Idle); + } + return err; +} + +void DeviceSubscription::StopSubscription() +{ + assertChipStackLockedByCurrentThread(); + VerifyOrDie(mState != State::Idle); + // Something is seriously wrong if we die on the line below + VerifyOrDie(mState != State::AwaitingDestruction); + + if (mState == State::Stopping) + { + // Stop is called again while we are still waiting on connected callbacks + return; + } + + if (mState == State::Connecting) + { + MoveToState(State::Stopping); + return; + } + + // By calling reset on our ReadClient we terminate the subscription. + VerifyOrDie(mClient); + mClient.reset(); + // After calling mOnDoneCallback we are indicating that `this` is deleted and we shouldn't do anything else with + // DeviceSubscription. + MoveToState(State::AwaitingDestruction); + mOnDoneCallback(mScopedNodeId); +} + +} // namespace admin diff --git a/examples/fabric-sync/admin/DeviceSubscription.h b/examples/fabric-sync/admin/DeviceSubscription.h new file mode 100644 index 00000000000000..59169223e6fbb7 --- /dev/null +++ b/examples/fabric-sync/admin/DeviceSubscription.h @@ -0,0 +1,94 @@ +/* + * 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 +#include +#include +#include +#include + +namespace admin { + +class DeviceSubscriptionManager; + +/// Attribute subscription to attributes that are important to keep track and send to fabric-bridge +/// via RPC when change has been identified. +/// +/// An instance of DeviceSubscription is intended to be used only once. Once a DeviceSubscription is +/// terminated, either from an error or from subscriptions getting shut down, we expect the instance +/// to be deleted. Any new subscription should instantiate another instance of DeviceSubscription. +class DeviceSubscription : public chip::app::ReadClient::Callback +{ +public: + using OnDoneCallback = std::function; + + DeviceSubscription(); + + CHIP_ERROR StartSubscription(OnDoneCallback onDoneCallback, chip::Controller::DeviceController & controller, + chip::ScopedNodeId nodeId); + + /// This will trigger stopping the subscription. Once subscription is stopped the OnDoneCallback + /// provided in StartSubscription will be called to indicate that subscription have been terminated. + /// + /// Must only be called after StartSubscription was successfully called. + void StopSubscription(); + + /////////////////////////////////////////////////////////////// + // ReadClient::Callback implementation + /////////////////////////////////////////////////////////////// + void OnAttributeData(const chip::app::ConcreteDataAttributePath & path, chip::TLV::TLVReader * data, + const chip::app::StatusIB & status) override; + void OnReportEnd() override; + void OnError(CHIP_ERROR error) override; + void OnDone(chip::app::ReadClient * apReadClient) override; + + /////////////////////////////////////////////////////////////// + // callbacks for CASE session establishment + /////////////////////////////////////////////////////////////// + void OnDeviceConnected(chip::Messaging::ExchangeManager & exchangeMgr, const chip::SessionHandle & sessionHandle); + void OnDeviceConnectionFailure(const chip::ScopedNodeId & peerId, CHIP_ERROR error); + +private: + enum class State : uint8_t + { + Idle, ///< Default state that the object starts out in, where no work has commenced + Connecting, ///< We are waiting for OnDeviceConnected or OnDeviceConnectionFailure callbacks to be called + Stopping, ///< We are waiting for OnDeviceConnected or OnDeviceConnectionFailure callbacks so we can terminate + SubscriptionStarted, ///< We have started a subscription. + AwaitingDestruction, ///< The object has completed its work and is awaiting destruction. + }; + + void MoveToState(const State aTargetState); + const char * GetStateStr() const; + + chip::ScopedNodeId mScopedNodeId; + + OnDoneCallback mOnDoneCallback; + std::unique_ptr mClient; + + chip::Callback::Callback mOnDeviceConnectedCallback; + chip::Callback::Callback mOnDeviceConnectionFailureCallback; + + bridge::AdministratorCommissioningChanged mCurrentAdministratorCommissioningAttributes; + + bool mChangeDetected = false; + State mState = State::Idle; +}; + +} // namespace admin diff --git a/examples/fabric-sync/admin/DeviceSubscriptionManager.cpp b/examples/fabric-sync/admin/DeviceSubscriptionManager.cpp new file mode 100644 index 00000000000000..d2209bdd5d52ba --- /dev/null +++ b/examples/fabric-sync/admin/DeviceSubscriptionManager.cpp @@ -0,0 +1,79 @@ +/* + * 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 "DeviceSubscriptionManager.h" +#include "DeviceManager.h" + +#include +#include +#include +#include + +using namespace ::chip; +using namespace ::chip::app; + +namespace admin { + +DeviceSubscriptionManager & DeviceSubscriptionManager::Instance() +{ + static DeviceSubscriptionManager instance; + return instance; +} + +CHIP_ERROR DeviceSubscriptionManager::StartSubscription(Controller::DeviceController & controller, ScopedNodeId scopedNodeId) +{ + assertChipStackLockedByCurrentThread(); + auto it = mDeviceSubscriptionMap.find(scopedNodeId); + VerifyOrReturnError((it == mDeviceSubscriptionMap.end()), CHIP_ERROR_INCORRECT_STATE); + + auto deviceSubscription = std::make_unique(); + VerifyOrReturnError(deviceSubscription, CHIP_ERROR_NO_MEMORY); + ReturnErrorOnFailure(deviceSubscription->StartSubscription( + [this](ScopedNodeId aNodeId) { this->DeviceSubscriptionTerminated(aNodeId); }, controller, scopedNodeId)); + + mDeviceSubscriptionMap[scopedNodeId] = std::move(deviceSubscription); + return CHIP_NO_ERROR; +} + +CHIP_ERROR DeviceSubscriptionManager::RemoveSubscription(ScopedNodeId scopedNodeId) +{ + assertChipStackLockedByCurrentThread(); + auto it = mDeviceSubscriptionMap.find(scopedNodeId); + VerifyOrReturnError((it != mDeviceSubscriptionMap.end()), CHIP_ERROR_NOT_FOUND); + // We cannot safely erase the DeviceSubscription from mDeviceSubscriptionMap. + // After calling StopSubscription we expect DeviceSubscription to eventually + // call the OnDoneCallback we provided in StartSubscription which will call + // DeviceSubscriptionTerminated where it will be erased from the + // mDeviceSubscriptionMap. + it->second->StopSubscription(); + return CHIP_NO_ERROR; +} + +void DeviceSubscriptionManager::DeviceSubscriptionTerminated(ScopedNodeId scopedNodeId) +{ + assertChipStackLockedByCurrentThread(); + auto it = mDeviceSubscriptionMap.find(scopedNodeId); + // DeviceSubscriptionTerminated is a private method that is expected to only + // be called by DeviceSubscription when it is terminal and is ready to be + // cleaned up and removed. If it is not mapped that means something has gone + // really wrong and there is likely a memory leak somewhere. + VerifyOrDie(it != mDeviceSubscriptionMap.end()); + mDeviceSubscriptionMap.erase(scopedNodeId); +} + +} // namespace admin diff --git a/examples/fabric-sync/admin/DeviceSubscriptionManager.h b/examples/fabric-sync/admin/DeviceSubscriptionManager.h new file mode 100644 index 00000000000000..eb32d3f439276d --- /dev/null +++ b/examples/fabric-sync/admin/DeviceSubscriptionManager.h @@ -0,0 +1,58 @@ +/* + * 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 "DeviceSubscription.h" + +#include +#include +#include + +#include + +namespace admin { + +class DeviceSubscriptionManager +{ +public: + static DeviceSubscriptionManager & Instance(); + + /// Usually called after we have added a synchronized device to fabric-bridge to monitor + /// for any changes that need to be propagated to fabric-bridge. + CHIP_ERROR StartSubscription(chip::Controller::DeviceController & controller, chip::ScopedNodeId scopedNodeId); + + CHIP_ERROR RemoveSubscription(chip::ScopedNodeId scopedNodeId); + +private: + struct ScopedNodeIdHasher + { + std::size_t operator()(const chip::ScopedNodeId & scopedNodeId) const + { + std::size_t h1 = std::hash{}(scopedNodeId.GetFabricIndex()); + std::size_t h2 = std::hash{}(scopedNodeId.GetNodeId()); + // Bitshifting h2 reduces collisions when fabricIndex == nodeId. + return h1 ^ (h2 << 1); + } + }; + + void DeviceSubscriptionTerminated(chip::ScopedNodeId scopedNodeId); + + std::unordered_map, ScopedNodeIdHasher> mDeviceSubscriptionMap; +}; + +} // namespace admin diff --git a/examples/fabric-sync/admin/DeviceSynchronization.cpp b/examples/fabric-sync/admin/DeviceSynchronization.cpp new file mode 100644 index 00000000000000..2f36eb9cb03f69 --- /dev/null +++ b/examples/fabric-sync/admin/DeviceSynchronization.cpp @@ -0,0 +1,323 @@ +/* + * 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 "DeviceSynchronization.h" +#include "DeviceManager.h" +#include "DeviceSubscriptionManager.h" + +#include +#include +#include +#include + +using namespace ::chip; +using namespace ::chip::app; +using chip::app::ReadClient; + +namespace admin { + +namespace { + +constexpr uint16_t kBasicInformationAttributeBufSize = 128; + +void OnDeviceConnectedWrapper(void * context, Messaging::ExchangeManager & exchangeMgr, const SessionHandle & sessionHandle) +{ + reinterpret_cast(context)->OnDeviceConnected(exchangeMgr, sessionHandle); +} + +void OnDeviceConnectionFailureWrapper(void * context, const ScopedNodeId & peerId, CHIP_ERROR error) +{ + reinterpret_cast(context)->OnDeviceConnectionFailure(peerId, error); +} + +bool SuccessOrLog(CHIP_ERROR err, const char * name) +{ + if (err == CHIP_NO_ERROR) + { + return true; + } + + ChipLogError(NotSpecified, "Failed to read %s: %" CHIP_ERROR_FORMAT, name, err.Format()); + + return false; +} + +} // namespace + +DeviceSynchronizer & DeviceSynchronizer::Instance() +{ + static DeviceSynchronizer instance; + return instance; +} + +DeviceSynchronizer::DeviceSynchronizer() : + mOnDeviceConnectedCallback(OnDeviceConnectedWrapper, this), + mOnDeviceConnectionFailureCallback(OnDeviceConnectionFailureWrapper, this) +{} + +void DeviceSynchronizer::OnAttributeData(const ConcreteDataAttributePath & path, TLV::TLVReader * data, const StatusIB & status) +{ + VerifyOrDie(path.mEndpointId == kRootEndpointId); + VerifyOrDie(path.mClusterId == Clusters::BasicInformation::Id); + + if (!status.IsSuccess()) + { + ChipLogError(NotSpecified, "Response Failure: %" CHIP_ERROR_FORMAT, status.ToChipError().Format()); + return; + } + + switch (path.mAttributeId) + { + case Clusters::BasicInformation::Attributes::UniqueID::Id: { + char uniqueIdBuffer[kBasicInformationAttributeBufSize]; + if (SuccessOrLog(data->GetString(uniqueIdBuffer, sizeof(uniqueIdBuffer)), "UniqueId")) + { + mCurrentDeviceData.uniqueId = std::string(uniqueIdBuffer); + } + } + break; + case Clusters::BasicInformation::Attributes::VendorName::Id: { + char vendorNameBuffer[kBasicInformationAttributeBufSize]; + if (SuccessOrLog(data->GetString(vendorNameBuffer, sizeof(vendorNameBuffer)), "VendorName")) + { + mCurrentDeviceData.vendorName = std::string(vendorNameBuffer); + } + } + break; + case Clusters::BasicInformation::Attributes::ProductName::Id: { + char productNameBuffer[kBasicInformationAttributeBufSize]; + if (SuccessOrLog(data->GetString(productNameBuffer, sizeof(productNameBuffer)), "ProductName")) + { + mCurrentDeviceData.productName = std::string(productNameBuffer); + } + } + break; + case Clusters::BasicInformation::Attributes::NodeLabel::Id: { + char nodeLabelBuffer[kBasicInformationAttributeBufSize]; + if (SuccessOrLog(data->GetString(nodeLabelBuffer, sizeof(nodeLabelBuffer)), "NodeLabel")) + { + mCurrentDeviceData.nodeLabel = std::string(nodeLabelBuffer); + } + } + break; + case Clusters::BasicInformation::Attributes::HardwareVersionString::Id: { + char hardwareVersionStringBuffer[kBasicInformationAttributeBufSize]; + if (SuccessOrLog(data->GetString(hardwareVersionStringBuffer, sizeof(hardwareVersionStringBuffer)), + "HardwareVersionString")) + { + mCurrentDeviceData.hardwareVersionString = std::string(hardwareVersionStringBuffer); + } + } + break; + case Clusters::BasicInformation::Attributes::SoftwareVersionString::Id: { + char softwareVersionStringBuffer[kBasicInformationAttributeBufSize]; + if (SuccessOrLog(data->GetString(softwareVersionStringBuffer, sizeof(softwareVersionStringBuffer)), + "SoftwareVersionString")) + { + mCurrentDeviceData.softwareVersionString = std::string(softwareVersionStringBuffer); + } + } + break; + default: + break; + } +} + +void DeviceSynchronizer::OnReportEnd() +{ + // Report end is at the end of all attributes (success) + MoveToState(State::ReceivedResponse); +} + +void DeviceSynchronizer::OnDone(app::ReadClient * apReadClient) +{ + ChipLogProgress(NotSpecified, "Synchronization complete for NodeId:" ChipLogFormatX64, ChipLogValueX64(mNodeId)); + + if (mState == State::ReceivedResponse && !DeviceMgr().IsCurrentBridgeDevice(mNodeId)) + { + GetUniqueId(); + if (mState == State::GettingUid) + { + ChipLogProgress(NotSpecified, + "GetUniqueId was successful and we rely on callback to call SynchronizationCompleteAddDevice."); + return; + } + SynchronizationCompleteAddDevice(); + } + + MoveToState(State::Idle); +} + +void DeviceSynchronizer::OnError(CHIP_ERROR error) +{ + MoveToState(State::ReceivedError); + ChipLogProgress(NotSpecified, "Error fetching device data: %" CHIP_ERROR_FORMAT, error.Format()); +} + +void DeviceSynchronizer::OnDeviceConnected(Messaging::ExchangeManager & exchangeMgr, const SessionHandle & sessionHandle) +{ + mClient = std::make_unique(app::InteractionModelEngine::GetInstance(), &exchangeMgr /* echangeMgr */, + *this /* callback */, ReadClient::InteractionType::Read); + VerifyOrDie(mClient); + + AttributePathParams readPaths[1]; + readPaths[0] = AttributePathParams(kRootEndpointId, Clusters::BasicInformation::Id); + + ReadPrepareParams readParams(sessionHandle); + + readParams.mpAttributePathParamsList = readPaths; + readParams.mAttributePathParamsListSize = 1; + + CHIP_ERROR err = mClient->SendRequest(readParams); + + if (err != CHIP_NO_ERROR) + { + ChipLogError(NotSpecified, "Failed to issue read for BasicInformation data"); + MoveToState(State::Idle); + } + MoveToState(State::AwaitingResponse); +} + +void DeviceSynchronizer::OnDeviceConnectionFailure(const ScopedNodeId & peerId, CHIP_ERROR error) +{ + ChipLogError(NotSpecified, "Device Sync failed to connect to " ChipLogFormatX64, ChipLogValueX64(peerId.GetNodeId())); + MoveToState(State::Idle); +} + +void DeviceSynchronizer::StartDeviceSynchronization(Controller::DeviceController * controller, NodeId nodeId, bool deviceIsIcd) +{ + VerifyOrDie(controller); + if (mState != State::Idle) + { + ChipLogError(NotSpecified, "Device Sync NOT POSSIBLE: another sync is in progress"); + return; + } + + mNodeId = nodeId; + + ChipLogProgress(NotSpecified, "Start device synchronization for NodeId:" ChipLogFormatX64, ChipLogValueX64(mNodeId)); + + mCurrentDeviceData = SynchronizedDevice_init_default; + mCurrentDeviceData.id = chip::ScopedNodeId(nodeId, controller->GetFabricIndex()); + mCurrentDeviceData.isIcd = deviceIsIcd; + + ReturnOnFailure(controller->GetConnectedDevice(nodeId, &mOnDeviceConnectedCallback, &mOnDeviceConnectionFailureCallback)); + mController = controller; + MoveToState(State::Connecting); +} + +void DeviceSynchronizer::GetUniqueId() +{ + VerifyOrDie(mState == State::ReceivedResponse); + VerifyOrDie(mController); + + // If we have a UniqueId we can return leaving state in ReceivedResponse. + VerifyOrReturn(!mCurrentDeviceData.uniqueId.has_value(), ChipLogDetail(NotSpecified, "We already have UniqueId")); + + auto * device = DeviceMgr().FindDeviceByNode(mNodeId); + // If there is no associated remote Fabric Sync Aggregator there is no other place for us to try + // getting the UniqueId from and can return leaving the state in ReceivedResponse. + VerifyOrReturn(device, ChipLogDetail(NotSpecified, "No remote Fabric Sync Aggregator to get UniqueId from")); + + // Because device is not-null we expect IsFabricSyncReady to be true. IsFabricSyncReady indicates we have a + // connection to the remote Fabric Sync Aggregator. + VerifyOrDie(DeviceMgr().IsFabricSyncReady()); + auto remoteBridgeNodeId = DeviceMgr().GetRemoteBridgeNodeId(); + EndpointId remoteEndpointIdOfInterest = device->GetEndpointId(); + + ChipLogDetail(NotSpecified, "Attempting to get UniqueId from remote Fabric Sync Aggregator"); + CHIP_ERROR err = mUniqueIdGetter.GetUniqueId( + [this](std::optional aUniqueId) { + if (aUniqueId.has_value()) + { + // Convert CharSpan to std::string and set it as uniqueId + this->mCurrentDeviceData.uniqueId = std::string(aUniqueId.value().data(), aUniqueId.value().size()); + } + else + { + ChipLogError(NotSpecified, "We expected to get UniqueId from remote Fabric Sync Aggregator, but failed"); + } + this->SynchronizationCompleteAddDevice(); + }, + *mController, remoteBridgeNodeId, remoteEndpointIdOfInterest); + + if (err == CHIP_NO_ERROR) + { + MoveToState(State::GettingUid); + } + else + { + ChipLogDetail(NotSpecified, "Failed to get UniqueId from remote Fabric Sync Aggregator") + } +} + +void DeviceSynchronizer::SynchronizationCompleteAddDevice() +{ + VerifyOrDie(mState == State::ReceivedResponse || mState == State::GettingUid); + ChipLogProgress(NotSpecified, "Synchronization complete and add device"); + + bridge::FabricBridge::Instance().AddSynchronizedDevice(mCurrentDeviceData); + + // TODO(#35077) Figure out how we should reflect CADMIN values of ICD. + if (!mCurrentDeviceData.isIcd) + { + VerifyOrDie(mController); + ScopedNodeId scopedNodeId(mNodeId, mController->GetFabricIndex()); + CHIP_ERROR err = DeviceSubscriptionManager::Instance().StartSubscription(*mController, scopedNodeId); + if (err != CHIP_NO_ERROR) + { + ChipLogError(NotSpecified, "Failed start subscription to NodeId:" ChipLogFormatX64, ChipLogValueX64(mNodeId)); + bridge::FabricBridge::Instance().DeviceReachableChanged(mCurrentDeviceData.id, false); + } + } + + MoveToState(State::Idle); +} + +void DeviceSynchronizer::MoveToState(const State targetState) +{ + mState = targetState; + ChipLogDetail(NotSpecified, "DeviceSynchronizer moving to [%10.10s]", GetStateStr()); +} + +const char * DeviceSynchronizer::GetStateStr() const +{ + switch (mState) + { + case State::Idle: + return "Idle"; + + case State::Connecting: + return "Connecting"; + + case State::AwaitingResponse: + return "AwaitingResponse"; + + case State::ReceivedResponse: + return "ReceivedResponse"; + + case State::ReceivedError: + return "ReceivedError"; + + case State::GettingUid: + return "GettingUid"; + } + return "N/A"; +} + +} // namespace admin diff --git a/examples/fabric-sync/admin/DeviceSynchronization.h b/examples/fabric-sync/admin/DeviceSynchronization.h new file mode 100644 index 00000000000000..1a72a447993b9d --- /dev/null +++ b/examples/fabric-sync/admin/DeviceSynchronization.h @@ -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. + * + */ +#pragma once + +#include "UniqueIdGetter.h" + +#include +#include +#include +#include +#include + +namespace admin { + +/// Ensures that device data is synchronized to the remote fabric bridge. +/// +/// Includes a state machine that: +/// - initiates a "read basic information data" command to fetch basic information +/// - upon receiving such information, ensures that synchronized device data is sent +/// to the remote end. +class DeviceSynchronizer : public chip::app::ReadClient::Callback +{ +public: + DeviceSynchronizer(); + + /// Usually called after commissioning is complete, initiates a + /// read of required data from the remote node ID and then will synchronize + /// the device towards the fabric bridge + /// + /// @param controller Must be a non-null pointer. The DeviceController instance + /// pointed to must out live the entire device synchronization process. + /// @param nodeId Node ID of the device we need to syncronize data from. + /// @param deviceIsIcd If the device is an ICD device. + void StartDeviceSynchronization(chip::Controller::DeviceController * controller, chip::NodeId nodeId, bool deviceIsIcd); + + /////////////////////////////////////////////////////////////// + // ReadClient::Callback implementation + /////////////////////////////////////////////////////////////// + void OnAttributeData(const chip::app::ConcreteDataAttributePath & path, chip::TLV::TLVReader * data, + const chip::app::StatusIB & status) override; + void OnReportEnd() override; + void OnError(CHIP_ERROR error) override; + void OnDone(chip::app::ReadClient * apReadClient) override; + + /////////////////////////////////////////////////////////////// + // callbacks for CASE session establishment + /////////////////////////////////////////////////////////////// + void OnDeviceConnected(chip::Messaging::ExchangeManager & exchangeMgr, const chip::SessionHandle & sessionHandle); + void OnDeviceConnectionFailure(const chip::ScopedNodeId & peerId, CHIP_ERROR error); + + static DeviceSynchronizer & Instance(); + +private: + enum class State : uint8_t + { + Idle, ///< Default state that the object starts out in, where no work has commenced + Connecting, ///< We are waiting for OnDeviceConnected or OnDeviceConnectionFailure callbacks to be called + AwaitingResponse, ///< We have started reading BasicInformation cluster attributes + ReceivedResponse, ///< We have received a ReportEnd from reading BasicInformation cluster attributes + ReceivedError, ///< We recieved an error while reading of BasicInformation cluster attributes + GettingUid, ///< We are getting UniqueId from the remote fabric sync bridge. + }; + + void GetUniqueId(); + void SynchronizationCompleteAddDevice(); + + void MoveToState(const State targetState); + const char * GetStateStr() const; + + std::unique_ptr mClient; + + chip::Callback::Callback mOnDeviceConnectedCallback; + chip::Callback::Callback mOnDeviceConnectionFailureCallback; + + State mState = State::Idle; + // mController is expected to remain valid throughout the entire device synchronization process (i.e. when + // mState != Idle). + chip::Controller::DeviceController * mController = nullptr; + chip::NodeId mNodeId = chip::kUndefinedNodeId; + bridge::SynchronizedDevice mCurrentDeviceData; + UniqueIdGetter mUniqueIdGetter; +}; + +} // namespace admin diff --git a/examples/fabric-sync/admin/FabricAdmin.cpp b/examples/fabric-sync/admin/FabricAdmin.cpp new file mode 100644 index 00000000000000..a012ac9d8c9a95 --- /dev/null +++ b/examples/fabric-sync/admin/FabricAdmin.cpp @@ -0,0 +1,117 @@ +/* + * 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 "FabricAdmin.h" + +using namespace ::chip; + +namespace admin { + +namespace { + +constexpr uint32_t kCommissionPrepareTimeMs = 500; + +} // namespace + +FabricAdmin FabricAdmin::sInstance; + +FabricAdmin & FabricAdmin::Instance() +{ + if (!sInstance.mInitialized) + { + sInstance.Init(); + } + return sInstance; +} + +CHIP_ERROR +FabricAdmin::CommissionRemoteBridge(Controller::CommissioningWindowPasscodeParams params, VendorId vendorId, uint16_t productId) +{ + char saltHex[Crypto::kSpake2p_Max_PBKDF_Salt_Length * 2 + 1]; + Encoding::BytesToHex(params.GetSalt().data(), params.GetSalt().size(), saltHex, sizeof(saltHex), + Encoding::HexFlags::kNullTerminate); + + ChipLogProgress(NotSpecified, "Received CommissionNode request"); + + SetupPayload setupPayload = SetupPayload(); + + setupPayload.setUpPINCode = params.GetSetupPIN(); + setupPayload.version = 0; + setupPayload.vendorID = vendorId; + setupPayload.productID = productId; + setupPayload.rendezvousInformation.SetValue(RendezvousInformationFlag::kOnNetwork); + + SetupDiscriminator discriminator{}; + discriminator.SetLongValue(params.GetDiscriminator()); + setupPayload.discriminator = discriminator; + + QRCodeSetupPayloadGenerator generator(setupPayload); + std::string code; + CHIP_ERROR err = generator.payloadBase38RepresentationWithAutoTLVBuffer(code); + + if (err == CHIP_NO_ERROR) + { + NodeId nodeId = DeviceMgr().GetNextAvailableNodeId(); + + // After responding with RequestCommissioningApproval to the node where the client initiated the + // RequestCommissioningApproval, you need to wait for it to open a commissioning window on its bridge. + usleep(kCommissionPrepareTimeMs * 1000); + + DeviceMgr().PairRemoteDevice(nodeId, code.c_str()); + } + else + { + ChipLogError(NotSpecified, "Unable to generate pairing code for setup payload: %" CHIP_ERROR_FORMAT, err.Format()); + return err; + } + + return CHIP_NO_ERROR; +} + +CHIP_ERROR FabricAdmin::KeepActive(ScopedNodeId scopedNodeId, uint32_t stayActiveDurationMs, uint32_t timeoutMs) +{ + ChipLogProgress(NotSpecified, "Received KeepActive request: Id[%d, 0x" ChipLogFormatX64 "], %u", scopedNodeId.GetFabricIndex(), + ChipLogValueX64(scopedNodeId.GetNodeId()), stayActiveDurationMs); + + KeepActiveWorkData * data = Platform::New(this, scopedNodeId, stayActiveDurationMs, timeoutMs); + VerifyOrReturnError(data != nullptr, CHIP_ERROR_NO_MEMORY); + + DeviceLayer::PlatformMgr().ScheduleWork(KeepActiveWork, reinterpret_cast(data)); + return CHIP_NO_ERROR; +} + +void FabricAdmin::ScheduleSendingKeepActiveOnCheckIn(ScopedNodeId scopedNodeId, uint32_t stayActiveDurationMs, uint32_t timeoutMs) +{ + // Accessing mPendingCheckIn should only be done while holding ChipStackLock + assertChipStackLockedByCurrentThread(); + + auto timeNow = System::SystemClock().GetMonotonicTimestamp(); + System::Clock::Timestamp expiryTimestamp = timeNow + System::Clock::Milliseconds64(timeoutMs); + KeepActiveDataForCheckIn checkInData = { .mStayActiveDurationMs = stayActiveDurationMs, + .mRequestExpiryTimestamp = expiryTimestamp }; + + auto it = mPendingCheckIn.find(scopedNodeId); + if (it != mPendingCheckIn.end()) + { + checkInData.mStayActiveDurationMs = std::max(checkInData.mStayActiveDurationMs, it->second.mStayActiveDurationMs); + checkInData.mRequestExpiryTimestamp = std::max(checkInData.mRequestExpiryTimestamp, it->second.mRequestExpiryTimestamp); + } + + mPendingCheckIn[scopedNodeId] = checkInData; +} + +} // namespace admin diff --git a/examples/fabric-sync/admin/FabricAdmin.h b/examples/fabric-sync/admin/FabricAdmin.h new file mode 100644 index 00000000000000..1219e594fa8c5f --- /dev/null +++ b/examples/fabric-sync/admin/FabricAdmin.h @@ -0,0 +1,93 @@ +/* + * 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 "DeviceManager.h" + +#include +#include +#include +#include + +namespace admin { + +struct ScopedNodeIdHasher +{ + std::size_t operator()(const chip::ScopedNodeId & scopedNodeId) const + { + std::size_t h1 = std::hash{}(scopedNodeId.GetFabricIndex()); + std::size_t h2 = std::hash{}(scopedNodeId.GetNodeId()); + // Bitshifting h2 reduces collisions when fabricIndex == nodeId. + return h1 ^ (h2 << 1); + } +}; + +class FabricAdmin final : public bridge::FabricAdminDelegate +{ +public: + static FabricAdmin & Instance(); + + CHIP_ERROR + CommissionRemoteBridge(chip::Controller::CommissioningWindowPasscodeParams params, chip::VendorId vendorId, + uint16_t productId) override; + + CHIP_ERROR KeepActive(chip::ScopedNodeId scopedNodeId, uint32_t stayActiveDurationMs, uint32_t timeoutMs) override; + + void ScheduleSendingKeepActiveOnCheckIn(chip::ScopedNodeId scopedNodeId, uint32_t stayActiveDurationMs, uint32_t timeoutMs); + +private: + struct KeepActiveDataForCheckIn + { + uint32_t mStayActiveDurationMs = 0; + chip::System::Clock::Timestamp mRequestExpiryTimestamp; + }; + + struct KeepActiveWorkData + { + KeepActiveWorkData(FabricAdmin * fabricAdmin, chip::ScopedNodeId scopedNodeId, uint32_t stayActiveDurationMs, + uint32_t timeoutMs) : + mFabricAdmin(fabricAdmin), + mScopedNodeId(scopedNodeId), mStayActiveDurationMs(stayActiveDurationMs), mTimeoutMs(timeoutMs) + {} + + FabricAdmin * mFabricAdmin; + chip::ScopedNodeId mScopedNodeId; + uint32_t mStayActiveDurationMs; + uint32_t mTimeoutMs; + }; + + static void KeepActiveWork(intptr_t arg) + { + KeepActiveWorkData * data = reinterpret_cast(arg); + data->mFabricAdmin->ScheduleSendingKeepActiveOnCheckIn(data->mScopedNodeId, data->mStayActiveDurationMs, data->mTimeoutMs); + chip::Platform::Delete(data); + } + + // Modifications to mPendingCheckIn should be done on the MatterEventLoop thread + // otherwise we would need a mutex protecting this data to prevent race as this + // data is accessible by both RPC thread and Matter eventloop. + std::unordered_map mPendingCheckIn; + + static FabricAdmin sInstance; + + bool mInitialized = false; + + void Init() { mInitialized = true; } +}; + +} // namespace admin diff --git a/examples/fabric-sync/admin/FabricSyncGetter.cpp b/examples/fabric-sync/admin/FabricSyncGetter.cpp new file mode 100644 index 00000000000000..7e5118090716c0 --- /dev/null +++ b/examples/fabric-sync/admin/FabricSyncGetter.cpp @@ -0,0 +1,125 @@ +/* + * 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 "FabricSyncGetter.h" + +using namespace ::chip; +using namespace ::chip::app; +using chip::app::ReadClient; + +namespace admin { + +namespace { + +void OnDeviceConnectedWrapper(void * context, Messaging::ExchangeManager & exchangeMgr, const SessionHandle & sessionHandle) +{ + reinterpret_cast(context)->OnDeviceConnected(exchangeMgr, sessionHandle); +} + +void OnDeviceConnectionFailureWrapper(void * context, const ScopedNodeId & peerId, CHIP_ERROR error) +{ + reinterpret_cast(context)->OnDeviceConnectionFailure(peerId, error); +} + +} // namespace + +FabricSyncGetter::FabricSyncGetter() : + mOnDeviceConnectedCallback(OnDeviceConnectedWrapper, this), + mOnDeviceConnectionFailureCallback(OnDeviceConnectionFailureWrapper, this) +{} + +CHIP_ERROR FabricSyncGetter::GetFabricSynchronizationData(OnDoneCallback onDoneCallback, Controller::DeviceController & controller, + NodeId nodeId, EndpointId endpointId) +{ + assertChipStackLockedByCurrentThread(); + + mEndpointId = endpointId; + mOnDoneCallback = onDoneCallback; + + CHIP_ERROR err = controller.GetConnectedDevice(nodeId, &mOnDeviceConnectedCallback, &mOnDeviceConnectionFailureCallback); + if (err != CHIP_NO_ERROR) + { + ChipLogError(NotSpecified, "Failed to connect to remote fabric bridge %" CHIP_ERROR_FORMAT, err.Format()); + } + return err; +} + +void FabricSyncGetter::OnAttributeData(const ConcreteDataAttributePath & path, TLV::TLVReader * data, const StatusIB & status) +{ + VerifyOrDie(path.mClusterId == Clusters::CommissionerControl::Id); + + if (!status.IsSuccess()) + { + ChipLogError(NotSpecified, "Response Failure: %" CHIP_ERROR_FORMAT, status.ToChipError().Format()); + return; + } + + switch (path.mAttributeId) + { + case Clusters::CommissionerControl::Attributes::SupportedDeviceCategories::Id: { + mOnDoneCallback(*data); + break; + } + default: + break; + } +} + +void FabricSyncGetter::OnError(CHIP_ERROR error) +{ + ChipLogProgress(NotSpecified, "Error Getting SupportedDeviceCategories: %" CHIP_ERROR_FORMAT, error.Format()); +} + +void FabricSyncGetter::OnDone(ReadClient * apReadClient) +{ + ChipLogProgress(NotSpecified, "Reading SupportedDeviceCategories is done."); +} + +void FabricSyncGetter::OnDeviceConnected(Messaging::ExchangeManager & exchangeMgr, const SessionHandle & sessionHandle) +{ + mClient = std::make_unique(app::InteractionModelEngine::GetInstance(), &exchangeMgr, *this /* callback */, + ReadClient::InteractionType::Read); + VerifyOrDie(mClient); + + AttributePathParams readPaths[1]; + readPaths[0] = AttributePathParams(mEndpointId, Clusters::CommissionerControl::Id, + Clusters::CommissionerControl::Attributes::SupportedDeviceCategories::Id); + + ReadPrepareParams readParams(sessionHandle); + + readParams.mpAttributePathParamsList = readPaths; + readParams.mAttributePathParamsListSize = 1; + + CHIP_ERROR err = mClient->SendRequest(readParams); + + if (err != CHIP_NO_ERROR) + { + ChipLogError(NotSpecified, "Failed to read SupportedDeviceCategories from the bridged device."); + OnDone(nullptr); + return; + } +} + +void FabricSyncGetter::OnDeviceConnectionFailure(const ScopedNodeId & peerId, CHIP_ERROR error) +{ + ChipLogError(NotSpecified, "FabricSyncGetter failed to connect to " ChipLogFormatX64, ChipLogValueX64(peerId.GetNodeId())); + + OnDone(nullptr); +} + +} // namespace admin diff --git a/examples/fabric-sync/admin/FabricSyncGetter.h b/examples/fabric-sync/admin/FabricSyncGetter.h new file mode 100644 index 00000000000000..bed3c2fbb54385 --- /dev/null +++ b/examples/fabric-sync/admin/FabricSyncGetter.h @@ -0,0 +1,79 @@ +/* + * 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 +#include + +#include +#include + +namespace admin { + +/** + * @brief Class used to get FabricSynchronization from SupportedDeviceCategories attribute of Commissioner Control Cluster. + * + * Functionality: + * - Establishes a CASE session to communicate with the remote bridge. + * - Retrieves the attribute data from the endpoint which host Aggregator. + * - Provides callbacks for success, error, and completion when retrieving data. + */ +class FabricSyncGetter : public chip::app::ReadClient::Callback +{ +public: + using OnDoneCallback = std::function; + + FabricSyncGetter(); + + /** + * @brief Initiates the process of retrieving fabric synchronization data from the target device. + * + * @param onDoneCallback A callback function to be invoked when the data retrieval is complete. + * @param controller The device controller used to establish a session with the target device. + * @param nodeId The Node ID of the target device. + * @param endpointId The Endpoint ID from which to retrieve the fabric synchronization data. + * @return CHIP_ERROR Returns an error if the process fails, CHIP_NO_ERROR on success. + */ + CHIP_ERROR GetFabricSynchronizationData(OnDoneCallback onDoneCallback, chip::Controller::DeviceController & controller, + chip::NodeId nodeId, chip::EndpointId endpointId); + + /////////////////////////////////////////////////////////////// + // ReadClient::Callback implementation + /////////////////////////////////////////////////////////////// + void OnAttributeData(const chip::app::ConcreteDataAttributePath & path, chip::TLV::TLVReader * data, + const chip::app::StatusIB & status) override; + void OnError(CHIP_ERROR error) override; + void OnDone(chip::app::ReadClient * apReadClient) override; + + /////////////////////////////////////////////////////////////// + // callbacks for CASE session establishment + /////////////////////////////////////////////////////////////// + void OnDeviceConnected(chip::Messaging::ExchangeManager & exchangeMgr, const chip::SessionHandle & sessionHandle); + void OnDeviceConnectionFailure(const chip::ScopedNodeId & peerId, CHIP_ERROR error); + +private: + std::unique_ptr mClient; + + OnDoneCallback mOnDoneCallback; + chip::EndpointId mEndpointId; + chip::Callback::Callback mOnDeviceConnectedCallback; + chip::Callback::Callback mOnDeviceConnectionFailureCallback; +}; + +} // namespace admin diff --git a/examples/fabric-sync/admin/PairingManager.cpp b/examples/fabric-sync/admin/PairingManager.cpp index a3f4af43dafd84..ba360051a27b84 100644 --- a/examples/fabric-sync/admin/PairingManager.cpp +++ b/examples/fabric-sync/admin/PairingManager.cpp @@ -1,5 +1,4 @@ /* - * * Copyright (c) 2024 Project CHIP Authors * All rights reserved. * @@ -17,6 +16,8 @@ */ #include "PairingManager.h" +#include "DeviceManager.h" +#include "DeviceSynchronization.h" #include #include @@ -284,6 +285,10 @@ void PairingManager::OnCommissioningComplete(NodeId nodeId, CHIP_ERROR err) { // print to console fprintf(stderr, "New device with Node ID: " ChipLogFormatX64 " has been successfully added.\n", ChipLogValueX64(nodeId)); + + // mCommissioner has a lifetime that is the entire life of the application itself + // so it is safe to provide to StartDeviceSynchronization. + DeviceSynchronizer::Instance().StartDeviceSynchronization(mCommissioner, nodeId, false); } else { diff --git a/examples/fabric-sync/admin/PairingManager.h b/examples/fabric-sync/admin/PairingManager.h index b9b404ee76bbfd..9d109911e195df 100644 --- a/examples/fabric-sync/admin/PairingManager.h +++ b/examples/fabric-sync/admin/PairingManager.h @@ -1,5 +1,4 @@ /* - * * Copyright (c) 2024 Project CHIP Authors * All rights reserved. * diff --git a/examples/fabric-sync/admin/UniqueIdGetter.cpp b/examples/fabric-sync/admin/UniqueIdGetter.cpp new file mode 100644 index 00000000000000..cc3567deda6bd2 --- /dev/null +++ b/examples/fabric-sync/admin/UniqueIdGetter.cpp @@ -0,0 +1,145 @@ +/* + * 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 "UniqueIdGetter.h" + +using namespace ::chip; +using namespace ::chip::app; +using chip::app::ReadClient; + +namespace admin { + +namespace { + +void OnDeviceConnectedWrapper(void * context, Messaging::ExchangeManager & exchangeMgr, const SessionHandle & sessionHandle) +{ + reinterpret_cast(context)->OnDeviceConnected(exchangeMgr, sessionHandle); +} + +void OnDeviceConnectionFailureWrapper(void * context, const ScopedNodeId & peerId, CHIP_ERROR error) +{ + reinterpret_cast(context)->OnDeviceConnectionFailure(peerId, error); +} + +bool SuccessOrLog(CHIP_ERROR err, const char * name) +{ + if (err == CHIP_NO_ERROR) + { + return true; + } + + ChipLogError(NotSpecified, "Failed to read %s: %" CHIP_ERROR_FORMAT, name, err.Format()); + + return false; +} + +} // namespace + +UniqueIdGetter::UniqueIdGetter() : + mOnDeviceConnectedCallback(OnDeviceConnectedWrapper, this), + mOnDeviceConnectionFailureCallback(OnDeviceConnectionFailureWrapper, this) +{} + +CHIP_ERROR UniqueIdGetter::GetUniqueId(OnDoneCallback onDoneCallback, Controller::DeviceController & controller, NodeId nodeId, + EndpointId endpointId) +{ + assertChipStackLockedByCurrentThread(); + VerifyOrDie(!mCurrentlyGettingUid); + + mEndpointId = endpointId; + mOnDoneCallback = onDoneCallback; + mUniqueIdHasValue = false; + memset(mUniqueId, 0, sizeof(mUniqueId)); + mCurrentlyGettingUid = true; + + CHIP_ERROR err = controller.GetConnectedDevice(nodeId, &mOnDeviceConnectedCallback, &mOnDeviceConnectionFailureCallback); + if (err != CHIP_NO_ERROR) + { + ChipLogError(NotSpecified, "Failed to connect to remote fabric sync bridge %" CHIP_ERROR_FORMAT, err.Format()); + mCurrentlyGettingUid = false; + } + return err; +} + +void UniqueIdGetter::OnAttributeData(const ConcreteDataAttributePath & path, TLV::TLVReader * data, const StatusIB & status) +{ + VerifyOrDie(path.mClusterId == Clusters::BridgedDeviceBasicInformation::Id); + + if (!status.IsSuccess()) + { + ChipLogError(NotSpecified, "Response Failure: %" CHIP_ERROR_FORMAT, status.ToChipError().Format()); + return; + } + + switch (path.mAttributeId) + { + case Clusters::BridgedDeviceBasicInformation::Attributes::UniqueID::Id: { + mUniqueIdHasValue = SuccessOrLog(data->GetString(mUniqueId, sizeof(mUniqueId)), "UniqueId"); + break; + } + default: + break; + } +} + +void UniqueIdGetter::OnError(CHIP_ERROR error) +{ + ChipLogProgress(NotSpecified, "Error Getting UID: %" CHIP_ERROR_FORMAT, error.Format()); +} + +void UniqueIdGetter::OnDone(ReadClient * apReadClient) +{ + mCurrentlyGettingUid = false; + mOnDoneCallback(mUniqueIdHasValue ? std::make_optional(mUniqueId) : std::nullopt); +} + +void UniqueIdGetter::OnDeviceConnected(Messaging::ExchangeManager & exchangeMgr, const SessionHandle & sessionHandle) +{ + VerifyOrDie(mCurrentlyGettingUid); + mClient = std::make_unique(app::InteractionModelEngine::GetInstance(), &exchangeMgr, *this /* callback */, + ReadClient::InteractionType::Read); + VerifyOrDie(mClient); + + AttributePathParams readPaths[1]; + readPaths[0] = AttributePathParams(mEndpointId, Clusters::BridgedDeviceBasicInformation::Id, + Clusters::BridgedDeviceBasicInformation::Attributes::UniqueID::Id); + + ReadPrepareParams readParams(sessionHandle); + + readParams.mpAttributePathParamsList = readPaths; + readParams.mAttributePathParamsListSize = 1; + + CHIP_ERROR err = mClient->SendRequest(readParams); + + if (err != CHIP_NO_ERROR) + { + ChipLogError(NotSpecified, "Failed to read unique ID from the bridged device."); + OnDone(nullptr); + return; + } +} + +void UniqueIdGetter::OnDeviceConnectionFailure(const ScopedNodeId & peerId, CHIP_ERROR error) +{ + VerifyOrDie(mCurrentlyGettingUid); + ChipLogError(NotSpecified, "UniqueIdGetter failed to connect to " ChipLogFormatX64, ChipLogValueX64(peerId.GetNodeId())); + + OnDone(nullptr); +} + +} // namespace admin diff --git a/examples/fabric-sync/admin/UniqueIdGetter.h b/examples/fabric-sync/admin/UniqueIdGetter.h new file mode 100644 index 00000000000000..eba4451599c3ee --- /dev/null +++ b/examples/fabric-sync/admin/UniqueIdGetter.h @@ -0,0 +1,79 @@ +/* + * 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 +#include + +#include +#include + +namespace admin { + +/** + * @brief Class used to get UniqueID from Bridged Device Basic Information Cluster + * + * When syncing a device from another fabric that does not have a UniqueID, spec + * dictates: + * When a Fabric Synchronizing Administrator commissions a Synchronized Device, + * it SHALL persist and maintain an association with the UniqueID in the Bridged + * Device Basic Information Cluster exposed by another Fabric Synchronizing + * Administrator. + * + * This class assists in retrieving the UniqueId in the above situation. + */ +class UniqueIdGetter : public chip::app::ReadClient::Callback +{ +public: + using OnDoneCallback = std::function)>; + + UniqueIdGetter(); + + CHIP_ERROR GetUniqueId(OnDoneCallback onDoneCallback, chip::Controller::DeviceController & controller, chip::NodeId nodeId, + chip::EndpointId endpointId); + + /////////////////////////////////////////////////////////////// + // ReadClient::Callback implementation + /////////////////////////////////////////////////////////////// + void OnAttributeData(const chip::app::ConcreteDataAttributePath & path, chip::TLV::TLVReader * data, + const chip::app::StatusIB & status) override; + void OnError(CHIP_ERROR error) override; + void OnDone(chip::app::ReadClient * apReadClient) override; + + /////////////////////////////////////////////////////////////// + // callbacks for CASE session establishment + /////////////////////////////////////////////////////////////// + void OnDeviceConnected(chip::Messaging::ExchangeManager & exchangeMgr, const chip::SessionHandle & sessionHandle); + void OnDeviceConnectionFailure(const chip::ScopedNodeId & peerId, CHIP_ERROR error); + +private: + std::unique_ptr mClient; + + OnDoneCallback mOnDoneCallback; + + chip::Callback::Callback mOnDeviceConnectedCallback; + chip::Callback::Callback mOnDeviceConnectionFailureCallback; + + bool mCurrentlyGettingUid = false; + bool mUniqueIdHasValue = false; + char mUniqueId[33]; + chip::EndpointId mEndpointId; +}; + +} // namespace admin diff --git a/examples/fabric-sync/bridge/BUILD.gn b/examples/fabric-sync/bridge/BUILD.gn index 310496bc54d06a..8fb79b89f2ea3b 100644 --- a/examples/fabric-sync/bridge/BUILD.gn +++ b/examples/fabric-sync/bridge/BUILD.gn @@ -17,7 +17,11 @@ import("${chip_root}/src/app/chip_data_model.gni") import("${chip_root}/src/lib/lib.gni") config("config") { - include_dirs = [ "include" ] + include_dirs = [ + ".", + "${chip_root}/examples/common", + "${chip_root}/examples/platform/linux", + ] } chip_data_model("fabric-bridge-zap") { @@ -41,15 +45,22 @@ source_set("fabric-bridge-lib") { public_configs = [ ":config" ] sources = [ + "include/Bridge.h", "include/BridgedAdministratorCommissioning.h", "include/BridgedDevice.h", "include/BridgedDeviceBasicInformationImpl.h", "include/BridgedDeviceManager.h", "include/CHIPProjectAppConfig.h", + "include/CommissionerControlDelegate.h", + "include/FabricAdminDelegate.h", + "include/FabricBridge.h", + "src/Bridge.cpp", "src/BridgedAdministratorCommissioning.cpp", "src/BridgedDevice.cpp", "src/BridgedDeviceBasicInformationImpl.cpp", "src/BridgedDeviceManager.cpp", + "src/CommissionerControlDelegate.cpp", + "src/FabricBridge.cpp", ] deps = [ diff --git a/examples/fabric-sync/bridge/include/Bridge.h b/examples/fabric-sync/bridge/include/Bridge.h new file mode 100644 index 00000000000000..d6ff91ededc4ec --- /dev/null +++ b/examples/fabric-sync/bridge/include/Bridge.h @@ -0,0 +1,54 @@ +/* + * 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 "CommissionableInit.h" +#include "CommissionerControlDelegate.h" +#include "FabricAdminDelegate.h" + +namespace bridge { + +/** + * @brief Initializes the local fabric bridge system. + * + * This function sets up and initializes all necessary components required for + * device management and commissioning within the local fabric bridge. It interacts + * with the provided FabricAdminDelegate instance to manage fabric-level operations. + * Specifically, it registers command handlers, initializes device attributes, and + * prepares the bridge for commissioning operations. + * + * @param delegate A pointer to a FabricAdminDelegate instance, allowing the bridge + * to initialize and manage fabric operations. + * + * @return CHIP_NO_ERROR if all initializations are successful, or an appropriate + * CHIP_ERROR code if an initialization step fails. + */ +CHIP_ERROR BridgeInit(FabricAdminDelegate * delegate); + +/** + * @brief Shuts down the local fabric bridge system. + * + * This function performs cleanup operations and shuts down the bridge system components + * responsible for device management and commissioning. It stops the Commissioner Control + * Server and handles any errors that may occur during shutdown. + * + * @return CHIP_NO_ERROR if shutdown is successful, or an appropriate CHIP_ERROR code if any component fails to shut down. + */ +CHIP_ERROR BridgeShutdown(); + +} // namespace bridge diff --git a/examples/fabric-sync/bridge/include/BridgedDevice.h b/examples/fabric-sync/bridge/include/BridgedDevice.h index ed5c9710624225..903d853ec8dce2 100644 --- a/examples/fabric-sync/bridge/include/BridgedDevice.h +++ b/examples/fabric-sync/bridge/include/BridgedDevice.h @@ -1,5 +1,4 @@ /* - * * Copyright (c) 2024 Project CHIP Authors * All rights reserved. * diff --git a/examples/fabric-sync/bridge/include/BridgedDeviceManager.h b/examples/fabric-sync/bridge/include/BridgedDeviceManager.h index 6d172767662a69..be6c6a41db262d 100644 --- a/examples/fabric-sync/bridge/include/BridgedDeviceManager.h +++ b/examples/fabric-sync/bridge/include/BridgedDeviceManager.h @@ -1,5 +1,4 @@ /* - * * Copyright (c) 2024 Project CHIP Authors * All rights reserved. * diff --git a/examples/fabric-sync/bridge/include/CHIPProjectAppConfig.h b/examples/fabric-sync/bridge/include/CHIPProjectAppConfig.h index 58f45c03052b17..ff6052169b488d 100644 --- a/examples/fabric-sync/bridge/include/CHIPProjectAppConfig.h +++ b/examples/fabric-sync/bridge/include/CHIPProjectAppConfig.h @@ -1,5 +1,4 @@ /* - * * Copyright (c) 2024 Project CHIP Authors * All rights reserved. * diff --git a/examples/fabric-sync/bridge/include/CommissionerControlDelegate.h b/examples/fabric-sync/bridge/include/CommissionerControlDelegate.h new file mode 100644 index 00000000000000..45a927307bb9a7 --- /dev/null +++ b/examples/fabric-sync/bridge/include/CommissionerControlDelegate.h @@ -0,0 +1,102 @@ +/* + * 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 "FabricAdminDelegate.h" + +#include +#include +#include + +namespace chip { +namespace app { +namespace Clusters { +namespace CommissionerControl { + +inline constexpr EndpointId kAggregatorEndpointId = 1; + +class CommissionerControlDelegate : public Delegate +{ +public: + CommissionerControlDelegate(bridge::FabricAdminDelegate * fabricAdmin) : mFabricAdmin(fabricAdmin) {} + + CHIP_ERROR HandleCommissioningApprovalRequest(const CommissioningApprovalRequest & request) override; + // TODO(#35627) clientNodeId should move towards ScopedNodeId. + CHIP_ERROR ValidateCommissionNodeCommand(NodeId clientNodeId, uint64_t requestId) override; + CHIP_ERROR GetCommissioningWindowParams(CommissioningWindowParams & outParams) override; + CHIP_ERROR HandleCommissionNode(const CommissioningWindowParams & params) override; + + ~CommissionerControlDelegate() = default; + +private: + enum class Step : uint8_t + { + // Ready to start reverse commissioning. + kIdle, + // Wait for the commission node command. + kWaitCommissionNodeRequest, + // Need to commission node. + kStartCommissionNode, + }; + + static const char * GetStateString(Step step) + { + switch (step) + { + case Step::kIdle: + return "kIdle"; + case Step::kWaitCommissionNodeRequest: + return "kWaitCommissionNodeRequest"; + case Step::kStartCommissionNode: + return "kStartCommissionNode"; + default: + return "Unknown"; + } + } + + void ResetDelegateState(); + + static constexpr size_t kLabelBufferSize = 64; + + Step mNextStep = Step::kIdle; + uint64_t mRequestId = 0; + NodeId mClientNodeId = kUndefinedNodeId; + VendorId mVendorId = VendorId::Unspecified; + uint16_t mProductId = 0; + char mLabelBuffer[kLabelBufferSize + 1]; + Optional mLabel; + + // Parameters needed for non-basic commissioning. + uint8_t mPBKDFSaltBuffer[Crypto::kSpake2p_Max_PBKDF_Salt_Length]; + ByteSpan mPBKDFSalt; + Crypto::Spake2pVerifierSerialized mPAKEPasscodeVerifierBuffer; + ByteSpan mPAKEPasscodeVerifier; + + bridge::FabricAdminDelegate * mFabricAdmin; +}; + +} // namespace CommissionerControl +} // namespace Clusters +} // namespace app +} // namespace chip + +namespace bridge { + +CHIP_ERROR CommissionerControlInit(bridge::FabricAdminDelegate * fabricAdmin); +CHIP_ERROR CommissionerControlShutdown(); + +} // namespace bridge diff --git a/examples/fabric-sync/bridge/include/FabricAdminDelegate.h b/examples/fabric-sync/bridge/include/FabricAdminDelegate.h new file mode 100644 index 00000000000000..8b67ffd5b29ed7 --- /dev/null +++ b/examples/fabric-sync/bridge/include/FabricAdminDelegate.h @@ -0,0 +1,55 @@ +/* + * 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 +#include +#include +#include +#include + +namespace bridge { + +class FabricAdminDelegate +{ +public: + virtual ~FabricAdminDelegate() = default; + + /** + * Reverse commission a bridge using the specified parameters. + * + * This function initiates the commissioning process for a bridge node, utilizing + * the provided passcode parameters, vendor ID, and product ID. + * + * @param params Parameters required for commissioning the device using passcode. + * @param vendorId The Vendor ID (VID) of the device being commissioned. This identifies + * the manufacturer of the device. + * @param productId The Product ID (PID) of the device being commissioned. This identifies + * the specific product within the vendor's lineup. + * + * @return CHIP_ERROR An error code indicating the success or failure of the operation. + * - CHIP_NO_ERROR: The RPC command was successfully sent and the commissioning process was initiated. + * - CHIP_ERROR_INTERNAL: An internal error occurred during the preparation or sending of the command. + */ + virtual CHIP_ERROR CommissionRemoteBridge(chip::Controller::CommissioningWindowPasscodeParams params, chip::VendorId vendorId, + uint16_t productId) = 0; + + virtual CHIP_ERROR KeepActive(chip::ScopedNodeId scopedNodeId, uint32_t stayActiveDurationMs, uint32_t timeoutMs) = 0; +}; + +} // namespace bridge diff --git a/examples/fabric-sync/bridge/include/FabricBridge.h b/examples/fabric-sync/bridge/include/FabricBridge.h new file mode 100644 index 00000000000000..48a3ddae4fc0c2 --- /dev/null +++ b/examples/fabric-sync/bridge/include/FabricBridge.h @@ -0,0 +1,161 @@ +/* + * 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 "FabricAdminDelegate.h" + +#include +#include +#include +#include +#include + +namespace bridge { + +#define ScopedNodeId_init_default chip::ScopedNodeId() +#define SynchronizedDevice_init_default \ + { \ + ScopedNodeId_init_default, /* id */ \ + std::nullopt, /* uniqueId */ \ + std::nullopt, /* vendorName */ \ + std::nullopt, /* vendorId */ \ + std::nullopt, /* productName */ \ + std::nullopt, /* productId */ \ + std::nullopt, /* nodeLabel */ \ + std::nullopt, /* hardwareVersion */ \ + std::nullopt, /* hardwareVersionString */ \ + std::nullopt, /* softwareVersion */ \ + std::nullopt, /* softwareVersionString */ \ + std::nullopt /* isIcd */ \ + } + +#define AdministratorCommissioningChanged_init_default \ + { \ + ScopedNodeId_init_default, /* id */ \ + chip::app::Clusters::AdministratorCommissioning::CommissioningWindowStatusEnum::kWindowNotOpen, /* windowStatus */ \ + std::nullopt, /* openerFabricIndex */ \ + std::nullopt /* openerVendorId */ \ + } + +struct SynchronizedDevice +{ + chip::ScopedNodeId id; + + std::optional uniqueId; + std::optional vendorName; + std::optional vendorId; + std::optional productName; + std::optional productId; + std::optional nodeLabel; + std::optional hardwareVersion; + std::optional hardwareVersionString; + std::optional softwareVersion; + std::optional softwareVersionString; + std::optional isIcd; +}; + +struct AdministratorCommissioningChanged +{ + chip::ScopedNodeId id; + chip::app::Clusters::AdministratorCommissioning::CommissioningWindowStatusEnum windowStatus; + std::optional openerFabricIndex; + std::optional openerVendorId; +}; + +class FabricBridge final +{ +public: + static FabricBridge & Instance(); + + void SetDelegate(FabricAdminDelegate * delegate) { mFabricAdmin = delegate; } + FabricAdminDelegate * GetDelegate() { return mFabricAdmin; } + + /** + * @brief Adds a synchronized device to the RPC client. + * + * This function attempts to add a device identified by its `nodeId` to the synchronized device list. + * It logs the progress and checks if an `AddSynchronizedDevice` operation is already in progress. + * If an operation is in progress, it returns `CHIP_ERROR_BUSY`. + * + * @return CHIP_ERROR An error code indicating the success or failure of the operation. + * - CHIP_NO_ERROR: The RPC command was successfully processed. + * - CHIP_ERROR_BUSY: Another operation is currently in progress. + * - CHIP_ERROR_INTERNAL: An internal error occurred while activating the RPC call. + */ + CHIP_ERROR AddSynchronizedDevice(const SynchronizedDevice & data); + + /** + * @brief Removes a synchronized device from the RPC client. + * + * This function attempts to remove a device identified by its `nodeId` from the synchronized device list. + * It logs the progress and checks if a `RemoveSynchronizedDevice` operation is already in progress. + * If an operation is in progress, it returns `CHIP_ERROR_BUSY`. + * + * @param scopedNodeId The Scoped Node ID of the device to be removed. + * @return CHIP_ERROR An error code indicating the success or failure of the operation. + * - CHIP_NO_ERROR: The RPC command was successfully processed. + * - CHIP_ERROR_BUSY: Another operation is currently in progress. + * - CHIP_ERROR_INTERNAL: An internal error occurred while activating the RPC call. + */ + CHIP_ERROR RemoveSynchronizedDevice(chip::ScopedNodeId scopedNodeId); + + /** + * @brief Received StayActiveResponse on behalf of client that previously called KeepActive + * + * @param scopedNodeId The Scoped Node ID of the device we recieved a StayActiveResponse. + * @param promisedActiveDurationMs the computed duration (in milliseconds) that the ICD intends to stay active for. + * @return CHIP_ERROR An error code indicating the success or failure of the operation. + * - CHIP_NO_ERROR: The RPC command was successfully processed. + * - CHIP_ERROR_BUSY: Another operation is currently in progress. + * - CHIP_ERROR_INTERNAL: An internal error occurred while activating the RPC call. + */ + CHIP_ERROR ActiveChanged(chip::ScopedNodeId scopedNodeId, uint32_t promisedActiveDurationMs); + + /** + * @brief CADMIN attribute has changed of one of the bridged devices that was previously added. + * + * @param data information regarding change in AdministratorCommissioning attributes + * @return CHIP_ERROR An error code indicating the success or failure of the operation. + * - CHIP_NO_ERROR: The RPC command was successfully processed. + * - CHIP_ERROR_BUSY: Another operation is currently in progress. + * - CHIP_ERROR_INTERNAL: An internal error occurred while activating the RPC call. + */ + CHIP_ERROR AdminCommissioningAttributeChanged(const AdministratorCommissioningChanged & data); + + /** + * @brief Notify the system that the reachability status of a bridged device has changed. + * + * @param scopedNodeId Identifier of the bridged device whose reachability has changed. + * @param reachability Boolean indicating the new reachability status of the device. + * - `true`: Device is reachable. + * - `false`: Device is not reachable. + * + * @return CHIP_ERROR Error code representing the outcome of the operation. + * - CHIP_NO_ERROR: The operation was successful. + * - CHIP_ERROR_BUSY: Another operation is currently in progress, preventing this action. + * - CHIP_ERROR_INTERNAL: An internal error occurred while processing the reachability change. + */ + CHIP_ERROR DeviceReachableChanged(chip::ScopedNodeId scopedNodeId, bool reachability); + +private: + static FabricBridge sInstance; + + FabricAdminDelegate * mFabricAdmin; +}; + +} // namespace bridge diff --git a/examples/fabric-sync/bridge/src/Bridge.cpp b/examples/fabric-sync/bridge/src/Bridge.cpp new file mode 100644 index 00000000000000..02bda529caec01 --- /dev/null +++ b/examples/fabric-sync/bridge/src/Bridge.cpp @@ -0,0 +1,147 @@ +/* + * 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 "Bridge.h" + +#include "BridgedAdministratorCommissioning.h" +#include "BridgedDevice.h" +#include "BridgedDeviceBasicInformationImpl.h" +#include "BridgedDeviceManager.h" +#include "FabricBridge.h" + +#include +#include +#include + +using namespace chip; +using namespace chip::app; +using namespace chip::app::Clusters; +using namespace chip::app::Clusters::AdministratorCommissioning; +using namespace chip::app::Clusters::BridgedDeviceBasicInformation; + +// This is declared here and not in a header because zap/embr assumes all clusters +// are defined in a static endpoint in the .zap file. From there, the codegen will +// automatically use PluginApplicationCallbacksHeader.jinja to declare and call +// the respective Init callbacks. However, because EcosystemInformation cluster is only +// ever on a dynamic endpoint, this doesn't get declared and called for us, so we +// need to declare and call it ourselves where the application is initialized. +void MatterEcosystemInformationPluginServerInitCallback(); + +namespace bridge { + +namespace { + +class BridgedDeviceInformationCommandHandler : public CommandHandlerInterface +{ +public: + // Register for the BridgedDeviceBasicInformation cluster on all endpoints. + BridgedDeviceInformationCommandHandler() : + CommandHandlerInterface(Optional::Missing(), BridgedDeviceBasicInformation::Id) + {} + + void InvokeCommand(HandlerContext & handlerContext) override; +}; + +void BridgedDeviceInformationCommandHandler::InvokeCommand(HandlerContext & handlerContext) +{ + using Protocols::InteractionModel::Status; + VerifyOrReturn(handlerContext.mRequestPath.mCommandId == BridgedDeviceBasicInformation::Commands::KeepActive::Id); + + EndpointId endpointId = handlerContext.mRequestPath.mEndpointId; + ChipLogProgress(NotSpecified, "Received command to KeepActive on Endpoint: %d", endpointId); + + handlerContext.SetCommandHandled(); + + BridgedDeviceBasicInformation::Commands::KeepActive::DecodableType commandData; + if (DataModel::Decode(handlerContext.mPayload, commandData) != CHIP_NO_ERROR) + { + handlerContext.mCommandHandler.AddStatus(handlerContext.mRequestPath, Status::InvalidCommand); + return; + } + + const uint32_t kMinTimeoutMs = 30 * 1000; + const uint32_t kMaxTimeoutMs = 60 * 60 * 1000; + if (commandData.timeoutMs < kMinTimeoutMs || commandData.timeoutMs > kMaxTimeoutMs) + { + handlerContext.mCommandHandler.AddStatus(handlerContext.mRequestPath, Status::ConstraintError); + return; + } + + BridgedDevice * device = BridgeDeviceMgr().GetDevice(endpointId); + if (device == nullptr || !device->IsIcd()) + { + handlerContext.mCommandHandler.AddStatus(handlerContext.mRequestPath, Status::Failure); + return; + } + + Status status = Status::Failure; + FabricAdminDelegate * adminDelegate = FabricBridge::Instance().GetDelegate(); + + if (adminDelegate) + { + CHIP_ERROR err = + adminDelegate->KeepActive(device->GetScopedNodeId(), commandData.stayActiveDuration, commandData.timeoutMs); + if (err == CHIP_NO_ERROR) + { + ChipLogProgress(NotSpecified, "KeepActive successfully processed"); + status = Status::Success; + } + else + { + ChipLogProgress(NotSpecified, "KeepActive failed to process: %s", ErrorStr(err)); + } + } + else + { + ChipLogProgress(NotSpecified, "Operation failed: adminDelegate is null"); + } + + handlerContext.mCommandHandler.AddStatus(handlerContext.mRequestPath, status); +} + +BridgedAdministratorCommissioning gBridgedAdministratorCommissioning; +BridgedDeviceBasicInformationImpl gBridgedDeviceBasicInformationAttributes; +BridgedDeviceInformationCommandHandler gBridgedDeviceInformationCommandHandler; + +} // namespace + +CHIP_ERROR BridgeInit(FabricAdminDelegate * delegate) +{ + MatterEcosystemInformationPluginServerInitCallback(); + CommandHandlerInterfaceRegistry::Instance().RegisterCommandHandler(&gBridgedDeviceInformationCommandHandler); + AttributeAccessInterfaceRegistry::Instance().Register(&gBridgedDeviceBasicInformationAttributes); + + BridgeDeviceMgr().Init(); + FabricBridge::Instance().SetDelegate(delegate); + ReturnErrorOnFailure(gBridgedAdministratorCommissioning.Init()); + ReturnErrorOnFailure(CommissionerControlInit(delegate)); + + return CHIP_NO_ERROR; +} + +CHIP_ERROR BridgeShutdown() +{ + CHIP_ERROR err = CommissionerControlShutdown(); + if (err != CHIP_NO_ERROR) + { + ChipLogError(NotSpecified, "Failed to shutdown Commissioner Control Server"); + } + + return err; +} + +} // namespace bridge diff --git a/examples/fabric-sync/bridge/src/BridgedDevice.cpp b/examples/fabric-sync/bridge/src/BridgedDevice.cpp index bd88c89145930e..1368f19bdadf05 100644 --- a/examples/fabric-sync/bridge/src/BridgedDevice.cpp +++ b/examples/fabric-sync/bridge/src/BridgedDevice.cpp @@ -1,5 +1,4 @@ /* - * * Copyright (c) 2024 Project CHIP Authors * All rights reserved. * diff --git a/examples/fabric-sync/bridge/src/BridgedDeviceManager.cpp b/examples/fabric-sync/bridge/src/BridgedDeviceManager.cpp index 09c1b643c00efb..62d423dc9a3ec7 100644 --- a/examples/fabric-sync/bridge/src/BridgedDeviceManager.cpp +++ b/examples/fabric-sync/bridge/src/BridgedDeviceManager.cpp @@ -1,5 +1,4 @@ /* - * * Copyright (c) 2024 Project CHIP Authors * All rights reserved. * diff --git a/examples/fabric-sync/bridge/src/CommissionerControlDelegate.cpp b/examples/fabric-sync/bridge/src/CommissionerControlDelegate.cpp new file mode 100644 index 00000000000000..12851177bcb212 --- /dev/null +++ b/examples/fabric-sync/bridge/src/CommissionerControlDelegate.cpp @@ -0,0 +1,267 @@ +/* + * 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 "CommissionerControlDelegate.h" + +#include +#include +#include +#include +#include +#include + +using namespace chip; +using namespace chip::app; + +namespace { + +// Constants +constexpr uint16_t kDiscriminator = 3840; +constexpr uint16_t kWindowTimeout = 300; +constexpr uint16_t kIteration = 1000; +constexpr uint32_t kSetupPinCode = 20202021; + +std::unique_ptr sCommissionerControlDelegate; + +} // namespace + +namespace chip { +namespace app { +namespace Clusters { +namespace CommissionerControl { + +void CommissionerControlDelegate::ResetDelegateState() +{ + ChipLogProgress(NotSpecified, "CommissionerControlDelegate: Resetting delegate state"); + + // Reset the step to the initial state + mNextStep = Step::kIdle; + + // Reset identifiers and product information + mRequestId = 0; + mClientNodeId = kUndefinedNodeId; + mVendorId = VendorId::Unspecified; + mProductId = 0; + + // Clear the label buffer and optional label + memset(mLabelBuffer, 0, sizeof(mLabelBuffer)); + mLabel.ClearValue(); + + // Reset PBKDF salt and PAKE passcode verifier buffers + mPBKDFSalt = ByteSpan(); + memset(mPBKDFSaltBuffer, 0, sizeof(mPBKDFSaltBuffer)); + + mPAKEPasscodeVerifier = ByteSpan(); + memset(mPAKEPasscodeVerifierBuffer, 0, sizeof(mPAKEPasscodeVerifierBuffer)); +} + +CHIP_ERROR CommissionerControlDelegate::HandleCommissioningApprovalRequest(const CommissioningApprovalRequest & request) +{ + ChipLogProgress(NotSpecified, "CommissionerControlDelegate: Entering HandleCommissioningApprovalRequest, current state: %s", + GetStateString(mNextStep)); + + VerifyOrReturnError(mNextStep == Step::kIdle, CHIP_ERROR_INCORRECT_STATE); + + CommissionerControl::Events::CommissioningRequestResult::Type result; + result.requestID = request.requestId; + result.clientNodeID = request.clientNodeId; + result.fabricIndex = request.fabricIndex; + result.statusCode = static_cast(Protocols::InteractionModel::Status::Success); + + mRequestId = request.requestId; + mClientNodeId = request.clientNodeId; + mVendorId = request.vendorId; + mProductId = request.productId; + + if (request.label.HasValue()) + { + const CharSpan & labelSpan = request.label.Value(); + size_t labelLength = labelSpan.size(); + + if (labelLength >= kLabelBufferSize) + { + ChipLogError(Zcl, "Label too long to fit in buffer"); + return CHIP_ERROR_BUFFER_TOO_SMALL; + } + + if (labelLength == 0) + { + mLabel.ClearValue(); + } + else + { + memcpy(mLabelBuffer, labelSpan.data(), labelLength); + mLabelBuffer[labelLength] = '\0'; // Null-terminate the copied string + mLabel.SetValue(CharSpan(mLabelBuffer, labelLength)); + } + } + else + { + mLabel.ClearValue(); + } + + CHIP_ERROR err = CommissionerControlServer::Instance().GenerateCommissioningRequestResultEvent(kAggregatorEndpointId, result); + + if (err == CHIP_NO_ERROR) + { + mNextStep = Step::kWaitCommissionNodeRequest; + ChipLogProgress(NotSpecified, "CommissionerControlDelegate: State transitioned to %s", GetStateString(mNextStep)); + } + else + { + ResetDelegateState(); + } + + return err; +} + +CHIP_ERROR CommissionerControlDelegate::ValidateCommissionNodeCommand(NodeId clientNodeId, uint64_t requestId) +{ + ChipLogProgress(NotSpecified, "CommissionerControlDelegate: Entering ValidateCommissionNodeCommand, current state: %s", + GetStateString(mNextStep)); + + CHIP_ERROR err = CHIP_NO_ERROR; + + VerifyOrReturnError(mNextStep == Step::kWaitCommissionNodeRequest, CHIP_ERROR_INCORRECT_STATE); + + // Verify if the CommissionNode command is sent from the same NodeId as the RequestCommissioningApproval. + VerifyOrExit(mClientNodeId == clientNodeId, err = CHIP_ERROR_WRONG_NODE_ID); + + // Verify if the provided RequestId matches the value provided to the RequestCommissioningApproval. + VerifyOrExit(mRequestId == requestId, err = CHIP_ERROR_INCORRECT_STATE); + + mNextStep = Step::kStartCommissionNode; + ChipLogProgress(NotSpecified, "CommissionerControlDelegate: State transitioned to %s", GetStateString(mNextStep)); + +exit: + return err; +} + +CHIP_ERROR CommissionerControlDelegate::GetCommissioningWindowParams(CommissioningWindowParams & outParams) +{ + // Populate outParams with the required details. + outParams.iterations = kIteration; + outParams.commissioningTimeout = kWindowTimeout; + outParams.discriminator = kDiscriminator; + + ReturnErrorOnFailure(Crypto::DRBG_get_bytes(mPBKDFSaltBuffer, sizeof(mPBKDFSaltBuffer))); + mPBKDFSalt = ByteSpan(mPBKDFSaltBuffer); + outParams.salt = mPBKDFSalt; + + Crypto::Spake2pVerifier verifier; + uint32_t setupPIN = kSetupPinCode; + ReturnErrorOnFailure(PASESession::GeneratePASEVerifier(verifier, kIteration, mPBKDFSalt, false, setupPIN)); + + MutableByteSpan serializedVerifierSpan(mPAKEPasscodeVerifierBuffer); + ReturnErrorOnFailure(verifier.Serialize(serializedVerifierSpan)); + mPAKEPasscodeVerifier = serializedVerifierSpan; + outParams.PAKEPasscodeVerifier = mPAKEPasscodeVerifier; + + return CHIP_NO_ERROR; +} + +CHIP_ERROR CommissionerControlDelegate::HandleCommissionNode(const CommissioningWindowParams & params) +{ + CHIP_ERROR err = CHIP_NO_ERROR; + + ChipLogProgress(NotSpecified, "CommissionerControlDelegate: Entering HandleCommissionNode, current state: %s", + GetStateString(mNextStep)); + + VerifyOrReturnError(mNextStep == Step::kStartCommissionNode, CHIP_ERROR_INCORRECT_STATE); + + // Attempt to reverse commission the bridge using provided commissioning parameters. + err = mFabricAdmin->CommissionRemoteBridge(Controller::CommissioningWindowPasscodeParams() + .SetSetupPIN(kSetupPinCode) + .SetTimeout(params.commissioningTimeout) + .SetDiscriminator(params.discriminator) + .SetIteration(params.iterations) + .SetSalt(params.salt), + mVendorId, mProductId); + + if (err != CHIP_NO_ERROR) + { + ChipLogError(NotSpecified, "Failed to reverse commission the fabric bridge. Error: %" CHIP_ERROR_FORMAT, err.Format()); + } + + // Reset the delegate's state to prepare for a new commissioning sequence. + ResetDelegateState(); + + return err; +} + +} // namespace CommissionerControl +} // namespace Clusters +} // namespace app +} // namespace chip + +namespace bridge { + +CHIP_ERROR CommissionerControlInit(bridge::FabricAdminDelegate * fabricAdmin) +{ + CHIP_ERROR err; + + if (sCommissionerControlDelegate) + { + ChipLogError(NotSpecified, "Commissioner Control Delegate already exists."); + return CHIP_ERROR_INCORRECT_STATE; + } + + sCommissionerControlDelegate = std::make_unique(fabricAdmin); + if (!sCommissionerControlDelegate) + { + ChipLogError(NotSpecified, "Failed to allocate memory for Commissioner Control Delegate."); + return CHIP_ERROR_NO_MEMORY; + } + + err = Clusters::CommissionerControl::CommissionerControlServer::Instance().Init(*sCommissionerControlDelegate); + if (err != CHIP_NO_ERROR) + { + ChipLogError(AppServer, "Initialization failed on Commissioner Control Delegate."); + sCommissionerControlDelegate.reset(); + return err; + } + + ChipLogProgress(Zcl, "Initializing SupportedDeviceCategories of Commissioner Control Cluster for this device."); + + BitMask supportedDeviceCategories; + supportedDeviceCategories.SetField(Clusters::CommissionerControl::SupportedDeviceCategoryBitmap::kFabricSynchronization, 1); + + Protocols::InteractionModel::Status status = + Clusters::CommissionerControl::CommissionerControlServer::Instance().SetSupportedDeviceCategoriesValue( + Clusters::CommissionerControl::kAggregatorEndpointId, supportedDeviceCategories); + + if (status != Protocols::InteractionModel::Status::Success) + { + ChipLogError(NotSpecified, "Failed to set SupportedDeviceCategories: %d", static_cast(status)); + sCommissionerControlDelegate.reset(); + return CHIP_ERROR_INTERNAL; + } + + return CHIP_NO_ERROR; +} + +CHIP_ERROR CommissionerControlShutdown() +{ + if (sCommissionerControlDelegate) + { + sCommissionerControlDelegate.reset(); + } + + return CHIP_NO_ERROR; +} + +} // namespace bridge diff --git a/examples/fabric-sync/bridge/src/FabricBridge.cpp b/examples/fabric-sync/bridge/src/FabricBridge.cpp new file mode 100644 index 00000000000000..d1c27d2409262d --- /dev/null +++ b/examples/fabric-sync/bridge/src/FabricBridge.cpp @@ -0,0 +1,223 @@ +/* + * 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 "FabricBridge.h" +#include "BridgedDevice.h" +#include "BridgedDeviceManager.h" + +#include +#include + +#include +#include + +using namespace chip; + +namespace bridge { + +FabricBridge FabricBridge::sInstance; + +FabricBridge & FabricBridge::Instance() +{ + return sInstance; +} + +CHIP_ERROR FabricBridge::AddSynchronizedDevice(const SynchronizedDevice & data) +{ + ChipLogProgress(NotSpecified, "Received AddSynchronizedDevice: Id=[%d:" ChipLogFormatX64 "]", data.id.GetFabricIndex(), + ChipLogValueX64(data.id.GetNodeId())); + + // Create a new BridgedDevice and set it as reachable + auto device = std::make_unique(data.id); + device->SetReachable(true); + + // Initialize BridgedDevice attributes from data + BridgedDevice::BridgedAttributes attributes; + + if (data.uniqueId.has_value()) + { + attributes.uniqueId = data.uniqueId.value(); + } + + if (data.vendorName.has_value()) + { + attributes.vendorName = data.vendorName.value(); + } + + if (data.vendorId.has_value()) + { + attributes.vendorId = data.vendorId.value(); + } + + if (data.productName.has_value()) + { + attributes.productName = data.productName.value(); + } + + if (data.productId.has_value()) + { + attributes.productId = data.productId.value(); + } + + if (data.nodeLabel.has_value()) + { + attributes.nodeLabel = data.nodeLabel.value(); + } + + if (data.hardwareVersion.has_value()) + { + attributes.hardwareVersion = data.hardwareVersion.value(); + } + + if (data.hardwareVersionString.has_value()) + { + attributes.hardwareVersionString = data.hardwareVersionString.value(); + } + + if (data.softwareVersion.has_value()) + { + attributes.softwareVersion = data.softwareVersion.value(); + } + + if (data.softwareVersionString.has_value()) + { + attributes.softwareVersionString = data.softwareVersionString.value(); + } + + // Set bridged device attributes and ICD status + device->SetBridgedAttributes(attributes); + device->SetIcd(data.isIcd.value_or(false)); + + // Add the device to the bridge manager with a parent endpoint + auto result = BridgeDeviceMgr().AddDeviceEndpoint(std::move(device), /* parentEndpointId= */ 1); + if (!result.has_value()) + { + ChipLogError(NotSpecified, "Failed to add device with Id=[%d:0x" ChipLogFormatX64 "]", data.id.GetFabricIndex(), + ChipLogValueX64(data.id.GetNodeId())); + return CHIP_ERROR_ENDPOINT_POOL_FULL; + } + + // Retrieve and verify the added device by ScopedNodeId + BridgedDevice * addedDevice = BridgeDeviceMgr().GetDeviceByScopedNodeId(data.id); + VerifyOrDie(addedDevice); + + ChipLogProgress(NotSpecified, "Added device with Id=[%d:0x" ChipLogFormatX64 "]", data.id.GetFabricIndex(), + ChipLogValueX64(data.id.GetNodeId())); + + // Add the Ecosystem Information Cluster to the device's endpoint + CHIP_ERROR err = + app::Clusters::EcosystemInformation::EcosystemInformationServer::Instance().AddEcosystemInformationClusterToEndpoint( + addedDevice->GetEndpointId()); + + if (err != CHIP_NO_ERROR) + { + ChipLogError(NotSpecified, "Failed to add Ecosystem Information Cluster to endpoint %u: %" CHIP_ERROR_FORMAT, + addedDevice->GetEndpointId(), err.Format()); + } + + return err; +} + +CHIP_ERROR FabricBridge::RemoveSynchronizedDevice(ScopedNodeId scopedNodeId) +{ + ChipLogProgress(NotSpecified, "Received RemoveSynchronizedDevice: Id=[%d:" ChipLogFormatX64 "]", scopedNodeId.GetFabricIndex(), + ChipLogValueX64(scopedNodeId.GetNodeId())); + + auto removedIdx = BridgeDeviceMgr().RemoveDeviceByScopedNodeId(scopedNodeId); + if (!removedIdx.has_value()) + { + ChipLogError(NotSpecified, "Failed to remove device with Id=[%d:0x" ChipLogFormatX64 "]", scopedNodeId.GetFabricIndex(), + ChipLogValueX64(scopedNodeId.GetNodeId())); + return CHIP_ERROR_NOT_FOUND; + } + + return CHIP_NO_ERROR; +} + +CHIP_ERROR FabricBridge::ActiveChanged(ScopedNodeId scopedNodeId, uint32_t promisedActiveDurationMs) +{ + ChipLogProgress(NotSpecified, "Received ActiveChanged: Id=[%d:" ChipLogFormatX64 "]", scopedNodeId.GetFabricIndex(), + ChipLogValueX64(scopedNodeId.GetNodeId())); + + auto * device = BridgeDeviceMgr().GetDeviceByScopedNodeId(scopedNodeId); + if (device == nullptr) + { + ChipLogError(NotSpecified, "Could not find bridged device associated with Id=[%d:0x" ChipLogFormatX64 "]", + scopedNodeId.GetFabricIndex(), ChipLogValueX64(scopedNodeId.GetNodeId())); + return CHIP_ERROR_NOT_FOUND; + } + + device->LogActiveChangeEvent(promisedActiveDurationMs); + return CHIP_NO_ERROR; +} + +CHIP_ERROR FabricBridge::AdminCommissioningAttributeChanged(const AdministratorCommissioningChanged & data) +{ + ChipLogProgress(NotSpecified, "Received CADMIN attribute change: Id=[%d:" ChipLogFormatX64 "]", data.id.GetFabricIndex(), + ChipLogValueX64(data.id.GetNodeId())); + + auto * device = BridgeDeviceMgr().GetDeviceByScopedNodeId(data.id); + if (device == nullptr) + { + ChipLogError(NotSpecified, "Could not find bridged device associated with Id=[%d:0x" ChipLogFormatX64 "]", + data.id.GetFabricIndex(), ChipLogValueX64(data.id.GetNodeId())); + return CHIP_ERROR_NOT_FOUND; + } + + BridgedDevice::AdminCommissioningAttributes adminCommissioningAttributes; + + VerifyOrReturnError(data.windowStatus < + app::Clusters::AdministratorCommissioning::CommissioningWindowStatusEnum::kUnknownEnumValue, + CHIP_ERROR_INVALID_ARGUMENT); + + adminCommissioningAttributes.commissioningWindowStatus = data.windowStatus; + if (data.openerFabricIndex.has_value()) + { + VerifyOrReturnError(data.openerFabricIndex >= kMinValidFabricIndex, CHIP_ERROR_INVALID_ARGUMENT); + VerifyOrReturnError(data.openerFabricIndex <= kMaxValidFabricIndex, CHIP_ERROR_INVALID_ARGUMENT); + adminCommissioningAttributes.openerFabricIndex = data.openerFabricIndex; + } + + if (data.openerVendorId.has_value()) + { + VerifyOrReturnError(data.openerVendorId != VendorId::NotSpecified, CHIP_ERROR_INVALID_ARGUMENT); + adminCommissioningAttributes.openerVendorId = data.openerVendorId; + } + + device->SetAdminCommissioningAttributes(adminCommissioningAttributes); + return CHIP_NO_ERROR; +} + +CHIP_ERROR FabricBridge::DeviceReachableChanged(ScopedNodeId scopedNodeId, bool reachability) +{ + ChipLogProgress(NotSpecified, "Received device reachable changed: Id=[%d:" ChipLogFormatX64 "]", scopedNodeId.GetFabricIndex(), + ChipLogValueX64(scopedNodeId.GetNodeId())); + + auto * device = BridgeDeviceMgr().GetDeviceByScopedNodeId(scopedNodeId); + if (device == nullptr) + { + ChipLogError(NotSpecified, "Could not find bridged device associated with Id=[%d:0x" ChipLogFormatX64 "]", + scopedNodeId.GetFabricIndex(), ChipLogValueX64(scopedNodeId.GetNodeId())); + return CHIP_ERROR_NOT_FOUND; + } + + device->ReachableChanged(reachability); + + return CHIP_NO_ERROR; +} + +} // namespace bridge diff --git a/examples/fabric-sync/main.cpp b/examples/fabric-sync/main.cpp index 3cd5ab8c20f1c9..8772400ff2ae16 100644 --- a/examples/fabric-sync/main.cpp +++ b/examples/fabric-sync/main.cpp @@ -1,5 +1,4 @@ /* - * * Copyright (c) 2024 Project CHIP Authors * All rights reserved. * @@ -17,7 +16,9 @@ */ #include +#include #include +#include #if defined(ENABLE_CHIP_SHELL) #include "ShellCommands.h" @@ -82,12 +83,21 @@ void ApplicationInit() // Redirect logs to the custom logging callback Logging::SetLogRedirectCallback(LoggingCallback); + + CHIP_ERROR err = bridge::BridgeInit(&admin::FabricAdmin::Instance()); + VerifyOrDieWithMsg(err == CHIP_NO_ERROR, NotSpecified, "Fabric-Sync: Failed to initialize bridge, error: %s", ErrorStr(err)); } void ApplicationShutdown() { ChipLogDetail(NotSpecified, "Fabric-Sync: ApplicationShutdown()"); CloseLogFile(); + + CHIP_ERROR err = bridge::BridgeShutdown(); + if (err != CHIP_NO_ERROR) + { + ChipLogError(NotSpecified, "Fabric-Sync: Failed to shutdown bridge, error: %s", ErrorStr(err)); + } } int main(int argc, char * argv[]) diff --git a/examples/fabric-sync/shell/AddBridgeCommand.cpp b/examples/fabric-sync/shell/AddBridgeCommand.cpp index 936ac1c321ca50..4b70998dccd273 100644 --- a/examples/fabric-sync/shell/AddBridgeCommand.cpp +++ b/examples/fabric-sync/shell/AddBridgeCommand.cpp @@ -54,6 +54,15 @@ void AddBridgeCommand::OnCommissioningComplete(NodeId deviceId, CHIP_ERROR err) ChipLogValueX64(mBridgeNodeId)); admin::DeviceMgr().UpdateLastUsedNodeId(mBridgeNodeId); + admin::DeviceMgr().SubscribeRemoteFabricBridge(); + + // After successful commissioning of the Commissionee, initiate Reverse Commissioning + // via the Commissioner Control Cluster. However, we must first verify that the + // remote Fabric-Bridge supports Fabric Synchronization. + // + // Note: The Fabric-Admin MUST NOT send the RequestCommissioningApproval command + // if the remote Fabric-Bridge lacks Fabric Synchronization support. + DeviceLayer::SystemLayer().ScheduleLambda([]() { admin::DeviceMgr().ReadSupportedDeviceCategories(); }); } else { @@ -70,11 +79,14 @@ CHIP_ERROR AddBridgeCommand::RunCommand() { // print to console fprintf(stderr, "Remote Fabric Bridge has already been configured.\n"); - return CHIP_NO_ERROR; + return CHIP_ERROR_INCORRECT_STATE; } admin::PairingManager::Instance().SetPairingDelegate(this); + ChipLogProgress(NotSpecified, "Running AddBridgeCommand with Node ID: %lu, PIN Code: %u, Address: %s, Port: %u", mBridgeNodeId, + mSetupPINCode, mRemoteAddr, mRemotePort); + return admin::DeviceMgr().PairRemoteFabricBridge(mBridgeNodeId, mSetupPINCode, mRemoteAddr, mRemotePort); } diff --git a/examples/fabric-sync/shell/AddDeviceCommand.cpp b/examples/fabric-sync/shell/AddDeviceCommand.cpp index 218691e74401ea..a85de7695deb98 100644 --- a/examples/fabric-sync/shell/AddDeviceCommand.cpp +++ b/examples/fabric-sync/shell/AddDeviceCommand.cpp @@ -71,6 +71,9 @@ CHIP_ERROR AddDeviceCommand::RunCommand() return CHIP_ERROR_INVALID_ARGUMENT; } + ChipLogProgress(NotSpecified, "Running AddDeviceCommand with Node ID: %lu, PIN Code: %u, Address: %s, Port: %u", mNodeId, + mSetupPINCode, mRemoteAddr, mRemotePort); + admin::PairingManager::Instance().SetPairingDelegate(this); return admin::DeviceMgr().PairRemoteDevice(mNodeId, mSetupPINCode, mRemoteAddr, mRemotePort); diff --git a/examples/fabric-sync/shell/RemoveBridgeCommand.cpp b/examples/fabric-sync/shell/RemoveBridgeCommand.cpp index 2eaaf6fc34476f..77bd76be33675a 100644 --- a/examples/fabric-sync/shell/RemoveBridgeCommand.cpp +++ b/examples/fabric-sync/shell/RemoveBridgeCommand.cpp @@ -57,11 +57,13 @@ CHIP_ERROR RemoveBridgeCommand::RunCommand() { // print to console fprintf(stderr, "Remote Fabric Bridge is not configured yet, nothing to remove.\n"); - return CHIP_NO_ERROR; + return CHIP_ERROR_INCORRECT_STATE; } mBridgeNodeId = bridgeNodeId; + ChipLogProgress(NotSpecified, "Running RemoveBridgeCommand"); + admin::PairingManager::Instance().SetPairingDelegate(this); return admin::DeviceMgr().UnpairRemoteFabricBridge(); diff --git a/examples/fabric-sync/shell/RemoveDeviceCommand.cpp b/examples/fabric-sync/shell/RemoveDeviceCommand.cpp index 266a2ad1065569..78440f854283aa 100644 --- a/examples/fabric-sync/shell/RemoveDeviceCommand.cpp +++ b/examples/fabric-sync/shell/RemoveDeviceCommand.cpp @@ -61,6 +61,8 @@ CHIP_ERROR RemoveDeviceCommand::RunCommand() admin::PairingManager::Instance().SetPairingDelegate(this); + ChipLogProgress(NotSpecified, "Running RemoveDeviceCommand with Node ID: %lu", mNodeId); + return admin::DeviceMgr().UnpairRemoteDevice(mNodeId); } diff --git a/examples/fabric-sync/shell/ShellCommands.cpp b/examples/fabric-sync/shell/ShellCommands.cpp index a654fbcc9f98f1..3bab442aa5fc47 100644 --- a/examples/fabric-sync/shell/ShellCommands.cpp +++ b/examples/fabric-sync/shell/ShellCommands.cpp @@ -1,5 +1,4 @@ /* - * * Copyright (c) 2024 Project CHIP Authors * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -134,7 +133,7 @@ static CHIP_ERROR HandleAddDeviceCommand(int argc, char ** argv) const char * remoteAddr = argv[3]; uint16_t remotePort = static_cast(strtoul(argv[4], nullptr, 10)); - auto command = std::make_unique(nodeId, setupPINCode, remoteAddr, remotePort); + auto command = std::make_unique(nodeId, setupPINCode, remoteAddr, remotePort); CHIP_ERROR result = command->RunCommand(); if (result == CHIP_NO_ERROR) @@ -216,7 +215,6 @@ void RegisterCommands() // Register the root `device` command with the top-level shell. Engine::Root().RegisterCommands(&sDeviceComand, 1); - return; } } // namespace Shell diff --git a/examples/fabric-sync/shell/ShellCommands.h b/examples/fabric-sync/shell/ShellCommands.h index ab957bc8cd92d7..b5bd5085fba3dc 100644 --- a/examples/fabric-sync/shell/ShellCommands.h +++ b/examples/fabric-sync/shell/ShellCommands.h @@ -1,5 +1,4 @@ /* - * * Copyright (c) 2024 Project CHIP Authors * All rights reserved. *