[Fabric-Sync] Integrate fabric bridge functionalities into fabric-sync (#36166)
diff --git a/examples/fabric-sync/bridge/BUILD.gn b/examples/fabric-sync/bridge/BUILD.gn
index cdf3671..310496b 100644
--- a/examples/fabric-sync/bridge/BUILD.gn
+++ b/examples/fabric-sync/bridge/BUILD.gn
@@ -40,7 +40,17 @@
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 0000000..06fd902
--- /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 <app-common/zap-generated/cluster-objects.h>
+#include <app/AttributeAccessInterfaceRegistry.h>
+
+/**
+ * @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 0000000..d2c5a64
--- /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 <app-common/zap-generated/cluster-objects.h>
+#include <app/util/attribute-storage.h>
+
+#include <string>
+
+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<chip::FabricIndex> openerFabricIndex = std::nullopt;
+ std::optional<chip::VendorId> 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 0000000..2340343
--- /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 <app-common/zap-generated/ids/Clusters.h>
+#include <app/AttributeAccessInterface.h>
+
+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 0000000..127898f
--- /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 <platform/CHIPDeviceLayer.h>
+
+#include "BridgedDevice.h"
+
+#include <memory>
+
+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<unsigned> AddDeviceEndpoint(std::unique_ptr<BridgedDevice> 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<unsigned> 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<BridgedDevice> 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 0000000..a0d87cb
--- /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 <app/AttributeAccessInterfaceRegistry.h>
+
+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<FabricIndex> encodeableFabricIndex = DataModel::NullNullable;
+ if (attr.openerFabricIndex.has_value())
+ {
+ encodeableFabricIndex.SetNonNull(attr.openerFabricIndex.value());
+ }
+ return aEncoder.Encode(encodeableFabricIndex);
+ }
+ case Attributes::AdminVendorId::Id: {
+ DataModel::Nullable<VendorId> 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 0000000..f462d1c
--- /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 <cstdio>
+
+#include <app/EventLogging.h>
+#include <app/reporting/reporting.h>
+#include <platform/CHIPDeviceLayer.h>
+
+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 0000000..7fa48a4
--- /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 <app-common/zap-generated/cluster-objects.h>
+#include <app-common/zap-generated/ids/Attributes.h>
+#include <app-common/zap-generated/ids/Clusters.h>
+
+#include <app/AttributeAccessInterfaceRegistry.h>
+
+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<Clusters::BridgedDeviceBasicInformation::Feature> 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 0000000..0265f91
--- /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 <app-common/zap-generated/ids/Attributes.h>
+#include <app-common/zap-generated/ids/Clusters.h>
+#include <app/AttributeAccessInterfaceRegistry.h>
+#include <app/ConcreteAttributePath.h>
+#include <app/EventLogging.h>
+#include <app/reporting/reporting.h>
+#include <app/server/Server.h>
+#include <app/util/af-types.h>
+#include <app/util/attribute-storage.h>
+#include <app/util/endpoint-config-api.h>
+#include <app/util/util.h>
+#include <crypto/RandUtils.h>
+#include <lib/support/CHIPMem.h>
+#include <lib/support/ZclString.h>
+
+#include <cstdio>
+#include <optional>
+#include <string>
+
+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<chip::EndpointId>(
+ static_cast<int>(emberAfEndpointFromIndex(static_cast<uint16_t>(emberAfFixedEndpointCount() - 1))) + 1);
+ mCurrentEndpointId = mFirstDynamicEndpointId;
+}
+
+std::optional<unsigned> BridgedDeviceManager::AddDeviceEndpoint(std::unique_ptr<BridgedDevice> dev,
+ chip::EndpointId parentEndpointId)
+{
+ EmberAfEndpointType * ep = dev->IsIcd() ? &sIcdBridgedNodeEndpoint : &sBridgedNodeEndpoint;
+
+ const chip::Span<const EmberAfDeviceType> & deviceTypeList = Span<const EmberAfDeviceType>(sBridgedDeviceTypes);
+
+ // TODO: this shares data version among different clusters, which seems incorrect
+ const chip::Span<chip::DataVersion> & dataVersionStorage = Span<DataVersion>(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<unsigned> 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;
+}