[Fabric-Sync] Port 'add-bridge' and 'remove-bridge' commands (#36241)
* [Fabric-Sync] Implemenat 'add-bridge' command
* Address review comments
diff --git a/examples/fabric-sync/BUILD.gn b/examples/fabric-sync/BUILD.gn
index de193bd..95b0ce3 100644
--- a/examples/fabric-sync/BUILD.gn
+++ b/examples/fabric-sync/BUILD.gn
@@ -22,21 +22,35 @@
executable("fabric-sync") {
cflags = [ "-Wconversion" ]
+ defines = []
+
+ if (chip_build_libshell) {
+ defines += [ "ENABLE_CHIP_SHELL" ]
+ }
+
include_dirs = [
".",
"${chip_root}/src/lib",
]
+ if (chip_build_libshell) {
+ include_dirs += [ "shell" ]
+ }
+
sources = [ "main.cpp" ]
deps = [
+ "${chip_root}/examples/fabric-sync/admin:fabric-admin-lib",
"${chip_root}/examples/fabric-sync/bridge:fabric-bridge-lib",
- "${chip_root}/examples/fabric-sync/bridge:fabric-bridge-zap",
"${chip_root}/examples/platform/linux:app-main",
"${chip_root}/src/lib",
"${chip_root}/third_party/inipp",
]
+ if (chip_build_libshell) {
+ deps += [ "${chip_root}/examples/fabric-sync/shell" ]
+ }
+
output_dir = root_out_dir
}
diff --git a/examples/fabric-sync/admin/BUILD.gn b/examples/fabric-sync/admin/BUILD.gn
new file mode 100644
index 0000000..c2ab715
--- /dev/null
+++ b/examples/fabric-sync/admin/BUILD.gn
@@ -0,0 +1,30 @@
+# 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.
+
+import("//build_overrides/chip.gni")
+import("${chip_root}/src/app/chip_data_model.gni")
+
+source_set("fabric-admin-lib") {
+ sources = [
+ "DeviceManager.cpp",
+ "DeviceManager.h",
+ "PairingManager.cpp",
+ "PairingManager.h",
+ ]
+
+ deps = [
+ "${chip_root}/examples/fabric-sync/bridge:fabric-bridge-lib",
+ "${chip_root}/src/lib",
+ ]
+}
diff --git a/examples/fabric-sync/admin/DeviceManager.cpp b/examples/fabric-sync/admin/DeviceManager.cpp
new file mode 100644
index 0000000..79f41d5
--- /dev/null
+++ b/examples/fabric-sync/admin/DeviceManager.cpp
@@ -0,0 +1,111 @@
+/*
+ *
+ * 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 "DeviceManager.h"
+
+#include <crypto/RandUtils.h>
+#include <lib/support/StringBuilder.h>
+
+#include <cstdio>
+#include <string>
+
+using namespace chip;
+
+// Define the static member
+DeviceManager DeviceManager::sInstance;
+
+void DeviceManager::Init()
+{
+ // TODO: (#34113) Init mLastUsedNodeId from chip config file
+ mLastUsedNodeId = 1;
+ mInitialized = true;
+
+ ChipLogProgress(NotSpecified, "DeviceManager initialized: last used nodeId " ChipLogFormatX64,
+ ChipLogValueX64(mLastUsedNodeId));
+}
+
+NodeId DeviceManager::GetNextAvailableNodeId()
+{
+ mLastUsedNodeId++;
+ VerifyOrDieWithMsg(mLastUsedNodeId < std::numeric_limits<NodeId>::max(), NotSpecified, "No more available NodeIds.");
+
+ return mLastUsedNodeId;
+}
+
+void DeviceManager::UpdateLastUsedNodeId(NodeId nodeId)
+{
+ if (nodeId > mLastUsedNodeId)
+ {
+ mLastUsedNodeId = nodeId;
+ ChipLogProgress(NotSpecified, "Updating last used NodeId to " ChipLogFormatX64, ChipLogValueX64(mLastUsedNodeId));
+ }
+}
+
+void DeviceManager::SetRemoteBridgeNodeId(chip::NodeId nodeId)
+{
+ mRemoteBridgeNodeId = nodeId;
+}
+
+CHIP_ERROR DeviceManager::PairRemoteFabricBridge(NodeId nodeId, uint32_t setupPINCode, const char * deviceRemoteIp,
+ uint16_t deviceRemotePort)
+{
+ CHIP_ERROR err = PairingManager::Instance().PairDevice(nodeId, setupPINCode, deviceRemoteIp, deviceRemotePort);
+
+ if (err != CHIP_NO_ERROR)
+ {
+ ChipLogError(NotSpecified,
+ "Failed to pair remote fabric bridge: Node ID " ChipLogFormatX64 " with error: %" CHIP_ERROR_FORMAT,
+ ChipLogValueX64(nodeId), err.Format());
+ return err;
+ }
+
+ ChipLogProgress(NotSpecified, "Successfully paired remote fabric bridge: Node ID " ChipLogFormatX64, ChipLogValueX64(nodeId));
+ return CHIP_NO_ERROR;
+}
+
+CHIP_ERROR DeviceManager::UnpairRemoteFabricBridge()
+{
+ if (mRemoteBridgeNodeId == kUndefinedNodeId)
+ {
+ ChipLogError(NotSpecified, "Remote bridge node ID is undefined; cannot unpair device.");
+ return CHIP_ERROR_INCORRECT_STATE;
+ }
+
+ CHIP_ERROR err = PairingManager::Instance().UnpairDevice(mRemoteBridgeNodeId);
+ if (err != CHIP_NO_ERROR)
+ {
+ ChipLogError(NotSpecified, "Failed to unpair remote bridge device " ChipLogFormatX64, ChipLogValueX64(mRemoteBridgeNodeId));
+ return err;
+ }
+
+ ChipLogProgress(NotSpecified, "Successfully unpaired remote fabric bridge: Node ID " ChipLogFormatX64,
+ ChipLogValueX64(mRemoteBridgeNodeId));
+ return CHIP_NO_ERROR;
+}
+
+void DeviceManager::OnDeviceRemoved(NodeId deviceId, CHIP_ERROR err)
+{
+ if (err != CHIP_NO_ERROR)
+ {
+ ChipLogError(NotSpecified, "Failed to remove synced device:(" ChipLogFormatX64 ") with error: %" CHIP_ERROR_FORMAT,
+ ChipLogValueX64(deviceId), err.Format());
+ return;
+ }
+
+ ChipLogProgress(NotSpecified, "Synced device with NodeId:" ChipLogFormatX64 " has been removed.", ChipLogValueX64(deviceId));
+}
diff --git a/examples/fabric-sync/admin/DeviceManager.h b/examples/fabric-sync/admin/DeviceManager.h
new file mode 100644
index 0000000..5615df5
--- /dev/null
+++ b/examples/fabric-sync/admin/DeviceManager.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 "PairingManager.h"
+
+#include <app-common/zap-generated/cluster-objects.h>
+#include <platform/CHIPDeviceLayer.h>
+
+class DeviceManager : public PairingDelegate
+{
+public:
+ DeviceManager() = default;
+
+ void Init();
+
+ chip::NodeId GetNextAvailableNodeId();
+
+ chip::NodeId GetRemoteBridgeNodeId() const { return mRemoteBridgeNodeId; }
+
+ void UpdateLastUsedNodeId(chip::NodeId nodeId);
+
+ void SetRemoteBridgeNodeId(chip::NodeId nodeId);
+
+ bool IsFabricSyncReady() const { return mRemoteBridgeNodeId != chip::kUndefinedNodeId; }
+
+ /**
+ * @brief Determines whether a given nodeId corresponds to the remote bridge device.
+ *
+ * @param nodeId The ID of the node being checked.
+ *
+ * @return true if the nodeId matches the remote bridge device; otherwise, false.
+ */
+ bool IsCurrentBridgeDevice(chip::NodeId nodeId) const { return nodeId == mRemoteBridgeNodeId; }
+
+ /**
+ * @brief Pair a remote fabric bridge with a given node ID.
+ *
+ * This function initiates the pairing process for a remote fabric bridge using the specified parameters.
+
+ * @param nodeId The user-defined ID for the node being commissioned. It doesn’t need to be the same ID,
+ * as for the first fabric.
+ * @param setupPINCode The setup PIN code used to authenticate the pairing process.
+ * @param deviceRemoteIp The IP address of the remote device that is being paired as part of the fabric bridge.
+ * @param deviceRemotePort The secured device port of the remote device that is being paired as part of the fabric bridge.
+ *
+ * @return CHIP_ERROR Returns CHIP_NO_ERROR on success or an appropriate error code on failure.
+ */
+ CHIP_ERROR PairRemoteFabricBridge(chip::NodeId nodeId, uint32_t setupPINCode, const char * deviceRemoteIp,
+ uint16_t deviceRemotePort);
+
+ CHIP_ERROR UnpairRemoteFabricBridge();
+
+private:
+ friend DeviceManager & DeviceMgr();
+
+ void OnDeviceRemoved(chip::NodeId deviceId, CHIP_ERROR err) override;
+
+ static DeviceManager sInstance;
+
+ chip::NodeId mLastUsedNodeId = 0;
+
+ // The Node ID of the remote bridge used for Fabric-Sync
+ // This represents the bridge on the other ecosystem.
+ chip::NodeId mRemoteBridgeNodeId = chip::kUndefinedNodeId;
+
+ bool mInitialized = false;
+};
+
+/**
+ * Returns the public interface of the DeviceManager singleton object.
+ *
+ * Applications should use this to access features of the DeviceManager
+ * object.
+ */
+inline DeviceManager & DeviceMgr()
+{
+ if (!DeviceManager::sInstance.mInitialized)
+ {
+ DeviceManager::sInstance.Init();
+ }
+ return DeviceManager::sInstance;
+}
diff --git a/examples/fabric-sync/admin/PairingManager.cpp b/examples/fabric-sync/admin/PairingManager.cpp
new file mode 100644
index 0000000..06c1f2b
--- /dev/null
+++ b/examples/fabric-sync/admin/PairingManager.cpp
@@ -0,0 +1,550 @@
+/*
+ *
+ * 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 "PairingManager.h"
+
+#include <netdb.h>
+#include <sys/socket.h>
+#include <sys/types.h>
+
+#include <lib/support/logging/CHIPLogging.h>
+#include <setup_payload/ManualSetupPayloadParser.h>
+#include <setup_payload/QRCodeSetupPayloadParser.h>
+
+using namespace ::chip;
+using namespace ::chip::Controller;
+
+namespace {
+
+CHIP_ERROR GetPayload(const char * setUpCode, SetupPayload & payload)
+{
+ VerifyOrReturnValue(setUpCode, CHIP_ERROR_INVALID_ARGUMENT);
+ bool isQRCode = strncmp(setUpCode, kQRCodePrefix, strlen(kQRCodePrefix)) == 0;
+ if (isQRCode)
+ {
+ ReturnErrorOnFailure(QRCodeSetupPayloadParser(setUpCode).populatePayload(payload));
+ VerifyOrReturnError(payload.isValidQRCodePayload(), CHIP_ERROR_INVALID_ARGUMENT);
+ }
+ else
+ {
+ ReturnErrorOnFailure(ManualSetupPayloadParser(setUpCode).populatePayload(payload));
+ VerifyOrReturnError(payload.isValidManualCode(), CHIP_ERROR_INVALID_ARGUMENT);
+ }
+
+ return CHIP_NO_ERROR;
+}
+
+bool ParseAddressWithInterface(const char * addressString, Inet::IPAddress & address, Inet::InterfaceId & interfaceId)
+{
+ struct addrinfo hints;
+ struct addrinfo * result;
+ int ret;
+
+ memset(&hints, 0, sizeof(hints));
+ hints.ai_family = AF_UNSPEC;
+ hints.ai_socktype = SOCK_DGRAM;
+ ret = getaddrinfo(addressString, nullptr, &hints, &result);
+ if (ret < 0)
+ {
+ ChipLogError(NotSpecified, "Invalid address: %s", addressString);
+ return false;
+ }
+
+ if (result->ai_family == AF_INET6)
+ {
+ struct sockaddr_in6 * addr = reinterpret_cast<struct sockaddr_in6 *>(result->ai_addr);
+ address = Inet::IPAddress::FromSockAddr(*addr);
+ interfaceId = Inet::InterfaceId(addr->sin6_scope_id);
+ }
+#if INET_CONFIG_ENABLE_IPV4
+ else if (result->ai_family == AF_INET)
+ {
+ address = Inet::IPAddress::FromSockAddr(*reinterpret_cast<struct sockaddr_in *>(result->ai_addr));
+ interfaceId = Inet::InterfaceId::Null();
+ }
+#endif // INET_CONFIG_ENABLE_IPV4
+ else
+ {
+ ChipLogError(NotSpecified, "Unsupported address: %s", addressString);
+ freeaddrinfo(result);
+ return false;
+ }
+
+ freeaddrinfo(result);
+ return true;
+}
+
+} // namespace
+
+PairingManager::PairingManager() :
+ mOnOpenCommissioningWindowCallback(OnOpenCommissioningWindowResponse, this),
+ mOnOpenCommissioningWindowVerifierCallback(OnOpenCommissioningWindowVerifierResponse, this),
+ mCurrentFabricRemoveCallback(OnCurrentFabricRemove, this)
+{}
+
+CHIP_ERROR PairingManager::Init(Controller::DeviceCommissioner * commissioner)
+{
+ VerifyOrReturnError(commissioner != nullptr, CHIP_ERROR_INCORRECT_STATE);
+
+ mCommissioner = commissioner;
+
+ return CHIP_NO_ERROR;
+}
+
+CHIP_ERROR PairingManager::OpenCommissioningWindow(NodeId nodeId, EndpointId endpointId, uint16_t commissioningTimeoutSec,
+ uint32_t iterations, uint16_t discriminator, const ByteSpan & salt,
+ const ByteSpan & verifier)
+{
+ if (mCommissioner == nullptr)
+ {
+ ChipLogError(NotSpecified, "Commissioner is null, cannot open commissioning window");
+ return CHIP_ERROR_INCORRECT_STATE;
+ }
+
+ // Check if a window is already open
+ if (mWindowOpener != nullptr)
+ {
+ ChipLogError(NotSpecified, "A commissioning window is already open");
+ return CHIP_ERROR_INCORRECT_STATE;
+ }
+
+ // Ensure salt and verifier sizes are valid
+ if (!salt.empty() && salt.size() > chip::Crypto::kSpake2p_Max_PBKDF_Salt_Length)
+ {
+ ChipLogError(NotSpecified, "Salt size exceeds buffer capacity");
+ return CHIP_ERROR_BUFFER_TOO_SMALL;
+ }
+
+ if (!verifier.empty() && verifier.size() > chip::Crypto::kSpake2p_VerifierSerialized_Length)
+ {
+ ChipLogError(NotSpecified, "Verifier size exceeds buffer capacity");
+ return CHIP_ERROR_BUFFER_TOO_SMALL;
+ }
+
+ if (!salt.empty())
+ {
+ memcpy(mSaltBuffer, salt.data(), salt.size());
+ mSalt = ByteSpan(mSaltBuffer, salt.size());
+ }
+ else
+ {
+ mSalt = ByteSpan();
+ }
+
+ if (!verifier.empty())
+ {
+ memcpy(mVerifierBuffer, verifier.data(), verifier.size());
+ mVerifier = ByteSpan(mVerifierBuffer, verifier.size());
+ }
+ else
+ {
+ mVerifier = ByteSpan();
+ }
+
+ return DeviceLayer::SystemLayer().ScheduleLambda([nodeId, endpointId, commissioningTimeoutSec, iterations, discriminator]() {
+ PairingManager & self = PairingManager::Instance();
+
+ if (self.mCommissioner == nullptr)
+ {
+ ChipLogError(NotSpecified, "Commissioner is null, cannot open commissioning window");
+ return;
+ }
+
+ self.mWindowOpener = Platform::MakeUnique<Controller::CommissioningWindowOpener>(self.mCommissioner);
+
+ if (!self.mVerifier.empty())
+ {
+ if (self.mSalt.empty())
+ {
+ ChipLogError(NotSpecified, "Salt is required when verifier is set");
+ self.mWindowOpener.reset();
+ return;
+ }
+
+ // Open the commissioning window with verifier parameters
+ CHIP_ERROR err =
+ self.mWindowOpener->OpenCommissioningWindow(Controller::CommissioningWindowVerifierParams()
+ .SetNodeId(nodeId)
+ .SetEndpointId(endpointId)
+ .SetTimeout(commissioningTimeoutSec)
+ .SetIteration(iterations)
+ .SetDiscriminator(discriminator)
+ .SetVerifier(self.mVerifier)
+ .SetSalt(self.mSalt)
+ .SetCallback(&self.mOnOpenCommissioningWindowVerifierCallback));
+ if (err != CHIP_NO_ERROR)
+ {
+ ChipLogError(NotSpecified, "Failed to open commissioning window with verifier: %s", ErrorStr(err));
+ self.mWindowOpener.reset();
+ }
+ }
+ else
+ {
+ SetupPayload ignored;
+ // Open the commissioning window with passcode parameters
+ CHIP_ERROR err = self.mWindowOpener->OpenCommissioningWindow(Controller::CommissioningWindowPasscodeParams()
+ .SetNodeId(nodeId)
+ .SetEndpointId(endpointId)
+ .SetTimeout(commissioningTimeoutSec)
+ .SetIteration(iterations)
+ .SetDiscriminator(discriminator)
+ .SetSetupPIN(NullOptional)
+ .SetSalt(NullOptional)
+ .SetCallback(&self.mOnOpenCommissioningWindowCallback),
+ ignored);
+ if (err != CHIP_NO_ERROR)
+ {
+ ChipLogError(NotSpecified, "Failed to open commissioning window with passcode: %s", ErrorStr(err));
+ self.mWindowOpener.reset();
+ }
+ }
+ });
+}
+
+void PairingManager::OnOpenCommissioningWindowResponse(void * context, NodeId remoteId, CHIP_ERROR err, SetupPayload payload)
+{
+ VerifyOrDie(context != nullptr);
+ PairingManager * self = static_cast<PairingManager *>(context);
+ if (self->mCommissioningWindowDelegate)
+ {
+ self->mCommissioningWindowDelegate->OnCommissioningWindowOpened(remoteId, err, payload);
+ self->SetOpenCommissioningWindowDelegate(nullptr);
+ }
+
+ OnOpenCommissioningWindowVerifierResponse(context, remoteId, err);
+}
+
+void PairingManager::OnOpenCommissioningWindowVerifierResponse(void * context, NodeId remoteId, CHIP_ERROR err)
+{
+ VerifyOrDie(context != nullptr);
+ PairingManager * self = static_cast<PairingManager *>(context);
+ LogErrorOnFailure(err);
+
+ // Reset the window opener once the window operation is complete
+ self->mWindowOpener.reset();
+}
+
+void PairingManager::OnStatusUpdate(DevicePairingDelegate::Status status)
+{
+ switch (status)
+ {
+ case DevicePairingDelegate::Status::SecurePairingSuccess:
+ ChipLogProgress(NotSpecified, "CASE establishment successful");
+ break;
+ case DevicePairingDelegate::Status::SecurePairingFailed:
+ ChipLogError(NotSpecified, "Secure Pairing Failed");
+ break;
+ }
+}
+
+void PairingManager::OnPairingComplete(CHIP_ERROR err)
+{
+ if (err == CHIP_NO_ERROR)
+ {
+ ChipLogProgress(NotSpecified, "PASE establishment successful");
+ }
+ else
+ {
+ ChipLogProgress(NotSpecified, "Pairing Failure: %s", ErrorStr(err));
+ }
+}
+
+void PairingManager::OnPairingDeleted(CHIP_ERROR err)
+{
+ if (err == CHIP_NO_ERROR)
+ {
+ ChipLogProgress(NotSpecified, "Pairing Deleted Success");
+ }
+ else
+ {
+ ChipLogProgress(NotSpecified, "Pairing Deleted Failure: %s", ErrorStr(err));
+ }
+}
+
+void PairingManager::OnCommissioningComplete(NodeId nodeId, CHIP_ERROR err)
+{
+ if (err == CHIP_NO_ERROR)
+ {
+ // print to console
+ fprintf(stderr, "New device with Node ID: " ChipLogFormatX64 " has been successfully added.\n", ChipLogValueX64(nodeId));
+ }
+ else
+ {
+ ChipLogProgress(NotSpecified, "Device commissioning Failure: %s", ErrorStr(err));
+ }
+
+ if (mCommissioningDelegate)
+ {
+ mCommissioningDelegate->OnCommissioningComplete(nodeId, err);
+ SetCommissioningDelegate(nullptr);
+ }
+}
+
+void PairingManager::OnReadCommissioningInfo(const Controller::ReadCommissioningInfo & info)
+{
+ ChipLogProgress(AppServer, "OnReadCommissioningInfo - vendorId=0x%04X productId=0x%04X", info.basic.vendorId,
+ info.basic.productId);
+
+ // The string in CharSpan received from the device is not null-terminated, we use std::string here for coping and
+ // appending a null-terminator at the end of the string.
+ std::string userActiveModeTriggerInstruction;
+
+ // Note: the callback doesn't own the buffer, should make a copy if it will be used it later.
+ if (info.icd.userActiveModeTriggerInstruction.size() != 0)
+ {
+ userActiveModeTriggerInstruction =
+ std::string(info.icd.userActiveModeTriggerInstruction.data(), info.icd.userActiveModeTriggerInstruction.size());
+ }
+
+ if (info.icd.userActiveModeTriggerHint.HasAny())
+ {
+ ChipLogProgress(AppServer, "OnReadCommissioningInfo - LIT UserActiveModeTriggerHint=0x%08x",
+ info.icd.userActiveModeTriggerHint.Raw());
+ ChipLogProgress(AppServer, "OnReadCommissioningInfo - LIT UserActiveModeTriggerInstruction=%s",
+ userActiveModeTriggerInstruction.c_str());
+ }
+ ChipLogProgress(AppServer, "OnReadCommissioningInfo ICD - IdleModeDuration=%u activeModeDuration=%u activeModeThreshold=%u",
+ info.icd.idleModeDuration, info.icd.activeModeDuration, info.icd.activeModeThreshold);
+}
+
+void PairingManager::OnDiscoveredDevice(const Dnssd::CommissionNodeData & nodeData)
+{
+ // Ignore nodes with closed commissioning window
+ VerifyOrReturn(nodeData.commissioningMode != 0);
+
+ auto & resolutionData = nodeData;
+
+ const uint16_t port = resolutionData.port;
+ char buf[Inet::IPAddress::kMaxStringLength];
+ resolutionData.ipAddress[0].ToString(buf);
+ ChipLogProgress(NotSpecified, "Discovered Device: %s:%u", buf, port);
+
+ // Stop Mdns discovery.
+ auto err = mCommissioner->StopCommissionableDiscovery();
+
+ // Some platforms does not implement a mechanism to stop mdns browse, so
+ // we just ignore CHIP_ERROR_NOT_IMPLEMENTED instead of bailing out.
+ if (CHIP_NO_ERROR != err && CHIP_ERROR_NOT_IMPLEMENTED != err)
+ {
+ return;
+ }
+
+ mCommissioner->RegisterDeviceDiscoveryDelegate(nullptr);
+
+ auto interfaceId = resolutionData.ipAddress[0].IsIPv6LinkLocal() ? resolutionData.interfaceId : Inet::InterfaceId::Null();
+ auto peerAddress = Transport::PeerAddress::UDP(resolutionData.ipAddress[0], port, interfaceId);
+ err = Pair(mNodeId, peerAddress);
+ if (CHIP_NO_ERROR != err)
+ {
+ ChipLogProgress(NotSpecified, "Failed to pair device: " ChipLogFormatX64 " %s", ChipLogValueX64(mNodeId), ErrorStr(err));
+ }
+}
+
+Optional<uint16_t> PairingManager::FailSafeExpiryTimeoutSecs() const
+{
+ // No manual input, so do not need to extend.
+ return Optional<uint16_t>();
+}
+
+bool PairingManager::ShouldWaitAfterDeviceAttestation()
+{
+ // If there is a vendor ID and product ID, request OnDeviceAttestationCompleted().
+ // Currently this is added in the case that the example is performing reverse commissioning,
+ // but it would be an improvement to store that explicitly.
+ // TODO: Issue #35297 - [Fabric Sync] Improve where we get VID and PID when validating CCTRL CommissionNode command
+ SetupPayload payload;
+ CHIP_ERROR err = GetPayload(mOnboardingPayload, payload);
+ return err == CHIP_NO_ERROR && (payload.vendorID != 0 || payload.productID != 0);
+}
+
+void PairingManager::OnDeviceAttestationCompleted(Controller::DeviceCommissioner * deviceCommissioner, DeviceProxy * device,
+ const Credentials::DeviceAttestationVerifier::AttestationDeviceInfo & info,
+ Credentials::AttestationVerificationResult attestationResult)
+{
+ SetupPayload payload;
+ CHIP_ERROR parse_error = GetPayload(mOnboardingPayload, payload);
+ if (parse_error == CHIP_NO_ERROR && (payload.vendorID != 0 || payload.productID != 0))
+ {
+ if (payload.vendorID == 0 || payload.productID == 0)
+ {
+ ChipLogProgress(NotSpecified,
+ "Failed validation: vendorID or productID must not be 0."
+ "Requested VID: %u, Requested PID: %u.",
+ payload.vendorID, payload.productID);
+ deviceCommissioner->ContinueCommissioningAfterDeviceAttestation(
+ device, Credentials::AttestationVerificationResult::kInvalidArgument);
+ return;
+ }
+
+ if (payload.vendorID != info.BasicInformationVendorId() || payload.productID != info.BasicInformationProductId())
+ {
+ ChipLogProgress(NotSpecified,
+ "Failed validation of vendorID or productID."
+ "Requested VID: %u, Requested PID: %u,"
+ "Detected VID: %u, Detected PID %u.",
+ payload.vendorID, payload.productID, info.BasicInformationVendorId(), info.BasicInformationProductId());
+ deviceCommissioner->ContinueCommissioningAfterDeviceAttestation(
+ device,
+ payload.vendorID == info.BasicInformationVendorId()
+ ? Credentials::AttestationVerificationResult::kDacProductIdMismatch
+ : Credentials::AttestationVerificationResult::kDacVendorIdMismatch);
+ return;
+ }
+
+ // NOTE: This will log errors even if the attestion was successful.
+ CHIP_ERROR err = deviceCommissioner->ContinueCommissioningAfterDeviceAttestation(device, attestationResult);
+ if (CHIP_NO_ERROR != err)
+ {
+ ChipLogError(NotSpecified, "Failed to continue commissioning after device attestation, error: %s", ErrorStr(err));
+ }
+ return;
+ }
+
+ // Don't bypass attestation, continue with error.
+ CHIP_ERROR err = deviceCommissioner->ContinueCommissioningAfterDeviceAttestation(device, attestationResult);
+ if (CHIP_NO_ERROR != err)
+ {
+ ChipLogError(NotSpecified, "Failed to continue commissioning after device attestation, error: %s", ErrorStr(err));
+ }
+}
+
+CommissioningParameters PairingManager::GetCommissioningParameters()
+{
+ auto params = CommissioningParameters();
+ params.SetSkipCommissioningComplete(false);
+ params.SetDeviceAttestationDelegate(this);
+
+ return params;
+}
+
+CHIP_ERROR PairingManager::Pair(NodeId remoteId, Transport::PeerAddress address)
+{
+ auto params = RendezvousParameters().SetSetupPINCode(mSetupPINCode).SetDiscriminator(mDiscriminator).SetPeerAddress(address);
+
+ CHIP_ERROR err = CHIP_NO_ERROR;
+ auto commissioningParams = GetCommissioningParameters();
+ err = CurrentCommissioner().PairDevice(remoteId, params, commissioningParams);
+
+ return err;
+}
+
+void PairingManager::OnCurrentFabricRemove(void * context, NodeId nodeId, CHIP_ERROR err)
+{
+ PairingManager * self = reinterpret_cast<PairingManager *>(context);
+ VerifyOrReturn(self != nullptr, ChipLogError(NotSpecified, "OnCurrentFabricRemove: context is null"));
+
+ if (err == CHIP_NO_ERROR)
+ {
+ // print to console
+ fprintf(stderr, "Device with Node ID: " ChipLogFormatX64 "has been successfully removed.\n", ChipLogValueX64(nodeId));
+ }
+ else
+ {
+ ChipLogProgress(NotSpecified, "Device unpair Failure: " ChipLogFormatX64 " %s", ChipLogValueX64(nodeId), ErrorStr(err));
+ }
+}
+
+void PairingManager::InitPairingCommand()
+{
+ mCommissioner->RegisterPairingDelegate(this);
+}
+
+CHIP_ERROR PairingManager::PairDeviceWithCode(NodeId nodeId, const char * payload)
+{
+ if (payload == nullptr || strlen(payload) > kMaxManualCodeLength + 1)
+ {
+ ChipLogError(NotSpecified, "PairDeviceWithCode failed: Invalid pairing payload");
+ return CHIP_ERROR_INVALID_STRING_LENGTH;
+ }
+
+ Platform::CopyString(mOnboardingPayload, sizeof(mOnboardingPayload), payload);
+
+ return DeviceLayer::SystemLayer().ScheduleLambda([nodeId]() {
+ PairingManager & self = PairingManager::Instance();
+
+ self.InitPairingCommand();
+
+ CommissioningParameters commissioningParams = self.GetCommissioningParameters();
+ auto discoveryType = DiscoveryType::kDiscoveryNetworkOnly;
+
+ self.mNodeId = nodeId;
+
+ CHIP_ERROR err = self.mCommissioner->PairDevice(nodeId, self.mOnboardingPayload, commissioningParams, discoveryType);
+ if (err != CHIP_NO_ERROR)
+ {
+ ChipLogError(NotSpecified, "Failed to pair device with code, error: %s", ErrorStr(err));
+ }
+ });
+}
+
+CHIP_ERROR PairingManager::PairDevice(chip::NodeId nodeId, uint32_t setupPINCode, const char * deviceRemoteIp,
+ uint16_t deviceRemotePort)
+{
+ if (deviceRemoteIp == nullptr || strlen(deviceRemoteIp) > Inet::IPAddress::kMaxStringLength)
+ {
+ ChipLogError(NotSpecified, "PairDevice failed: Invalid device remote IP address");
+ return CHIP_ERROR_INVALID_STRING_LENGTH;
+ }
+
+ Platform::CopyString(mRemoteIpAddr, sizeof(mRemoteIpAddr), deviceRemoteIp);
+
+ return DeviceLayer::SystemLayer().ScheduleLambda([nodeId, setupPINCode, deviceRemotePort]() {
+ PairingManager & self = PairingManager::Instance();
+
+ self.InitPairingCommand();
+ self.mSetupPINCode = setupPINCode;
+
+ Inet::IPAddress address;
+ Inet::InterfaceId interfaceId;
+
+ if (!ParseAddressWithInterface(self.mRemoteIpAddr, address, interfaceId))
+ {
+ ChipLogError(NotSpecified, "Invalid IP address: %s", self.mRemoteIpAddr);
+ return;
+ }
+
+ CHIP_ERROR err = self.Pair(nodeId, Transport::PeerAddress::UDP(address, deviceRemotePort, interfaceId));
+ if (err != CHIP_NO_ERROR)
+ {
+ ChipLogError(NotSpecified, "Failed to pair device, error: %s", ErrorStr(err));
+ }
+ });
+}
+
+CHIP_ERROR PairingManager::UnpairDevice(NodeId nodeId)
+{
+ return DeviceLayer::SystemLayer().ScheduleLambda([nodeId]() {
+ PairingManager & self = PairingManager::Instance();
+
+ self.InitPairingCommand();
+
+ self.mCurrentFabricRemover = Platform::MakeUnique<Controller::CurrentFabricRemover>(self.mCommissioner);
+
+ if (!self.mCurrentFabricRemover)
+ {
+ ChipLogError(NotSpecified, "Failed to unpair device, mCurrentFabricRemover is null");
+ return;
+ }
+
+ CHIP_ERROR err = self.mCurrentFabricRemover->RemoveCurrentFabric(nodeId, &self.mCurrentFabricRemoveCallback);
+ if (err != CHIP_NO_ERROR)
+ {
+ ChipLogError(NotSpecified, "Failed to unpair device, error: %s", ErrorStr(err));
+ }
+ });
+}
diff --git a/examples/fabric-sync/admin/PairingManager.h b/examples/fabric-sync/admin/PairingManager.h
new file mode 100644
index 0000000..07cba74
--- /dev/null
+++ b/examples/fabric-sync/admin/PairingManager.h
@@ -0,0 +1,203 @@
+/*
+ *
+ * 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 <controller/CHIPDeviceController.h>
+#include <controller/CommissioningDelegate.h>
+#include <controller/CommissioningWindowOpener.h>
+#include <controller/CurrentFabricRemover.h>
+#include <crypto/CHIPCryptoPAL.h>
+
+// Constants
+constexpr uint16_t kMaxManualCodeLength = 22;
+
+class CommissioningWindowDelegate
+{
+public:
+ virtual void OnCommissioningWindowOpened(chip::NodeId deviceId, CHIP_ERROR err, chip::SetupPayload payload) = 0;
+ virtual ~CommissioningWindowDelegate() = default;
+};
+
+class CommissioningDelegate
+{
+public:
+ virtual void OnCommissioningComplete(chip::NodeId deviceId, CHIP_ERROR err) = 0;
+ virtual ~CommissioningDelegate() = default;
+};
+
+class PairingDelegate
+{
+public:
+ virtual void OnDeviceRemoved(chip::NodeId deviceId, CHIP_ERROR err) = 0;
+ virtual ~PairingDelegate() = default;
+};
+
+/**
+ * The PairingManager class is responsible for managing the commissioning and pairing process
+ * of Matter devices. PairingManager is designed to be used as a singleton, meaning that there
+ * should only be one instance of it running at any given time.
+ *
+ * Usage:
+ *
+ * 1. The class should be initialized when the system starts up, typically by invoking the static
+ * instance method to get the singleton.
+ * 2. To open a commissioning window, the appropriate method should be called on the PairingManager instance.
+ * 3. The PairingManager will handle the lifecycle of the CommissioningWindowOpener and ensure that
+ * resources are cleaned up appropriately when pairing is complete or the process is aborted.
+ *
+ * Example:
+ *
+ * @code
+ * PairingManager& manager = PairingManager::Instance();
+ * manager.OpenCommissioningWindow();
+ * @endcode
+ */
+class PairingManager : public chip::Controller::DevicePairingDelegate,
+ public chip::Controller::DeviceDiscoveryDelegate,
+ public chip::Credentials::DeviceAttestationDelegate
+{
+public:
+ static PairingManager & Instance()
+ {
+ static PairingManager instance;
+ return instance;
+ }
+
+ CHIP_ERROR Init(chip::Controller::DeviceCommissioner * commissioner);
+
+ void SetOpenCommissioningWindowDelegate(CommissioningWindowDelegate * delegate) { mCommissioningWindowDelegate = delegate; }
+ void SetCommissioningDelegate(CommissioningDelegate * delegate) { mCommissioningDelegate = delegate; }
+ void SetPairingDelegate(PairingDelegate * delegate) { mPairingDelegate = delegate; }
+ PairingDelegate * GetPairingDelegate() { return mPairingDelegate; }
+
+ chip::Controller::DeviceCommissioner & CurrentCommissioner() { return *mCommissioner; };
+
+ /**
+ * Opens a commissioning window on the specified node and endpoint.
+ * Only one commissioning window can be active at a time. If a commissioning
+ * window is already open, this function will return an error.
+ *
+ * @param nodeId The target node ID for commissioning.
+ * @param endpointId The target endpoint ID for commissioning.
+ * @param commissioningTimeoutSec Timeout for the commissioning window in seconds.
+ * @param iterations Iterations for PBKDF calculations.
+ * @param discriminator Discriminator for commissioning.
+ * @param salt Optional salt for verifier-based commissioning.
+ * @param verifier Optional verifier for enhanced commissioning security.
+ *
+ * @return CHIP_ERROR_INCORRECT_STATE if a commissioning window is already open.
+ */
+ CHIP_ERROR OpenCommissioningWindow(chip::NodeId nodeId, chip::EndpointId endpointId, uint16_t commissioningTimeoutSec,
+ uint32_t iterations, uint16_t discriminator, const chip::ByteSpan & salt,
+ const chip::ByteSpan & verifier);
+
+ /**
+ * Pairs a device using a setup code payload.
+ *
+ * @param nodeId The target node ID for pairing.
+ * @param payload The setup code payload, which typically contains device-specific pairing information.
+ *
+ * @return CHIP_NO_ERROR on successful initiation of the pairing process, or an appropriate CHIP_ERROR if pairing fails.
+ */
+ CHIP_ERROR PairDeviceWithCode(chip::NodeId nodeId, const char * payload);
+
+ /**
+ * Pairs a device using its setup PIN code and remote IP address.
+ *
+ * @param nodeId The target node ID for pairing.
+ * @param setupPINCode The setup PIN code for the device, used for establishing a secure connection.
+ * @param deviceRemoteIp The IP address of the remote device.
+ * @param deviceRemotePort The port number on which the device is listening for pairing requests.
+ *
+ * @return CHIP_NO_ERROR if the pairing process is initiated successfully, or an appropriate CHIP_ERROR if pairing fails.
+ */
+ CHIP_ERROR PairDevice(chip::NodeId nodeId, uint32_t setupPINCode, const char * deviceRemoteIp, uint16_t deviceRemotePort);
+
+ /**
+ * Unpairs a device with the specified node ID.
+ *
+ * @param nodeId The node ID of the device to be unpaired.
+ *
+ * @return CHIP_NO_ERROR if the device is successfully unpaired, or an appropriate CHIP_ERROR if the process fails.
+ */
+ CHIP_ERROR UnpairDevice(chip::NodeId nodeId);
+
+private:
+ // Constructors
+ PairingManager();
+ PairingManager(const PairingManager &) = delete;
+ PairingManager & operator=(const PairingManager &) = delete;
+
+ // Private member functions (static and non-static)
+ chip::Controller::CommissioningParameters GetCommissioningParameters();
+ void InitPairingCommand();
+ CHIP_ERROR Pair(chip::NodeId remoteId, chip::Transport::PeerAddress address);
+
+ /////////// DevicePairingDelegate Interface /////////
+ void OnStatusUpdate(chip::Controller::DevicePairingDelegate::Status status) override;
+ void OnPairingComplete(CHIP_ERROR error) override;
+ void OnPairingDeleted(CHIP_ERROR error) override;
+ void OnReadCommissioningInfo(const chip::Controller::ReadCommissioningInfo & info) override;
+ void OnCommissioningComplete(chip::NodeId deviceId, CHIP_ERROR error) override;
+
+ /////////// DeviceDiscoveryDelegate Interface /////////
+ void OnDiscoveredDevice(const chip::Dnssd::CommissionNodeData & nodeData) override;
+
+ /////////// DeviceAttestationDelegate Interface /////////
+ chip::Optional<uint16_t> FailSafeExpiryTimeoutSecs() const override;
+ bool ShouldWaitAfterDeviceAttestation() override;
+ void OnDeviceAttestationCompleted(chip::Controller::DeviceCommissioner * deviceCommissioner, chip::DeviceProxy * device,
+ const chip::Credentials::DeviceAttestationVerifier::AttestationDeviceInfo & info,
+ chip::Credentials::AttestationVerificationResult attestationResult) override;
+
+ static void OnOpenCommissioningWindowResponse(void * context, chip::NodeId deviceId, CHIP_ERROR status,
+ chip::SetupPayload payload);
+ static void OnOpenCommissioningWindowVerifierResponse(void * context, chip::NodeId deviceId, CHIP_ERROR status);
+ static void OnCurrentFabricRemove(void * context, chip::NodeId remoteNodeId, CHIP_ERROR status);
+
+ // Private data members
+ chip::Controller::DeviceCommissioner * mCommissioner = nullptr;
+
+ CommissioningWindowDelegate * mCommissioningWindowDelegate = nullptr;
+ CommissioningDelegate * mCommissioningDelegate = nullptr;
+ PairingDelegate * mPairingDelegate = nullptr;
+
+ chip::NodeId mNodeId = chip::kUndefinedNodeId;
+ chip::ByteSpan mVerifier;
+ chip::ByteSpan mSalt;
+ uint16_t mDiscriminator = 0;
+ uint32_t mSetupPINCode = 0;
+ uint8_t mVerifierBuffer[chip::Crypto::kSpake2p_VerifierSerialized_Length];
+ uint8_t mSaltBuffer[chip::Crypto::kSpake2p_Max_PBKDF_Salt_Length];
+ char mRemoteIpAddr[chip::Inet::IPAddress::kMaxStringLength];
+ char mOnboardingPayload[kMaxManualCodeLength + 1];
+
+ /**
+ * Holds the unique_ptr to the current CommissioningWindowOpener.
+ * Only one commissioning window opener can be active at a time.
+ * The pointer is reset when the commissioning window is closed or when an error occurs.
+ */
+ chip::Platform::UniquePtr<chip::Controller::CommissioningWindowOpener> mWindowOpener;
+ chip::Callback::Callback<chip::Controller::OnOpenCommissioningWindow> mOnOpenCommissioningWindowCallback;
+ chip::Callback::Callback<chip::Controller::OnOpenCommissioningWindowWithVerifier> mOnOpenCommissioningWindowVerifierCallback;
+
+ // For Unpair
+ chip::Platform::UniquePtr<chip::Controller::CurrentFabricRemover> mCurrentFabricRemover;
+ chip::Callback::Callback<chip::Controller::OnCurrentFabricRemove> mCurrentFabricRemoveCallback;
+};
diff --git a/examples/fabric-sync/main.cpp b/examples/fabric-sync/main.cpp
index 66541b3..45e0ce3 100644
--- a/examples/fabric-sync/main.cpp
+++ b/examples/fabric-sync/main.cpp
@@ -17,6 +17,11 @@
*/
#include <AppMain.h>
+#include <admin/PairingManager.h>
+
+#if defined(ENABLE_CHIP_SHELL)
+#include "ShellCommands.h"
+#endif
using namespace chip;
@@ -90,6 +95,19 @@
VerifyOrDie(ChipLinuxAppInit(argc, argv) == 0);
+#if defined(ENABLE_CHIP_SHELL)
+ Shell::RegisterCommands();
+#endif
+
+ CHIP_ERROR err = PairingManager::Instance().Init(GetDeviceCommissioner());
+ if (err != CHIP_NO_ERROR)
+ {
+ ChipLogProgress(NotSpecified, "Failed to init PairingManager: %s ", ErrorStr(err));
+
+ // End the program with non zero error code to indicate a error.
+ return 1;
+ }
+
ChipLinuxAppMainLoop();
return 0;
diff --git a/examples/fabric-sync/shell/AddBridgeCommand.cpp b/examples/fabric-sync/shell/AddBridgeCommand.cpp
new file mode 100644
index 0000000..d02ad9f
--- /dev/null
+++ b/examples/fabric-sync/shell/AddBridgeCommand.cpp
@@ -0,0 +1,80 @@
+/*
+ * 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 "AddBridgeCommand.h"
+
+#include <admin/DeviceManager.h>
+#include <lib/shell/streamer.h>
+
+using namespace ::chip;
+
+namespace commands {
+
+AddBridgeCommand::AddBridgeCommand(chip::NodeId nodeId, uint32_t setupPINCode, const char * remoteAddr, uint16_t remotePort) :
+ mBridgeNodeId(nodeId), mSetupPINCode(setupPINCode), mRemoteAddr(remoteAddr), mRemotePort(remotePort)
+{}
+
+void AddBridgeCommand::OnCommissioningComplete(NodeId deviceId, CHIP_ERROR err)
+{
+ if (mBridgeNodeId != deviceId)
+ {
+ if (err != CHIP_NO_ERROR)
+ {
+ ChipLogError(NotSpecified, "Failed to pair non-bridge device (0x:" ChipLogFormatX64 ") with error: %" CHIP_ERROR_FORMAT,
+ ChipLogValueX64(deviceId), err.Format());
+ }
+ else
+ {
+ ChipLogProgress(NotSpecified, "Commissioning complete for non-bridge device: NodeId: " ChipLogFormatX64,
+ ChipLogValueX64(deviceId));
+ }
+ return;
+ }
+
+ if (err == CHIP_NO_ERROR)
+ {
+ DeviceMgr().SetRemoteBridgeNodeId(mBridgeNodeId);
+ ChipLogProgress(NotSpecified, "Successfully paired bridge device: NodeId: " ChipLogFormatX64,
+ ChipLogValueX64(mBridgeNodeId));
+
+ DeviceMgr().UpdateLastUsedNodeId(mBridgeNodeId);
+ }
+ else
+ {
+ ChipLogError(NotSpecified, "Failed to pair bridge device (0x:" ChipLogFormatX64 ") with error: %" CHIP_ERROR_FORMAT,
+ ChipLogValueX64(deviceId), err.Format());
+ }
+
+ CommandRegistry::Instance().ResetActiveCommand();
+}
+
+CHIP_ERROR AddBridgeCommand::RunCommand()
+{
+ if (DeviceMgr().IsFabricSyncReady())
+ {
+ // print to console
+ fprintf(stderr, "Remote Fabric Bridge has already been configured.\n");
+ return CHIP_ERROR_BUSY;
+ }
+
+ PairingManager::Instance().SetCommissioningDelegate(this);
+
+ return DeviceMgr().PairRemoteFabricBridge(mBridgeNodeId, mSetupPINCode, mRemoteAddr, mRemotePort);
+}
+
+} // namespace commands
diff --git a/examples/fabric-sync/shell/AddBridgeCommand.h b/examples/fabric-sync/shell/AddBridgeCommand.h
new file mode 100644
index 0000000..d4b2c16
--- /dev/null
+++ b/examples/fabric-sync/shell/AddBridgeCommand.h
@@ -0,0 +1,40 @@
+/*
+ * 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 <CommandRegistry.h>
+#include <admin/PairingManager.h>
+
+namespace commands {
+
+class AddBridgeCommand : public Command, public CommissioningDelegate
+{
+public:
+ AddBridgeCommand(chip::NodeId nodeId, uint32_t setupPINCode, const char * remoteAddr, uint16_t remotePort);
+ void OnCommissioningComplete(chip::NodeId deviceId, CHIP_ERROR err) override;
+ CHIP_ERROR RunCommand() override;
+
+private:
+ chip::NodeId mBridgeNodeId;
+ uint32_t mSetupPINCode;
+ const char * mRemoteAddr;
+ uint16_t mRemotePort;
+};
+
+} // namespace commands
diff --git a/examples/fabric-sync/shell/BUILD.gn b/examples/fabric-sync/shell/BUILD.gn
new file mode 100644
index 0000000..6f64b91
--- /dev/null
+++ b/examples/fabric-sync/shell/BUILD.gn
@@ -0,0 +1,44 @@
+# 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.
+
+import("//build_overrides/chip.gni")
+import("${chip_root}/src/app/chip_data_model.gni")
+
+config("config") {
+ include_dirs = [
+ ".",
+ "${chip_root}/examples/common",
+ "${chip_root}/examples/fabric-sync",
+ ]
+}
+
+source_set("shell") {
+ public_configs = [ ":config" ]
+
+ sources = [
+ "AddBridgeCommand.cpp",
+ "AddBridgeCommand.h",
+ "CommandRegistry.h",
+ "RemoveBridgeCommand.cpp",
+ "RemoveBridgeCommand.h",
+ "ShellCommands.cpp",
+ "ShellCommands.h",
+ ]
+
+ deps = [
+ "${chip_root}/examples/fabric-sync/admin:fabric-admin-lib",
+ "${chip_root}/examples/fabric-sync/bridge:fabric-bridge-lib",
+ "${chip_root}/src/lib",
+ ]
+}
diff --git a/examples/fabric-sync/shell/CommandRegistry.h b/examples/fabric-sync/shell/CommandRegistry.h
new file mode 100644
index 0000000..ec53e17
--- /dev/null
+++ b/examples/fabric-sync/shell/CommandRegistry.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 <admin/PairingManager.h>
+
+namespace commands {
+
+class Command
+{
+public:
+ virtual ~Command() = default;
+ virtual CHIP_ERROR RunCommand() = 0;
+};
+
+class CommandRegistry
+{
+public:
+ static CommandRegistry & Instance()
+ {
+ static CommandRegistry instance;
+ return instance;
+ }
+
+ void SetActiveCommand(std::unique_ptr<Command> command) { mActiveCommand = std::move(command); }
+
+ Command * GetActiveCommand() { return mActiveCommand.get(); }
+
+ void ResetActiveCommand() { mActiveCommand.reset(); }
+
+ bool IsCommandActive() const { return mActiveCommand != nullptr; }
+
+private:
+ CommandRegistry() = default;
+ std::unique_ptr<Command> mActiveCommand;
+};
+
+} // namespace commands
diff --git a/examples/fabric-sync/shell/RemoveBridgeCommand.cpp b/examples/fabric-sync/shell/RemoveBridgeCommand.cpp
new file mode 100644
index 0000000..9e8f1ed
--- /dev/null
+++ b/examples/fabric-sync/shell/RemoveBridgeCommand.cpp
@@ -0,0 +1,70 @@
+/*
+ * 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 "RemoveBridgeCommand.h"
+
+#include <admin/DeviceManager.h>
+#include <lib/shell/streamer.h>
+
+using namespace ::chip;
+
+namespace commands {
+
+void RemoveBridgeCommand::OnDeviceRemoved(NodeId deviceId, CHIP_ERROR err)
+{
+ if (mBridgeNodeId != deviceId)
+ {
+ ChipLogProgress(NotSpecified, "An non-bridge device: NodeId: " ChipLogFormatX64 " is removed.", ChipLogValueX64(deviceId));
+ return;
+ }
+
+ if (err == CHIP_NO_ERROR)
+ {
+ DeviceMgr().SetRemoteBridgeNodeId(kUndefinedNodeId);
+ ChipLogProgress(NotSpecified, "Successfully removed bridge device: NodeId: " ChipLogFormatX64,
+ ChipLogValueX64(mBridgeNodeId));
+ }
+ else
+ {
+ ChipLogError(NotSpecified, "Failed to remove bridge device (0x:" ChipLogFormatX64 ") with error: %" CHIP_ERROR_FORMAT,
+ ChipLogValueX64(deviceId), err.Format());
+ }
+
+ CommandRegistry::Instance().ResetActiveCommand();
+}
+
+CHIP_ERROR RemoveBridgeCommand::RunCommand()
+{
+ NodeId bridgeNodeId = DeviceMgr().GetRemoteBridgeNodeId();
+
+ if (bridgeNodeId == kUndefinedNodeId)
+ {
+ // print to console
+ fprintf(stderr, "Remote Fabric Bridge is not configured yet, nothing to remove.\n");
+ return CHIP_ERROR_BUSY;
+ }
+
+ mBridgeNodeId = bridgeNodeId;
+
+ PairingManager::Instance().SetPairingDelegate(this);
+ DeviceMgr().UnpairRemoteFabricBridge();
+
+ return CHIP_NO_ERROR;
+}
+
+} // namespace commands
diff --git a/examples/fabric-sync/shell/RemoveBridgeCommand.h b/examples/fabric-sync/shell/RemoveBridgeCommand.h
new file mode 100644
index 0000000..401a539
--- /dev/null
+++ b/examples/fabric-sync/shell/RemoveBridgeCommand.h
@@ -0,0 +1,36 @@
+/*
+ * 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 <CommandRegistry.h>
+#include <admin/PairingManager.h>
+
+namespace commands {
+
+class RemoveBridgeCommand : public Command, public PairingDelegate
+{
+public:
+ void OnDeviceRemoved(chip::NodeId deviceId, CHIP_ERROR err) override;
+ CHIP_ERROR RunCommand() override;
+
+private:
+ chip::NodeId mBridgeNodeId = chip::kUndefinedNodeId;
+};
+
+} // namespace commands
diff --git a/examples/fabric-sync/shell/ShellCommands.cpp b/examples/fabric-sync/shell/ShellCommands.cpp
new file mode 100644
index 0000000..1edbd43
--- /dev/null
+++ b/examples/fabric-sync/shell/ShellCommands.cpp
@@ -0,0 +1,147 @@
+/*
+ *
+ * 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 "ShellCommands.h"
+#include "AddBridgeCommand.h"
+#include "RemoveBridgeCommand.h"
+
+#include <admin/DeviceManager.h>
+#include <inttypes.h>
+#include <lib/core/CHIPCore.h>
+#include <lib/shell/Commands.h>
+#include <lib/shell/Engine.h>
+#include <lib/shell/commands/Help.h>
+#include <lib/support/CHIPArgParser.hpp>
+#include <lib/support/CHIPMem.h>
+#include <lib/support/CodeUtils.h>
+#include <platform/CHIPDeviceLayer.h>
+
+namespace chip {
+namespace Shell {
+
+static CHIP_ERROR PrintAllCommands()
+{
+ streamer_t * sout = streamer_get();
+ streamer_printf(sout, " help Usage: app <subcommand>\r\n");
+ streamer_printf(sout,
+ " add-bridge Pair remote fabric bridge to local fabric. Usage: app add-bridge node-id setup-pin-code "
+ "device-remote-ip device-remote-port\r\n");
+ streamer_printf(sout, " remove-bridge Remove the remote fabric bridge from the local fabric. Usage: app remove-bridge\r\n");
+ streamer_printf(sout, " sync-device Sync a device from other ecosystem. Usage: app sync-device endpointid\r\n");
+ streamer_printf(sout, "\r\n");
+
+ return CHIP_NO_ERROR;
+}
+
+static CHIP_ERROR HandleAddBridgeCommand(int argc, char ** argv)
+{
+ if (argc != 5)
+ {
+ fprintf(stderr,
+ "Invalid arguments. Usage: app add-bridge <node-id> <setup-pin-code> <device-remote-ip> <device-remote-port>\n");
+ return CHIP_ERROR_INVALID_ARGUMENT;
+ }
+
+ // Check if there is already an active command
+ if (commands::CommandRegistry::Instance().IsCommandActive())
+ {
+ fprintf(stderr, "Another command is currently active. Please wait until it completes.\n");
+ return CHIP_ERROR_BUSY;
+ }
+
+ // Parse arguments
+ chip::NodeId nodeId = static_cast<chip::NodeId>(strtoull(argv[1], nullptr, 10));
+ uint32_t setupPINCode = static_cast<uint32_t>(strtoul(argv[2], nullptr, 10));
+ const char * remoteAddr = argv[3];
+ uint16_t remotePort = static_cast<uint16_t>(strtoul(argv[4], nullptr, 10));
+
+ auto command = std::make_unique<commands::AddBridgeCommand>(nodeId, setupPINCode, remoteAddr, remotePort);
+
+ CHIP_ERROR result = command->RunCommand();
+ if (result == CHIP_NO_ERROR)
+ {
+ commands::CommandRegistry::Instance().SetActiveCommand(std::move(command));
+ }
+
+ return result;
+}
+
+static CHIP_ERROR HandleRemoveBridgeCommand(int argc, char ** argv)
+{
+ if (argc != 1)
+ {
+ fprintf(stderr, "Invalid arguments. Usage: app remove-bridge\n");
+ return CHIP_ERROR_INVALID_ARGUMENT;
+ }
+
+ // Check if there is already an active command
+ if (commands::CommandRegistry::Instance().IsCommandActive())
+ {
+ fprintf(stderr, "Another command is currently active. Please wait until it completes.\n");
+ return CHIP_ERROR_BUSY;
+ }
+
+ auto command = std::make_unique<commands::RemoveBridgeCommand>();
+
+ CHIP_ERROR result = command->RunCommand();
+ if (result == CHIP_NO_ERROR)
+ {
+ commands::CommandRegistry::Instance().SetActiveCommand(std::move(command));
+ }
+
+ return result;
+}
+
+static CHIP_ERROR AppPlatformHandler(int argc, char ** argv)
+{
+ CHIP_ERROR error = CHIP_NO_ERROR;
+
+ if (argc == 0 || strcmp(argv[0], "help") == 0)
+ {
+ return PrintAllCommands();
+ }
+ else if (argc == 0 || strcmp(argv[0], "?") == 0)
+ {
+ return PrintAllCommands();
+ }
+ else if (strcmp(argv[0], "add-bridge") == 0)
+ {
+ return HandleAddBridgeCommand(argc, argv);
+ }
+ else if (strcmp(argv[0], "remove-bridge") == 0)
+ {
+ return HandleRemoveBridgeCommand(argc, argv);
+ }
+ else
+ {
+ return CHIP_ERROR_INVALID_ARGUMENT;
+ }
+ return error;
+}
+
+void RegisterCommands()
+{
+
+ static const shell_command_t sDeviceComand = { &AppPlatformHandler, "app", "App commands. Usage: app [command_name]" };
+
+ // Register the root `device` command with the top-level shell.
+ Engine::Root().RegisterCommands(&sDeviceComand, 1);
+ return;
+}
+
+} // namespace Shell
+} // namespace chip
diff --git a/examples/fabric-sync/shell/ShellCommands.h b/examples/fabric-sync/shell/ShellCommands.h
new file mode 100644
index 0000000..ab957bc
--- /dev/null
+++ b/examples/fabric-sync/shell/ShellCommands.h
@@ -0,0 +1,27 @@
+/*
+ *
+ * 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 <platform/CHIPDeviceLayer.h>
+
+namespace chip {
+namespace Shell {
+
+void RegisterCommands();
+
+} // namespace Shell
+} // namespace chip