From bd8f8b83ceea7cde78f1f7855e1b1f4b91916acb Mon Sep 17 00:00:00 2001 From: Yufeng Wang Date: Mon, 21 Oct 2024 19:31:54 -0700 Subject: [PATCH] [Fabric-Sync] Integrate fabric bridge functionalities into fabric-sync (#36166) --- examples/fabric-sync/bridge/BUILD.gn | 12 +- .../BridgedAdministratorCommissioning.h | 58 +++ .../bridge/include/BridgedDevice.h | 92 +++++ .../BridgedDeviceBasicInformationImpl.h | 32 ++ .../bridge/include/BridgedDeviceManager.h | 136 +++++++ .../src/BridgedAdministratorCommissioning.cpp | 81 ++++ .../fabric-sync/bridge/src/BridgedDevice.cpp | 118 ++++++ .../src/BridgedDeviceBasicInformationImpl.cpp | 107 ++++++ .../bridge/src/BridgedDeviceManager.cpp | 345 ++++++++++++++++++ 9 files changed, 980 insertions(+), 1 deletion(-) create mode 100644 examples/fabric-sync/bridge/include/BridgedAdministratorCommissioning.h create mode 100644 examples/fabric-sync/bridge/include/BridgedDevice.h create mode 100644 examples/fabric-sync/bridge/include/BridgedDeviceBasicInformationImpl.h create mode 100644 examples/fabric-sync/bridge/include/BridgedDeviceManager.h create mode 100644 examples/fabric-sync/bridge/src/BridgedAdministratorCommissioning.cpp create mode 100644 examples/fabric-sync/bridge/src/BridgedDevice.cpp create mode 100644 examples/fabric-sync/bridge/src/BridgedDeviceBasicInformationImpl.cpp create mode 100644 examples/fabric-sync/bridge/src/BridgedDeviceManager.cpp diff --git a/examples/fabric-sync/bridge/BUILD.gn b/examples/fabric-sync/bridge/BUILD.gn index cdf3671b7d1cc5..310496bc54d06a 100644 --- a/examples/fabric-sync/bridge/BUILD.gn +++ b/examples/fabric-sync/bridge/BUILD.gn @@ -40,7 +40,17 @@ source_set("fabric-bridge-common") { source_set("fabric-bridge-lib") { public_configs = [ ":config" ] - sources = [ "include/CHIPProjectAppConfig.h" ] + sources = [ + "include/BridgedAdministratorCommissioning.h", + "include/BridgedDevice.h", + "include/BridgedDeviceBasicInformationImpl.h", + "include/BridgedDeviceManager.h", + "include/CHIPProjectAppConfig.h", + "src/BridgedAdministratorCommissioning.cpp", + "src/BridgedDevice.cpp", + "src/BridgedDeviceBasicInformationImpl.cpp", + "src/BridgedDeviceManager.cpp", + ] deps = [ "${chip_root}/examples/fabric-sync/bridge:fabric-bridge-common", diff --git a/examples/fabric-sync/bridge/include/BridgedAdministratorCommissioning.h b/examples/fabric-sync/bridge/include/BridgedAdministratorCommissioning.h new file mode 100644 index 00000000000000..06fd9026d44225 --- /dev/null +++ b/examples/fabric-sync/bridge/include/BridgedAdministratorCommissioning.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 +#include + +/** + * @brief CADMIN cluster implementation for handling attribute interactions of bridged device endpoints. + * + * The current Administrator Commissioning Cluster server's zap generated code will automatically + * register an Attribute Access Interface for the root node endpoint implementation. In order to + * properly respond to a read attribute for bridged devices we are representing, we override the + * currently registered Attribute Interface such that we are first to receive any read attribute + * request on Administrator Commissioning Cluster, and if it is not an endpoint for a device we + * are a bridge for we redirect to the default cluster server implementation of Administrator + * Commissioning Cluster. + */ +class BridgedAdministratorCommissioning : public chip::app::AttributeAccessInterface +{ +public: + // Register for the AdministratorCommissioning cluster on all endpoints. + BridgedAdministratorCommissioning() : + AttributeAccessInterface(chip::NullOptional, chip::app::Clusters::AdministratorCommissioning::Id) + {} + + CHIP_ERROR Init(); + + CHIP_ERROR Read(const chip::app::ConcreteReadAttributePath & aPath, chip::app::AttributeValueEncoder & aEncoder) override; + + // We do not allow writing to CADMIN attributes of a bridged device endpoint. We simply redirect + // write requests to the original attribute interface. + CHIP_ERROR Write(const chip::app::ConcreteDataAttributePath & aPath, chip::app::AttributeValueDecoder & aDecoder) override + { + VerifyOrDie(mOriginalAttributeInterface); + return mOriginalAttributeInterface->Write(aPath, aDecoder); + } + +private: + // If mOriginalAttributeInterface is removed from here, the class description needs to be updated + // to reflect this change. + chip::app::AttributeAccessInterface * mOriginalAttributeInterface = nullptr; +}; diff --git a/examples/fabric-sync/bridge/include/BridgedDevice.h b/examples/fabric-sync/bridge/include/BridgedDevice.h new file mode 100644 index 00000000000000..d2c5a64b9ef4b7 --- /dev/null +++ b/examples/fabric-sync/bridge/include/BridgedDevice.h @@ -0,0 +1,92 @@ +/* + * + * 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 + +class BridgedDevice +{ +public: + /// Defines all attributes that we keep track of for a bridged device + struct BridgedAttributes + { + std::string uniqueId; + std::string vendorName; + uint16_t vendorId = 0; + std::string productName; + uint16_t productId = 0; + std::string nodeLabel; + uint16_t hardwareVersion = 0; + std::string hardwareVersionString; + uint32_t softwareVersion = 0; + std::string softwareVersionString; + }; + + struct AdminCommissioningAttributes + { + chip::app::Clusters::AdministratorCommissioning::CommissioningWindowStatusEnum commissioningWindowStatus = + chip::app::Clusters::AdministratorCommissioning::CommissioningWindowStatusEnum::kWindowNotOpen; + std::optional openerFabricIndex = std::nullopt; + std::optional openerVendorId = std::nullopt; + }; + + BridgedDevice(chip::ScopedNodeId scopedNodeId); + virtual ~BridgedDevice() = default; + + [[nodiscard]] bool IsReachable() const { return mReachable; } + void SetReachable(bool reachable); + // Reachability attribute changed and requires marking attribute as dirty and sending + // event. + void ReachableChanged(bool reachable); + + void LogActiveChangeEvent(uint32_t promisedActiveDurationMs); + + [[nodiscard]] bool IsIcd() const { return mIsIcd; } + void SetIcd(bool icd) { mIsIcd = icd; } + + inline void SetEndpointId(chip::EndpointId id) { mEndpointId = id; }; + inline chip::EndpointId GetEndpointId() { return mEndpointId; }; + inline chip::ScopedNodeId GetScopedNodeId() { return mScopedNodeId; }; + inline void SetParentEndpointId(chip::EndpointId id) { mParentEndpointId = id; }; + inline chip::EndpointId GetParentEndpointId() { return mParentEndpointId; }; + + [[nodiscard]] const BridgedAttributes & GetBridgedAttributes() const { return mAttributes; } + void SetBridgedAttributes(const BridgedAttributes & value) { mAttributes = value; } + + void SetAdminCommissioningAttributes(const AdminCommissioningAttributes & aAdminCommissioningAttributes); + const AdminCommissioningAttributes & GetAdminCommissioningAttributes() const { return mAdminCommissioningAttributes; } + + /// Convenience method to set just the unique id of a bridged device as it + /// is one of the few attributes that is not always bulk-set + void SetUniqueId(const std::string & value) { mAttributes.uniqueId = value; } + +protected: + bool mReachable = false; + bool mIsIcd = false; + + chip::ScopedNodeId mScopedNodeId; + chip::EndpointId mEndpointId = 0; + chip::EndpointId mParentEndpointId = 0; + + BridgedAttributes mAttributes; + AdminCommissioningAttributes mAdminCommissioningAttributes; +}; diff --git a/examples/fabric-sync/bridge/include/BridgedDeviceBasicInformationImpl.h b/examples/fabric-sync/bridge/include/BridgedDeviceBasicInformationImpl.h new file mode 100644 index 00000000000000..23403438ab2be8 --- /dev/null +++ b/examples/fabric-sync/bridge/include/BridgedDeviceBasicInformationImpl.h @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2024 Project CHIP Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#pragma once + +#include +#include + +class BridgedDeviceBasicInformationImpl : public chip::app::AttributeAccessInterface +{ +public: + BridgedDeviceBasicInformationImpl() : + chip::app::AttributeAccessInterface(chip::NullOptional /* endpointId */, + chip::app::Clusters::BridgedDeviceBasicInformation::Id) + {} + + // AttributeAccessInterface implementation + CHIP_ERROR Read(const chip::app::ConcreteReadAttributePath & path, chip::app::AttributeValueEncoder & encoder) override; + CHIP_ERROR Write(const chip::app::ConcreteDataAttributePath & path, chip::app::AttributeValueDecoder & decoder) override; +}; diff --git a/examples/fabric-sync/bridge/include/BridgedDeviceManager.h b/examples/fabric-sync/bridge/include/BridgedDeviceManager.h new file mode 100644 index 00000000000000..127898fc5b2fab --- /dev/null +++ b/examples/fabric-sync/bridge/include/BridgedDeviceManager.h @@ -0,0 +1,136 @@ +/* + * + * 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 "BridgedDevice.h" + +#include + +class BridgedDeviceManager +{ +public: + BridgedDeviceManager() = default; + + /** + * @brief Initializes the BridgedDeviceManager. + * + * This function sets up the initial state of the BridgedDeviceManager, clearing + * any existing devices and setting the starting dynamic endpoint ID. + */ + void Init(); + + /** + * @brief Adds a device to a dynamic endpoint. + * + * This function attempts to add a device to a dynamic endpoint. It tries to find an available + * endpoint slot and retries the addition process up to a specified maximum number of times if + * the endpoint already exists. If the addition is successful, it returns the index of the + * dynamic endpoint; + * + * Ensures that the device has a unique id: + * - device MUST set its unique id if any BEFORE calling this method + * - if no unique id (i.e. empty string), a new unique id will be generated + * - Add will fail if the unique id is not unique + * + * @param dev A pointer to the device to be added. + * @param parentEndpointId The parent endpoint ID. Defaults to an invalid endpoint ID. + * @return int The index of the dynamic endpoint if successful, nullopt otherwise + */ + std::optional AddDeviceEndpoint(std::unique_ptr dev, + chip::EndpointId parentEndpointId = chip::kInvalidEndpointId); + + /** + * @brief Removes a device from a dynamic endpoint. + * + * This function attempts to remove a device from a dynamic endpoint by iterating through the + * available endpoints and checking if the device matches. If the device is found, it clears the + * dynamic endpoint, logs the removal, and returns the index of the removed endpoint. + * If the device is not found, it returns -1. + * + * @param dev A pointer to the device to be removed. + * @return int The index of the removed dynamic endpoint if successful, -1 otherwise. + */ + int RemoveDeviceEndpoint(BridgedDevice * dev); + + /** + * @brief Gets a device from its endpoint ID. + * + * This function iterates through the available devices and returns the device that matches the + * specified endpoint ID. If no device matches the endpoint ID, it returns nullptr. + * + * @param endpointId The endpoint ID of the device to be retrieved. + * @return BridgedDevice* A pointer to the device if found, nullptr otherwise. + */ + BridgedDevice * GetDevice(chip::EndpointId endpointId) const; + + /** + * @brief Gets a device from its ScopedNodeId. + * + * This function iterates through the available devices and returns the device that matches the + * specified ScopedNodeId. If no device matches the ScopedNodeId, it returns nullptr. + * + * @param scopedNodeId The ScopedNodeId of the device to be retrieved. + * @return BridgedDevice* A pointer to the device if found, nullptr otherwise. + */ + BridgedDevice * GetDeviceByScopedNodeId(chip::ScopedNodeId scopedNodeId) const; + + /** + * @brief Removes a device from a dynamic endpoint by its ScopedNodeId. + * + * This function attempts to remove a device and the associated dynamic endpoint by iterating through + * the available device and checking if the device matches the specified ScopedNodeId. If the device is + * found, it removes the dynamic endpoint. + * + * @param scopedNodeId The ScopedNodeId of the device to be removed. + * @return unsigned of the index of the removed dynamic endpoint if successful, nullopt otherwise. + */ + std::optional RemoveDeviceByScopedNodeId(chip::ScopedNodeId scopedNodeId); + + /** + * Finds the device with the given unique id (if any) + */ + BridgedDevice * GetDeviceByUniqueId(const std::string & id); + +private: + friend BridgedDeviceManager & BridgeDeviceMgr(); + + /** + * Creates a new unique ID that is not used by any other mDevice + */ + std::string GenerateUniqueId(); + + static BridgedDeviceManager sInstance; + + chip::EndpointId mCurrentEndpointId; + chip::EndpointId mFirstDynamicEndpointId; + std::unique_ptr mDevices[CHIP_DEVICE_CONFIG_DYNAMIC_ENDPOINT_COUNT + 1]; +}; + +/** + * Returns the public interface of the BridgedDeviceManager singleton object. + * + * Applications should use this to access features of the BridgedDeviceManager + * object. + */ +inline BridgedDeviceManager & BridgeDeviceMgr() +{ + return BridgedDeviceManager::sInstance; +} diff --git a/examples/fabric-sync/bridge/src/BridgedAdministratorCommissioning.cpp b/examples/fabric-sync/bridge/src/BridgedAdministratorCommissioning.cpp new file mode 100644 index 00000000000000..a0d87cbb3b81ef --- /dev/null +++ b/examples/fabric-sync/bridge/src/BridgedAdministratorCommissioning.cpp @@ -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. + */ + +#include "BridgedAdministratorCommissioning.h" + +#include "BridgedDevice.h" +#include "BridgedDeviceManager.h" +#include + +using namespace chip; +using namespace chip::app; +using namespace chip::app::Clusters; +using namespace chip::app::Clusters::AdministratorCommissioning; + +CHIP_ERROR BridgedAdministratorCommissioning::Init() +{ + // We expect initialization after emberAfInit(). This allows us to unregister the existing + // AccessAttributeInterface for AdministratorCommissioning and register ourselves, ensuring we + // get the callback for reading attribute. If the read is not intended for a bridged device we will + // forward it to the original attribute interface that we are unregistering. + mOriginalAttributeInterface = AttributeAccessInterfaceRegistry::Instance().Get(kRootEndpointId, AdministratorCommissioning::Id); + VerifyOrReturnError(mOriginalAttributeInterface, CHIP_ERROR_INTERNAL); + AttributeAccessInterfaceRegistry::Instance().Unregister(mOriginalAttributeInterface); + VerifyOrDie(AttributeAccessInterfaceRegistry::Instance().Register(this)); + return CHIP_NO_ERROR; +} + +CHIP_ERROR BridgedAdministratorCommissioning::Read(const ConcreteReadAttributePath & aPath, AttributeValueEncoder & aEncoder) +{ + VerifyOrDie(aPath.mClusterId == Clusters::AdministratorCommissioning::Id); + EndpointId endpointId = aPath.mEndpointId; + BridgedDevice * device = BridgeDeviceMgr().GetDevice(endpointId); + + if (!device) + { + VerifyOrDie(mOriginalAttributeInterface); + return mOriginalAttributeInterface->Read(aPath, aEncoder); + } + auto attr = device->GetAdminCommissioningAttributes(); + + switch (aPath.mAttributeId) + { + case Attributes::WindowStatus::Id: { + return aEncoder.Encode(attr.commissioningWindowStatus); + } + case Attributes::AdminFabricIndex::Id: { + DataModel::Nullable encodeableFabricIndex = DataModel::NullNullable; + if (attr.openerFabricIndex.has_value()) + { + encodeableFabricIndex.SetNonNull(attr.openerFabricIndex.value()); + } + return aEncoder.Encode(encodeableFabricIndex); + } + case Attributes::AdminVendorId::Id: { + DataModel::Nullable encodeableVendorId = DataModel::NullNullable; + if (attr.openerVendorId.has_value()) + { + encodeableVendorId.SetNonNull(attr.openerVendorId.value()); + } + return aEncoder.Encode(encodeableVendorId); + } + default: + break; + } + + return CHIP_NO_ERROR; +} diff --git a/examples/fabric-sync/bridge/src/BridgedDevice.cpp b/examples/fabric-sync/bridge/src/BridgedDevice.cpp new file mode 100644 index 00000000000000..f462d1cce6efd5 --- /dev/null +++ b/examples/fabric-sync/bridge/src/BridgedDevice.cpp @@ -0,0 +1,118 @@ +/* + * + * 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 "BridgedDevice.h" + +#include + +#include +#include +#include + +using namespace chip; +using namespace chip::app::Clusters::Actions; + +BridgedDevice::BridgedDevice(ScopedNodeId scopedNodeId) +{ + mReachable = false; + mScopedNodeId = scopedNodeId; + mEndpointId = kInvalidEndpointId; +} + +void BridgedDevice::LogActiveChangeEvent(uint32_t promisedActiveDurationMs) +{ + EndpointId endpointId = mEndpointId; + + DeviceLayer::SystemLayer().ScheduleLambda([endpointId, promisedActiveDurationMs]() { + app::Clusters::BridgedDeviceBasicInformation::Events::ActiveChanged::Type event{}; + event.promisedActiveDuration = promisedActiveDurationMs; + EventNumber eventNumber = 0; + + CHIP_ERROR err = app::LogEvent(event, endpointId, eventNumber); + if (err != CHIP_NO_ERROR) + { + ChipLogProgress(NotSpecified, "LogEvent for ActiveChanged failed %s", err.AsString()); + } + }); +} + +void BridgedDevice::SetReachable(bool reachable) +{ + mReachable = reachable; + + if (reachable) + { + ChipLogProgress(NotSpecified, "BridgedDevice[%s]: ONLINE", mAttributes.uniqueId.c_str()); + } + else + { + ChipLogProgress(NotSpecified, "BridgedDevice[%s]: OFFLINE", mAttributes.uniqueId.c_str()); + } +} + +void BridgedDevice::ReachableChanged(bool reachable) +{ + EndpointId endpointId = mEndpointId; + bool reachableChanged = (mReachable != reachable); + if (reachableChanged) + { + SetReachable(reachable); + DeviceLayer::SystemLayer().ScheduleLambda([endpointId]() { + MatterReportingAttributeChangeCallback(endpointId, app::Clusters::BridgedDeviceBasicInformation::Id, + app::Clusters::BridgedDeviceBasicInformation::Attributes::Reachable::Id); + + app::Clusters::BridgedDeviceBasicInformation::Events::ReachableChanged::Type event{}; + EventNumber eventNumber = 0; + + CHIP_ERROR err = app::LogEvent(event, endpointId, eventNumber); + if (err != CHIP_NO_ERROR) + { + ChipLogProgress(NotSpecified, "LogEvent for ActiveChanged failed %s", err.AsString()); + } + }); + } +} + +void BridgedDevice::SetAdminCommissioningAttributes(const AdminCommissioningAttributes & aAdminCommissioningAttributes) +{ + EndpointId endpointId = mEndpointId; + bool windowChanged = + (aAdminCommissioningAttributes.commissioningWindowStatus != mAdminCommissioningAttributes.commissioningWindowStatus); + bool fabricIndexChanged = (aAdminCommissioningAttributes.openerFabricIndex != mAdminCommissioningAttributes.openerFabricIndex); + bool vendorChanged = (aAdminCommissioningAttributes.openerVendorId != mAdminCommissioningAttributes.openerVendorId); + + mAdminCommissioningAttributes = aAdminCommissioningAttributes; + + DeviceLayer::SystemLayer().ScheduleLambda([endpointId, windowChanged, fabricIndexChanged, vendorChanged]() { + if (windowChanged) + { + MatterReportingAttributeChangeCallback(endpointId, app::Clusters::AdministratorCommissioning::Id, + app::Clusters::AdministratorCommissioning::Attributes::WindowStatus::Id); + } + if (fabricIndexChanged) + { + MatterReportingAttributeChangeCallback(endpointId, app::Clusters::AdministratorCommissioning::Id, + app::Clusters::AdministratorCommissioning::Attributes::AdminFabricIndex::Id); + } + if (vendorChanged) + { + MatterReportingAttributeChangeCallback(endpointId, app::Clusters::AdministratorCommissioning::Id, + app::Clusters::AdministratorCommissioning::Attributes::AdminVendorId::Id); + } + }); +} diff --git a/examples/fabric-sync/bridge/src/BridgedDeviceBasicInformationImpl.cpp b/examples/fabric-sync/bridge/src/BridgedDeviceBasicInformationImpl.cpp new file mode 100644 index 00000000000000..7fa48a4a158ddd --- /dev/null +++ b/examples/fabric-sync/bridge/src/BridgedDeviceBasicInformationImpl.cpp @@ -0,0 +1,107 @@ +/* + * Copyright (c) 2024 Project CHIP Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include "BridgedDeviceBasicInformationImpl.h" + +#include "BridgedDeviceManager.h" + +#include +#include +#include + +#include + +static constexpr unsigned kBridgedDeviceBasicInformationClusterRevision = 4; + +using namespace ::chip; +using namespace ::chip::app; +using namespace ::chip::app::Clusters; + +CHIP_ERROR BridgedDeviceBasicInformationImpl::Read(const ConcreteReadAttributePath & path, AttributeValueEncoder & encoder) +{ + // Registration is done for the bridged device basic information only + VerifyOrDie(path.mClusterId == app::Clusters::BridgedDeviceBasicInformation::Id); + + BridgedDevice * dev = BridgeDeviceMgr().GetDevice(path.mEndpointId); + VerifyOrReturnError(dev != nullptr, CHIP_ERROR_NOT_FOUND); + + switch (path.mAttributeId) + { + case BasicInformation::Attributes::Reachable::Id: + encoder.Encode(dev->IsReachable()); + break; + case BasicInformation::Attributes::NodeLabel::Id: + encoder.Encode(CharSpan::fromCharString(dev->GetBridgedAttributes().nodeLabel.c_str())); + break; + case BasicInformation::Attributes::ClusterRevision::Id: + encoder.Encode(kBridgedDeviceBasicInformationClusterRevision); + break; + case BasicInformation::Attributes::FeatureMap::Id: { + BitMask features; + features.Set(Clusters::BridgedDeviceBasicInformation::Feature::kBridgedICDSupport, dev->IsIcd()); + encoder.Encode(features); + } + break; + case BasicInformation::Attributes::UniqueID::Id: + encoder.Encode(CharSpan::fromCharString(dev->GetBridgedAttributes().uniqueId.c_str())); + break; + case BasicInformation::Attributes::VendorName::Id: + encoder.Encode(CharSpan::fromCharString(dev->GetBridgedAttributes().vendorName.c_str())); + break; + case BasicInformation::Attributes::VendorID::Id: + encoder.Encode(dev->GetBridgedAttributes().vendorId); + break; + case BasicInformation::Attributes::ProductName::Id: + encoder.Encode(CharSpan::fromCharString(dev->GetBridgedAttributes().productName.c_str())); + break; + case BasicInformation::Attributes::ProductID::Id: + encoder.Encode(dev->GetBridgedAttributes().productId); + break; + case BasicInformation::Attributes::HardwareVersion::Id: + encoder.Encode(dev->GetBridgedAttributes().hardwareVersion); + break; + case BasicInformation::Attributes::HardwareVersionString::Id: + encoder.Encode(CharSpan::fromCharString(dev->GetBridgedAttributes().hardwareVersionString.c_str())); + break; + case BasicInformation::Attributes::SoftwareVersion::Id: + encoder.Encode(dev->GetBridgedAttributes().softwareVersion); + break; + case BasicInformation::Attributes::SoftwareVersionString::Id: + encoder.Encode(CharSpan::fromCharString(dev->GetBridgedAttributes().softwareVersionString.c_str())); + break; + default: + return CHIP_ERROR_INVALID_ARGUMENT; + } + return CHIP_NO_ERROR; +} + +CHIP_ERROR BridgedDeviceBasicInformationImpl::Write(const ConcreteDataAttributePath & path, AttributeValueDecoder & decoder) +{ + VerifyOrDie(path.mClusterId == app::Clusters::BridgedDeviceBasicInformation::Id); + + BridgedDevice * dev = BridgeDeviceMgr().GetDevice(path.mEndpointId); + VerifyOrReturnError(dev != nullptr, CHIP_ERROR_NOT_FOUND); + + if (!dev->IsReachable()) + { + return CHIP_ERROR_NOT_CONNECTED; + } + + ChipLogProgress(NotSpecified, "Bridged device basic information attempt to write attribute: ep=%d", path.mAttributeId); + + // nothing writable right now ... + + return CHIP_ERROR_INVALID_ARGUMENT; +} diff --git a/examples/fabric-sync/bridge/src/BridgedDeviceManager.cpp b/examples/fabric-sync/bridge/src/BridgedDeviceManager.cpp new file mode 100644 index 00000000000000..0265f912027ca0 --- /dev/null +++ b/examples/fabric-sync/bridge/src/BridgedDeviceManager.cpp @@ -0,0 +1,345 @@ +/* + * + * 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 "BridgedDeviceManager.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +using namespace chip; +using namespace chip::app; +using namespace chip::Credentials; +using namespace chip::Inet; +using namespace chip::Transport; +using namespace chip::DeviceLayer; +using namespace chip::app::Clusters; + +namespace { + +constexpr uint8_t kMaxRetries = 10; +constexpr int kNodeLabelSize = 32; +constexpr int kUniqueIdSize = 32; +constexpr int kVendorNameSize = 32; +constexpr int kProductNameSize = 32; +constexpr int kHardwareVersionSize = 64; +constexpr int kSoftwareVersionSize = 64; + +// Current ZCL implementation of Struct uses a max-size array of 254 bytes +constexpr int kDescriptorAttributeArraySize = 254; + +#define ZCL_ADMINISTRATOR_COMMISSIONING_CLUSTER_REVISION (1u) + +// ENDPOINT DEFINITIONS: +// ================================================================================= +// +// Endpoint definitions will be reused across multiple endpoints for every instance of the +// endpoint type. +// There will be no intrinsic storage for the endpoint attributes declared here. +// Instead, all attributes will be treated as EXTERNAL, and therefore all reads +// or writes to the attributes must be handled within the emberAfExternalAttributeWriteCallback +// and emberAfExternalAttributeReadCallback functions declared herein. This fits +// the typical model of a bridge, since a bridge typically maintains its own +// state database representing the devices connected to it. + +// (taken from matter-devices.xml) +#define DEVICE_TYPE_BRIDGED_NODE 0x0013 + +// Device Version for dynamic endpoints: +#define DEVICE_VERSION_DEFAULT 1 + +// --------------------------------------------------------------------------- +// +// SYNCED DEVICE ENDPOINT: contains the following clusters: +// - Descriptor +// - Bridged Device Basic Information +// - Administrator Commissioning + +// clang-format off +// Declare Descriptor cluster attributes +DECLARE_DYNAMIC_ATTRIBUTE_LIST_BEGIN(descriptorAttrs) + DECLARE_DYNAMIC_ATTRIBUTE(Descriptor::Attributes::DeviceTypeList::Id, ARRAY, kDescriptorAttributeArraySize, 0), /* device list */ + DECLARE_DYNAMIC_ATTRIBUTE(Descriptor::Attributes::ServerList::Id, ARRAY, kDescriptorAttributeArraySize, 0), /* server list */ + DECLARE_DYNAMIC_ATTRIBUTE(Descriptor::Attributes::ClientList::Id, ARRAY, kDescriptorAttributeArraySize, 0), /* client list */ + DECLARE_DYNAMIC_ATTRIBUTE(Descriptor::Attributes::PartsList::Id, ARRAY, kDescriptorAttributeArraySize, 0), /* parts list */ +DECLARE_DYNAMIC_ATTRIBUTE_LIST_END(); + +// Declare Bridged Device Basic Information cluster attributes +DECLARE_DYNAMIC_ATTRIBUTE_LIST_BEGIN(bridgedDeviceBasicAttrs) + // The attributes below are MANDATORY in the Bridged Device Information Cluster + DECLARE_DYNAMIC_ATTRIBUTE(BridgedDeviceBasicInformation::Attributes::Reachable::Id, BOOLEAN, 1, 0), + DECLARE_DYNAMIC_ATTRIBUTE(BridgedDeviceBasicInformation::Attributes::UniqueID::Id, CHAR_STRING, kUniqueIdSize, 0), + DECLARE_DYNAMIC_ATTRIBUTE(BridgedDeviceBasicInformation::Attributes::FeatureMap::Id, BITMAP32, 4, 0), + + // The attributes below are OPTIONAL in the bridged device, however they are MANDATORY in the BasicInformation cluster + // so they can always be provided if desired + DECLARE_DYNAMIC_ATTRIBUTE(BridgedDeviceBasicInformation::Attributes::VendorName::Id, CHAR_STRING, kVendorNameSize, 0), + DECLARE_DYNAMIC_ATTRIBUTE(BridgedDeviceBasicInformation::Attributes::VendorID::Id, INT16U, 2, 0), + DECLARE_DYNAMIC_ATTRIBUTE(BridgedDeviceBasicInformation::Attributes::ProductName::Id, CHAR_STRING, kProductNameSize, 0), + DECLARE_DYNAMIC_ATTRIBUTE(BridgedDeviceBasicInformation::Attributes::ProductID::Id, INT16U, 2, 0), + DECLARE_DYNAMIC_ATTRIBUTE(BridgedDeviceBasicInformation::Attributes::NodeLabel::Id, CHAR_STRING, kNodeLabelSize, 0), + DECLARE_DYNAMIC_ATTRIBUTE(BridgedDeviceBasicInformation::Attributes::HardwareVersion::Id, INT16U, 2, 0), + DECLARE_DYNAMIC_ATTRIBUTE(BridgedDeviceBasicInformation::Attributes::HardwareVersionString::Id, CHAR_STRING, + kHardwareVersionSize, 0), + DECLARE_DYNAMIC_ATTRIBUTE(BridgedDeviceBasicInformation::Attributes::SoftwareVersion::Id, INT32U, 4, 0), + DECLARE_DYNAMIC_ATTRIBUTE(BridgedDeviceBasicInformation::Attributes::SoftwareVersionString::Id, CHAR_STRING, + kSoftwareVersionSize, 0), +DECLARE_DYNAMIC_ATTRIBUTE_LIST_END(); + +// Declare Ecosystem Information cluster attributes +DECLARE_DYNAMIC_ATTRIBUTE_LIST_BEGIN(ecosystemInformationBasicAttrs) + DECLARE_DYNAMIC_ATTRIBUTE(EcosystemInformation::Attributes::DeviceDirectory::Id, ARRAY, kDescriptorAttributeArraySize, 0), + DECLARE_DYNAMIC_ATTRIBUTE(EcosystemInformation::Attributes::LocationDirectory::Id, ARRAY, kDescriptorAttributeArraySize, 0), + DECLARE_DYNAMIC_ATTRIBUTE_LIST_END(); + +// Declare Administrator Commissioning cluster attributes +DECLARE_DYNAMIC_ATTRIBUTE_LIST_BEGIN(AdministratorCommissioningAttrs) + DECLARE_DYNAMIC_ATTRIBUTE(AdministratorCommissioning::Attributes::WindowStatus::Id, ENUM8, 1, 0), + DECLARE_DYNAMIC_ATTRIBUTE(AdministratorCommissioning::Attributes::AdminFabricIndex::Id, FABRIC_IDX, 1, 0), + DECLARE_DYNAMIC_ATTRIBUTE(AdministratorCommissioning::Attributes::AdminVendorId::Id, VENDOR_ID, 2, 0), + DECLARE_DYNAMIC_ATTRIBUTE(AdministratorCommissioning::Attributes::ClusterRevision::Id, INT16U, ZCL_ADMINISTRATOR_COMMISSIONING_CLUSTER_REVISION, 0), +DECLARE_DYNAMIC_ATTRIBUTE_LIST_END(); +// clang-format on + +constexpr CommandId bridgedDeviceBasicInformationCommands[] = { + app::Clusters::BridgedDeviceBasicInformation::Commands::KeepActive::Id, + kInvalidCommandId, +}; + +constexpr CommandId administratorCommissioningCommands[] = { + app::Clusters::AdministratorCommissioning::Commands::OpenCommissioningWindow::Id, + app::Clusters::AdministratorCommissioning::Commands::OpenBasicCommissioningWindow::Id, + app::Clusters::AdministratorCommissioning::Commands::RevokeCommissioning::Id, + kInvalidCommandId, +}; + +// clang-format off +// Declare Cluster List for Bridged Node endpoint +DECLARE_DYNAMIC_CLUSTER_LIST_BEGIN(bridgedNodeClusters) + DECLARE_DYNAMIC_CLUSTER(Descriptor::Id, descriptorAttrs, ZAP_CLUSTER_MASK(SERVER), nullptr, nullptr), + DECLARE_DYNAMIC_CLUSTER(BridgedDeviceBasicInformation::Id, bridgedDeviceBasicAttrs, ZAP_CLUSTER_MASK(SERVER), nullptr, nullptr), + DECLARE_DYNAMIC_CLUSTER(EcosystemInformation::Id, ecosystemInformationBasicAttrs, ZAP_CLUSTER_MASK(SERVER), nullptr, nullptr), + DECLARE_DYNAMIC_CLUSTER(AdministratorCommissioning::Id, AdministratorCommissioningAttrs, ZAP_CLUSTER_MASK(SERVER), + administratorCommissioningCommands, nullptr) +DECLARE_DYNAMIC_CLUSTER_LIST_END; + +DECLARE_DYNAMIC_CLUSTER_LIST_BEGIN(icdBridgedNodeClusters) + DECLARE_DYNAMIC_CLUSTER(Descriptor::Id, descriptorAttrs, ZAP_CLUSTER_MASK(SERVER), nullptr, nullptr), + DECLARE_DYNAMIC_CLUSTER(BridgedDeviceBasicInformation::Id, bridgedDeviceBasicAttrs, ZAP_CLUSTER_MASK(SERVER), + bridgedDeviceBasicInformationCommands, nullptr), + DECLARE_DYNAMIC_CLUSTER(AdministratorCommissioning::Id, AdministratorCommissioningAttrs, ZAP_CLUSTER_MASK(SERVER), + administratorCommissioningCommands, nullptr) +DECLARE_DYNAMIC_CLUSTER_LIST_END; +// clang-format on + +// Declare Bridged Node endpoint +DECLARE_DYNAMIC_ENDPOINT(sBridgedNodeEndpoint, bridgedNodeClusters); +DECLARE_DYNAMIC_ENDPOINT(sIcdBridgedNodeEndpoint, icdBridgedNodeClusters); + +// TODO: this is a single version array, however we may have many +// different clusters that are independent. +DataVersion sBridgedNodeDataVersions[ArraySize(bridgedNodeClusters)]; + +const EmberAfDeviceType sBridgedDeviceTypes[] = { { DEVICE_TYPE_BRIDGED_NODE, DEVICE_VERSION_DEFAULT } }; + +} // namespace + +// Define the static member +BridgedDeviceManager BridgedDeviceManager::sInstance; + +void BridgedDeviceManager::Init() +{ + mFirstDynamicEndpointId = static_cast( + static_cast(emberAfEndpointFromIndex(static_cast(emberAfFixedEndpointCount() - 1))) + 1); + mCurrentEndpointId = mFirstDynamicEndpointId; +} + +std::optional BridgedDeviceManager::AddDeviceEndpoint(std::unique_ptr dev, + chip::EndpointId parentEndpointId) +{ + EmberAfEndpointType * ep = dev->IsIcd() ? &sIcdBridgedNodeEndpoint : &sBridgedNodeEndpoint; + + const chip::Span & deviceTypeList = Span(sBridgedDeviceTypes); + + // TODO: this shares data version among different clusters, which seems incorrect + const chip::Span & dataVersionStorage = Span(sBridgedNodeDataVersions); + + if (dev->GetBridgedAttributes().uniqueId.empty()) + { + dev->SetUniqueId(GenerateUniqueId()); + } + else if (GetDeviceByUniqueId(dev->GetBridgedAttributes().uniqueId) != nullptr) + { + ChipLogProgress(NotSpecified, "A device with unique id '%s' already exists", dev->GetBridgedAttributes().uniqueId.c_str()); + return std::nullopt; + } + + for (unsigned index = 0; index < CHIP_DEVICE_CONFIG_DYNAMIC_ENDPOINT_COUNT; index++) + { + if (mDevices[index]) + { + continue; + } + + for (int retryCount = 0; retryCount < kMaxRetries; retryCount++) + { + DeviceLayer::StackLock lock; + dev->SetEndpointId(mCurrentEndpointId); + dev->SetParentEndpointId(parentEndpointId); + CHIP_ERROR err = + emberAfSetDynamicEndpoint(index, mCurrentEndpointId, ep, dataVersionStorage, deviceTypeList, parentEndpointId); + if (err == CHIP_NO_ERROR) + { + ChipLogProgress(NotSpecified, "Added device with Id=[%d:0x" ChipLogFormatX64 "] to dynamic endpoint %d (index=%d)", + dev->GetScopedNodeId().GetFabricIndex(), ChipLogValueX64(dev->GetScopedNodeId().GetNodeId()), + mCurrentEndpointId, index); + mDevices[index] = std::move(dev); + return index; + } + if (err != CHIP_ERROR_ENDPOINT_EXISTS) + { + return std::nullopt; // Return error as endpoint addition failed due to an error other than endpoint already exists + } + // Increment the endpoint ID and handle wrap condition + if (++mCurrentEndpointId < mFirstDynamicEndpointId) + { + mCurrentEndpointId = mFirstDynamicEndpointId; + } + } + ChipLogError(NotSpecified, "Failed to add dynamic endpoint after %d retries", kMaxRetries); + return std::nullopt; // Return error as all retries are exhausted + } + + ChipLogProgress(NotSpecified, "Failed to add dynamic endpoint: No endpoints available!"); + return std::nullopt; +} + +int BridgedDeviceManager::RemoveDeviceEndpoint(BridgedDevice * dev) +{ + uint8_t index = 0; + while (index < CHIP_DEVICE_CONFIG_DYNAMIC_ENDPOINT_COUNT) + { + if (mDevices[index].get() == dev) + { + DeviceLayer::StackLock lock; + // Silence complaints about unused ep when progress logging + // disabled. + [[maybe_unused]] EndpointId ep = emberAfClearDynamicEndpoint(index); + mDevices[index] = nullptr; + ChipLogProgress(NotSpecified, "Removed device %s from dynamic endpoint %d (index=%d)", + dev->GetBridgedAttributes().uniqueId.c_str(), ep, index); + return index; + } + index++; + } + return -1; +} + +BridgedDevice * BridgedDeviceManager::GetDevice(chip::EndpointId endpointId) const +{ + for (uint8_t index = 0; index < CHIP_DEVICE_CONFIG_DYNAMIC_ENDPOINT_COUNT; ++index) + { + if (mDevices[index] && mDevices[index]->GetEndpointId() == endpointId) + { + return mDevices[index].get(); + } + } + return nullptr; +} + +std::string BridgedDeviceManager::GenerateUniqueId() +{ + char rand_buffer[kUniqueIdSize + 1]; + memset(rand_buffer, 0, sizeof(rand_buffer)); + + static const char kRandCharChoices[] = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + + while (true) + { + /// for nice viewing, prefix the generated value + memcpy(rand_buffer, "GEN-", 4); + for (unsigned idx = 4; idx < kUniqueIdSize; idx++) + { + rand_buffer[idx] = kRandCharChoices[Crypto::GetRandU8() % (sizeof(kRandCharChoices) - 1)]; + } + + // we know zero-terminated due to the memset + std::string uniqueIdChoice = rand_buffer; + + if (!GetDeviceByUniqueId(uniqueIdChoice)) + { + return uniqueIdChoice; + } + } +} + +BridgedDevice * BridgedDeviceManager::GetDeviceByUniqueId(const std::string & id) +{ + for (uint8_t index = 0; index < CHIP_DEVICE_CONFIG_DYNAMIC_ENDPOINT_COUNT; ++index) + { + if (mDevices[index] && mDevices[index]->GetBridgedAttributes().uniqueId == id) + { + return mDevices[index].get(); + } + } + return nullptr; +} + +BridgedDevice * BridgedDeviceManager::GetDeviceByScopedNodeId(chip::ScopedNodeId scopedNodeId) const +{ + for (uint8_t index = 0; index < CHIP_DEVICE_CONFIG_DYNAMIC_ENDPOINT_COUNT; ++index) + { + if (mDevices[index] && mDevices[index]->GetScopedNodeId() == scopedNodeId) + { + return mDevices[index].get(); + } + } + return nullptr; +} + +std::optional BridgedDeviceManager::RemoveDeviceByScopedNodeId(chip::ScopedNodeId scopedNodeId) +{ + for (unsigned index = 0; index < CHIP_DEVICE_CONFIG_DYNAMIC_ENDPOINT_COUNT; ++index) + { + if (mDevices[index] && mDevices[index]->GetScopedNodeId() == scopedNodeId) + { + DeviceLayer::StackLock lock; + EndpointId ep = emberAfClearDynamicEndpoint(index); + mDevices[index] = nullptr; + ChipLogProgress(NotSpecified, "Removed device with Id=[%d:0x" ChipLogFormatX64 "] from dynamic endpoint %d (index=%d)", + scopedNodeId.GetFabricIndex(), ChipLogValueX64(scopedNodeId.GetNodeId()), ep, index); + return index; + } + } + return std::nullopt; +}