[Linux] Initial implementation of fabric-admin to facilitate Fabric Synchronization (#33393)
* Initial implementation of fabric-admin app
* Address review comments
diff --git a/examples/fabric-admin/.gn b/examples/fabric-admin/.gn
new file mode 100644
index 0000000..3b11e2b
--- /dev/null
+++ b/examples/fabric-admin/.gn
@@ -0,0 +1,25 @@
+# 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/build.gni")
+
+# The location of the build configuration file.
+buildconfig = "${build_root}/config/BUILDCONFIG.gn"
+
+# CHIP uses angle bracket includes.
+check_system_includes = true
+
+default_args = {
+ import("//args.gni")
+}
diff --git a/examples/fabric-admin/BUILD.gn b/examples/fabric-admin/BUILD.gn
new file mode 100644
index 0000000..79e175f
--- /dev/null
+++ b/examples/fabric-admin/BUILD.gn
@@ -0,0 +1,120 @@
+# 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/build.gni")
+import("//build_overrides/chip.gni")
+
+import("//build_overrides/editline.gni")
+import("${chip_root}/build/chip/tools.gni")
+import("${chip_root}/examples/fabric-admin/fabric-admin.gni")
+import("${chip_root}/src/lib/core/core.gni")
+
+assert(chip_build_tools)
+
+config("config") {
+ include_dirs = [
+ ".",
+ "${chip_root}/examples/common",
+ "${chip_root}/zzz_generated/app-common/app-common",
+ "${chip_root}/zzz_generated/chip-tool",
+ "${chip_root}/src/lib",
+ ]
+
+ defines = [ "CONFIG_USE_SEPARATE_EVENTLOOP=${config_use_separate_eventloop}" ]
+
+ # Note: CONFIG_USE_LOCAL_STORAGE is tested for via #ifdef, not #if.
+ if (config_use_local_storage) {
+ defines += [ "CONFIG_USE_LOCAL_STORAGE" ]
+ }
+
+ cflags = [ "-Wconversion" ]
+}
+
+static_library("fabric-admin-utils") {
+ sources = [
+ "${chip_root}/src/controller/ExamplePersistentStorage.cpp",
+ "${chip_root}/src/controller/ExamplePersistentStorage.h",
+ "${chip_root}/zzz_generated/chip-tool/zap-generated/cluster/ComplexArgumentParser.cpp",
+ "${chip_root}/zzz_generated/chip-tool/zap-generated/cluster/logging/DataModelLogger.cpp",
+ "commands/clusters/ModelCommand.cpp",
+ "commands/clusters/ModelCommand.h",
+ "commands/common/CHIPCommand.cpp",
+ "commands/common/CHIPCommand.h",
+ "commands/common/Command.cpp",
+ "commands/common/Command.h",
+ "commands/common/Commands.cpp",
+ "commands/common/Commands.h",
+ "commands/common/CredentialIssuerCommands.h",
+ "commands/common/HexConversion.h",
+ "commands/common/RemoteDataModelLogger.cpp",
+ "commands/common/RemoteDataModelLogger.h",
+ "commands/pairing/OpenCommissioningWindowCommand.cpp",
+ "commands/pairing/OpenCommissioningWindowCommand.h",
+ "commands/pairing/PairingCommand.cpp",
+ "commands/pairing/ToTLVCert.cpp",
+ ]
+
+ deps = [ "${chip_root}/src/app:events" ]
+
+ sources += [ "commands/interactive/InteractiveCommands.cpp" ]
+ deps += [
+ "${chip_root}/examples/common/websocket-server",
+ "${chip_root}/src/platform/logging:headers",
+ "${editline_root}:editline",
+ ]
+
+ if (chip_device_platform == "darwin") {
+ sources += [ "commands/common/DeviceScanner.cpp" ]
+ }
+
+ public_deps = [
+ "${chip_root}/examples/common/tracing:commandline",
+ "${chip_root}/src/app/icd/client:handler",
+ "${chip_root}/src/app/icd/client:manager",
+ "${chip_root}/src/app/server",
+ "${chip_root}/src/app/tests/suites/commands/interaction_model",
+ "${chip_root}/src/controller/data_model",
+ "${chip_root}/src/credentials:file_attestation_trust_store",
+ "${chip_root}/src/lib",
+ "${chip_root}/src/lib/core:types",
+ "${chip_root}/src/lib/support/jsontlv",
+ "${chip_root}/src/platform",
+ "${chip_root}/third_party/inipp",
+ "${chip_root}/third_party/jsoncpp",
+ ]
+
+ public_configs = [ ":config" ]
+
+ if (chip_enable_transport_trace) {
+ public_deps +=
+ [ "${chip_root}/examples/common/tracing:trace_handlers_decoder" ]
+ }
+
+ output_dir = root_out_dir
+}
+
+executable("fabric-admin") {
+ sources = [ "main.cpp" ]
+
+ deps = [
+ ":fabric-admin-utils",
+ "${chip_root}/src/platform/logging:force_stdio",
+ ]
+
+ output_dir = root_out_dir
+}
+
+group("default") {
+ deps = [ ":fabric-admin" ]
+}
diff --git a/examples/fabric-admin/args.gni b/examples/fabric-admin/args.gni
new file mode 100644
index 0000000..83300d7
--- /dev/null
+++ b/examples/fabric-admin/args.gni
@@ -0,0 +1,34 @@
+# 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}/config/standalone/args.gni")
+
+chip_device_project_config_include = "<CHIPProjectAppConfig.h>"
+chip_project_config_include = "<CHIPProjectAppConfig.h>"
+chip_system_project_config_include = "<SystemProjectConfig.h>"
+
+chip_project_config_include_dirs =
+ [ "${chip_root}/examples/fabric-admin/include" ]
+chip_project_config_include_dirs += [ "${chip_root}/config/standalone" ]
+
+matter_enable_tracing_support = true
+
+matter_log_json_payload_hex = true
+matter_log_json_payload_decode_full = true
+
+# make fabric-admin very strict by default
+chip_tlv_validate_char_string_on_read = true
+chip_tlv_validate_char_string_on_write = true
diff --git a/examples/fabric-admin/build_overrides b/examples/fabric-admin/build_overrides
new file mode 120000
index 0000000..b430cf6
--- /dev/null
+++ b/examples/fabric-admin/build_overrides
@@ -0,0 +1 @@
+../build_overrides
\ No newline at end of file
diff --git a/examples/fabric-admin/commands/clusters/ClusterCommand.h b/examples/fabric-admin/commands/clusters/ClusterCommand.h
new file mode 100644
index 0000000..4865f05
--- /dev/null
+++ b/examples/fabric-admin/commands/clusters/ClusterCommand.h
@@ -0,0 +1,196 @@
+/*
+ * 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/tests/suites/commands/interaction_model/InteractionModel.h>
+
+#include "DataModelLogger.h"
+#include "ModelCommand.h"
+
+class ClusterCommand : public InteractionModelCommands, public ModelCommand, public chip::app::CommandSender::Callback
+{
+public:
+ ClusterCommand(CredentialIssuerCommands * credsIssuerConfig) :
+ InteractionModelCommands(this), ModelCommand("command-by-id", credsIssuerConfig)
+ {
+ AddArgument("cluster-id", 0, UINT32_MAX, &mClusterId);
+ AddByIdArguments();
+ AddArguments();
+ }
+
+ ClusterCommand(chip::ClusterId clusterId, CredentialIssuerCommands * credsIssuerConfig) :
+ InteractionModelCommands(this), ModelCommand("command-by-id", credsIssuerConfig), mClusterId(clusterId)
+ {
+ AddByIdArguments();
+ AddArguments();
+ }
+
+ ~ClusterCommand() {}
+
+ CHIP_ERROR SendCommand(chip::DeviceProxy * device, std::vector<chip::EndpointId> endpointIds) override
+ {
+ return InteractionModelCommands::SendCommand(device, endpointIds.at(0), mClusterId, mCommandId, mPayload);
+ }
+
+ template <class T>
+ CHIP_ERROR SendCommand(chip::DeviceProxy * device, chip::EndpointId endpointId, chip::ClusterId clusterId,
+ chip::CommandId commandId, const T & value)
+ {
+ return InteractionModelCommands::SendCommand(device, endpointId, clusterId, commandId, value);
+ }
+
+ CHIP_ERROR SendGroupCommand(chip::GroupId groupId, chip::FabricIndex fabricIndex) override
+ {
+ return InteractionModelCommands::SendGroupCommand(groupId, fabricIndex, mClusterId, mCommandId, mPayload);
+ }
+
+ template <class T>
+ CHIP_ERROR SendGroupCommand(chip::GroupId groupId, chip::FabricIndex fabricIndex, chip::ClusterId clusterId,
+ chip::CommandId commandId, const T & value)
+ {
+ return InteractionModelCommands::SendGroupCommand(groupId, fabricIndex, clusterId, commandId, value);
+ }
+
+ /////////// CommandSender Callback Interface /////////
+ virtual void OnResponse(chip::app::CommandSender * client, const chip::app::ConcreteCommandPath & path,
+ const chip::app::StatusIB & status, chip::TLV::TLVReader * data) override
+ {
+ CHIP_ERROR error = status.ToChipError();
+ if (CHIP_NO_ERROR != error)
+ {
+ LogErrorOnFailure(RemoteDataModelLogger::LogErrorAsJSON(path, status));
+
+ ChipLogError(NotSpecified, "Response Failure: %s", chip::ErrorStr(error));
+ mError = error;
+ return;
+ }
+
+ if (data != nullptr)
+ {
+ LogErrorOnFailure(RemoteDataModelLogger::LogCommandAsJSON(path, data));
+
+ error = DataModelLogger::LogCommand(path, data);
+ if (CHIP_NO_ERROR != error)
+ {
+ ChipLogError(NotSpecified, "Response Failure: Can not decode Data");
+ mError = error;
+ return;
+ }
+ }
+ }
+
+ virtual void OnError(const chip::app::CommandSender * client, CHIP_ERROR error) override
+ {
+ LogErrorOnFailure(RemoteDataModelLogger::LogErrorAsJSON(error));
+
+ ChipLogProgress(NotSpecified, "Error: %s", chip::ErrorStr(error));
+ mError = error;
+ }
+
+ virtual void OnDone(chip::app::CommandSender * client) override
+ {
+ if (mCommandSender.size())
+ {
+ mCommandSender.front().reset();
+ mCommandSender.erase(mCommandSender.begin());
+ }
+
+ // If the command is repeated N times, wait for all the responses to comes in
+ // before exiting.
+ bool shouldStop = true;
+ if (mRepeatCount.HasValue())
+ {
+ mRepeatCount.SetValue(static_cast<uint16_t>(mRepeatCount.Value() - 1));
+ shouldStop = mRepeatCount.Value() == 0;
+ }
+
+ if (shouldStop)
+ {
+ SetCommandExitStatus(mError);
+ }
+ }
+
+ void Shutdown() override
+ {
+ mError = CHIP_NO_ERROR;
+ ModelCommand::Shutdown();
+ }
+
+protected:
+ ClusterCommand(const char * commandName, CredentialIssuerCommands * credsIssuerConfig) :
+ InteractionModelCommands(this), ModelCommand(commandName, credsIssuerConfig)
+ {
+ // Subclasses are responsible for calling AddArguments.
+ }
+
+ void AddByIdArguments()
+ {
+ AddArgument("command-id", 0, UINT32_MAX, &mCommandId);
+ AddArgument("payload", &mPayload,
+ "The command payload. This should be a JSON-encoded object, with string representations of field ids as keys. "
+ " The values for the keys are represented as follows, depending on the type:\n"
+ " * struct: a JSON-encoded object, with field ids as keys.\n"
+ " * list: a JSON-encoded array of values.\n"
+ " * null: A literal null.\n"
+ " * boolean: A literal true or false.\n"
+ " * unsigned integer: One of:\n"
+ " a) The number directly, as decimal.\n"
+ " b) A string starting with \"u:\" followed by decimal digits\n"
+ " * signed integer: One of:\n"
+ " a) The number directly, if it's negative.\n"
+ " b) A string starting with \"s:\" followed by decimal digits\n"
+ " * single-precision float: A string starting with \"f:\" followed by the number.\n"
+ " * double-precision float: One of:\n"
+ " a) The number directly, if it's not an integer.\n"
+ " b) A string starting with \"d:\" followed by the number.\n"
+ " * octet string: A string starting with \"hex:\" followed by the hex encoding of the bytes.\n"
+ " * string: A string with the characters.\n"
+ "\n"
+ " An example payload may look like this: '{ \"0x0\": { \"0\": null, \"1\": false }, \"1\": [17, \"u:17\"], "
+ "\"0x2\": [ -17, \"s:17\", \"s:-17\" ], \"0x3\": \"f:2\", \"0x4\": [ \"d:3\", 4.5 ], \"0x5\": \"hex:ab12\", "
+ "\"0x6\": \"ab12\" }' and represents:\n"
+ " Field 0: a struct with two fields, one with value null and one with value false.\n"
+ " Field 1: A list of unsigned integers.\n"
+ " Field 2: A list of signed integers.\n"
+ " Field 3: A single-precision float.\n"
+ " Field 4: A list of double-precision floats.\n"
+ " Field 5: A 2-byte octet string.\n"
+ " Field 6: A 4-char character string.");
+ }
+
+ void AddArguments()
+ {
+ AddArgument("timedInteractionTimeoutMs", 0, UINT16_MAX, &mTimedInteractionTimeoutMs,
+ "If provided, do a timed invoke with the given timed interaction timeout. See \"7.6.10. Timed Interaction\" in "
+ "the Matter specification.");
+ AddArgument("busyWaitForMs", 0, UINT16_MAX, &mBusyWaitForMs,
+ "If provided, block the main thread processing for the given time right after sending a command.");
+ AddArgument("suppressResponse", 0, 1, &mSuppressResponse);
+ AddArgument("repeat-count", 1, UINT16_MAX, &mRepeatCount);
+ AddArgument("repeat-delay-ms", 0, UINT16_MAX, &mRepeatDelayInMs);
+ ModelCommand::AddArguments();
+ }
+
+private:
+ chip::ClusterId mClusterId;
+ chip::CommandId mCommandId;
+
+ CHIP_ERROR mError = CHIP_NO_ERROR;
+ CustomArgument mPayload;
+};
diff --git a/examples/fabric-admin/commands/clusters/ComplexArgument.h b/examples/fabric-admin/commands/clusters/ComplexArgument.h
new file mode 100644
index 0000000..954ea1d
--- /dev/null
+++ b/examples/fabric-admin/commands/clusters/ComplexArgument.h
@@ -0,0 +1,422 @@
+/*
+ * 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.
+ *
+ */
+
+/**
+ * This file allocate/free memory using the chip platform abstractions
+ * (Platform::MemoryCalloc and Platform::MemoryFree) for hosting a subset of the
+ * data model internal types until they are consumed by the DataModel::Encode machinery:
+ * - chip::app:DataModel::List<T>
+ * - chip::ByteSpan
+ * - chip::CharSpan
+ *
+ * Memory allocation happens during the 'Setup' phase, while memory deallocation happens
+ * during the 'Finalize' phase.
+ *
+ * The 'Finalize' phase during the destructor phase, and if needed, 'Finalize' will call
+ * the 'Finalize' phase of its descendant.
+ */
+
+#pragma once
+
+#include <app-common/zap-generated/cluster-objects.h>
+#include <app/data-model/List.h>
+#include <app/data-model/Nullable.h>
+#include <commands/common/HexConversion.h>
+#include <json/json.h>
+#include <lib/core/Optional.h>
+#include <lib/support/BytesToHex.h>
+#include <lib/support/CHIPMemString.h>
+#include <lib/support/SafeInt.h>
+
+#include "JsonParser.h"
+
+inline constexpr uint8_t kMaxLabelLength = UINT8_MAX;
+inline constexpr char kNullString[] = "null";
+
+class ComplexArgumentParser
+{
+public:
+ ComplexArgumentParser() {}
+
+ template <typename T,
+ typename std::enable_if_t<std::is_integral<T>::value && !std::is_signed<T>::value &&
+ !std::is_same<std::remove_cv_t<std::remove_reference_t<T>>, bool>::value,
+ int> = 0>
+ static CHIP_ERROR Setup(const char * label, T & request, Json::Value value)
+ {
+ if (value.isNumeric())
+ {
+ if (chip::CanCastTo<T>(value.asLargestUInt()))
+ {
+ request = static_cast<T>(value.asLargestUInt());
+ return CHIP_NO_ERROR;
+ }
+ }
+ else if (value.isString())
+ {
+ // Check for a hex number; JSON does not support those as numbers,
+ // so they have to be done as strings. And we might as well support
+ // string-encoded unsigned numbers in general if we're doing that.
+ bool isHexNotation = strncmp(value.asCString(), "0x", 2) == 0 || strncmp(value.asCString(), "0X", 2) == 0;
+
+ std::stringstream str;
+ isHexNotation ? str << std::hex << value.asCString() : str << value.asCString();
+ uint64_t val;
+ str >> val;
+ if (!str.fail() && str.eof() && chip::CanCastTo<T>(val))
+ {
+ request = static_cast<T>(val);
+ return CHIP_NO_ERROR;
+ }
+ }
+
+ ChipLogError(NotSpecified, "Error while encoding %s as an unsigned integer.", label);
+ return CHIP_ERROR_INVALID_ARGUMENT;
+ }
+
+ template <typename T, std::enable_if_t<std::is_signed<T>::value, bool> = true>
+ static CHIP_ERROR Setup(const char * label, T & request, Json::Value value)
+ {
+ if (!value.isNumeric() || !chip::CanCastTo<T>(value.asLargestInt()))
+ {
+ ChipLogError(NotSpecified, "Error while encoding %s as an unsigned integer.", label);
+ return CHIP_ERROR_INVALID_ARGUMENT;
+ }
+
+ request = static_cast<T>(value.asLargestInt());
+ return CHIP_NO_ERROR;
+ }
+
+ template <typename T, typename std::enable_if_t<std::is_enum<T>::value, int> = 0>
+ static CHIP_ERROR Setup(const char * label, T & request, Json::Value value)
+ {
+ std::underlying_type_t<T> requestValue;
+ ReturnErrorOnFailure(ComplexArgumentParser::Setup(label, requestValue, value));
+
+ request = static_cast<T>(requestValue);
+ return CHIP_NO_ERROR;
+ }
+
+ template <typename T>
+ static CHIP_ERROR Setup(const char * label, chip::BitFlags<T> & request, Json::Value & value)
+ {
+ T requestValue;
+ ReturnErrorOnFailure(ComplexArgumentParser::Setup(label, requestValue, value));
+
+ request = chip::BitFlags<T>(requestValue);
+ return CHIP_NO_ERROR;
+ }
+
+ template <typename T>
+ static CHIP_ERROR Setup(const char * label, chip::BitMask<T> & request, Json::Value & value)
+ {
+ T requestValue;
+ ReturnErrorOnFailure(ComplexArgumentParser::Setup(label, requestValue, value));
+
+ request = chip::BitMask<T>(requestValue);
+ return CHIP_NO_ERROR;
+ }
+
+ template <typename T>
+ static CHIP_ERROR Setup(const char * label, chip::Optional<T> & request, Json::Value & value)
+ {
+ T requestValue;
+ ReturnErrorOnFailure(ComplexArgumentParser::Setup(label, requestValue, value));
+
+ request = chip::Optional<T>(requestValue);
+ return CHIP_NO_ERROR;
+ }
+
+ template <typename T>
+ static CHIP_ERROR Setup(const char * label, chip::app::DataModel::Nullable<T> & request, Json::Value & value)
+ {
+ if (value.isNull())
+ {
+ request.SetNull();
+ return CHIP_NO_ERROR;
+ }
+
+ T requestValue;
+ ReturnErrorOnFailure(ComplexArgumentParser::Setup(label, requestValue, value));
+
+ request = chip::app::DataModel::Nullable<T>(requestValue);
+ return CHIP_NO_ERROR;
+ }
+
+ template <typename T>
+ static CHIP_ERROR Setup(const char * label, chip::app::DataModel::List<T> & request, Json::Value & value)
+ {
+ if (!value.isArray())
+ {
+ ChipLogError(NotSpecified, "Error while encoding %s as an array.", label);
+ return CHIP_ERROR_INVALID_ARGUMENT;
+ }
+
+ auto content = static_cast<typename std::remove_const<T>::type *>(chip::Platform::MemoryCalloc(value.size(), sizeof(T)));
+ VerifyOrReturnError(content != nullptr, CHIP_ERROR_NO_MEMORY);
+
+ Json::ArrayIndex size = value.size();
+ for (Json::ArrayIndex i = 0; i < size; i++)
+ {
+ char labelWithIndex[kMaxLabelLength];
+ // GCC 7.0.1 has introduced some new warnings for snprintf (-Werror=format-truncation) by default.
+ // This is not particularly useful when using snprintf and especially in this context, so in order
+ // to disable the warning the %s is constrained to be of max length: (254 - 11 - 2) where:
+ // - 254 is kMaxLabelLength - 1 (for null)
+ // - 11 is the maximum length of a %d (-2147483648, 2147483647)
+ // - 2 is the length for the "[" and "]" characters.
+ snprintf(labelWithIndex, sizeof(labelWithIndex), "%.241s[%d]", label, i);
+ ReturnErrorOnFailure(ComplexArgumentParser::Setup(labelWithIndex, content[i], value[i]));
+ }
+
+ request = chip::app::DataModel::List<T>(content, value.size());
+ return CHIP_NO_ERROR;
+ }
+
+ static CHIP_ERROR Setup(const char * label, chip::ByteSpan & request, Json::Value & value)
+ {
+ if (!value.isString())
+ {
+ ChipLogError(NotSpecified, "Error while encoding %s as an octet string: Not a string.", label);
+ return CHIP_ERROR_INVALID_ARGUMENT;
+ }
+
+ auto str = value.asString();
+ auto size = str.size();
+ uint8_t * buffer = nullptr;
+
+ if (IsStrString(str.c_str()))
+ {
+ // Skip the prefix
+ str.erase(0, kStrStringPrefixLen);
+ size = str.size();
+
+ buffer = static_cast<uint8_t *>(chip::Platform::MemoryCalloc(size, sizeof(uint8_t)));
+ VerifyOrReturnError(buffer != nullptr, CHIP_ERROR_NO_MEMORY);
+
+ memcpy(buffer, str.c_str(), size);
+ }
+ else
+ {
+ if (IsHexString(str.c_str()))
+ {
+ // Skip the prefix
+ str.erase(0, kHexStringPrefixLen);
+ size = str.size();
+ }
+
+ CHIP_ERROR err = HexToBytes(
+ chip::CharSpan(str.c_str(), size),
+ [&buffer](size_t allocSize) {
+ buffer = static_cast<uint8_t *>(chip::Platform::MemoryCalloc(allocSize, sizeof(uint8_t)));
+ return buffer;
+ },
+ &size);
+
+ if (err != CHIP_NO_ERROR)
+ {
+ if (buffer != nullptr)
+ {
+ chip::Platform::MemoryFree(buffer);
+ }
+
+ return err;
+ }
+ }
+
+ request = chip::ByteSpan(buffer, size);
+ return CHIP_NO_ERROR;
+ }
+
+ static CHIP_ERROR Setup(const char * label, chip::CharSpan & request, Json::Value & value)
+ {
+ if (!value.isString())
+ {
+ ChipLogError(NotSpecified, "Error while encoding %s as a string: Not a string.", label);
+ return CHIP_ERROR_INVALID_ARGUMENT;
+ }
+
+ size_t size = strlen(value.asCString());
+ auto buffer = static_cast<char *>(chip::Platform::MemoryCalloc(size, sizeof(char)));
+ VerifyOrReturnError(buffer != nullptr, CHIP_ERROR_NO_MEMORY);
+
+ memcpy(buffer, value.asCString(), size);
+
+ request = chip::CharSpan(buffer, size);
+ return CHIP_NO_ERROR;
+ }
+
+ static CHIP_ERROR Setup(const char * label, float & request, Json::Value & value)
+ {
+ if (!value.isNumeric())
+ {
+ ChipLogError(NotSpecified, "Error while encoding %s as a float: Not a number.", label);
+ return CHIP_ERROR_INVALID_ARGUMENT;
+ }
+
+ request = static_cast<float>(value.asFloat());
+ return CHIP_NO_ERROR;
+ }
+
+ static CHIP_ERROR Setup(const char * label, double & request, Json::Value & value)
+ {
+ if (!value.isNumeric())
+ {
+ ChipLogError(NotSpecified, "Error while encoding %s as a double: Not a number.", label);
+ return CHIP_ERROR_INVALID_ARGUMENT;
+ }
+
+ request = static_cast<double>(value.asDouble());
+ return CHIP_NO_ERROR;
+ }
+
+ static CHIP_ERROR Setup(const char * label, bool & request, Json::Value & value)
+ {
+ if (!value.isBool())
+ {
+ ChipLogError(NotSpecified, "Error while encoding %s as a boolean: Not a boolean.", label);
+ return CHIP_ERROR_INVALID_ARGUMENT;
+ }
+
+ request = value.asBool();
+ return CHIP_NO_ERROR;
+ }
+
+ static CHIP_ERROR EnsureMemberExist(const char * label, const char * memberName, bool hasMember)
+ {
+ if (hasMember)
+ {
+ return CHIP_NO_ERROR;
+ }
+
+ ChipLogError(NotSpecified, "%s is required. Should be provided as {\"%s\": value}", label, memberName);
+ return CHIP_ERROR_INVALID_ARGUMENT;
+ }
+
+ static CHIP_ERROR EnsureNoMembersRemaining(const char * label, const Json::Value & value)
+ {
+ auto remainingFields = value.getMemberNames();
+ if (remainingFields.size() == 0)
+ {
+ return CHIP_NO_ERROR;
+ }
+#if CHIP_ERROR_LOGGING
+ for (auto & field : remainingFields)
+ {
+ ChipLogError(NotSpecified, "Unexpected field name: '%s.%s'", label, field.c_str());
+ }
+#endif // CHIP_ERROR_LOGGING
+ return CHIP_ERROR_INVALID_ARGUMENT;
+ }
+
+ template <typename T>
+ static void Finalize(T & request)
+ {
+ // Nothing to do
+ }
+
+ template <typename T>
+ static void Finalize(chip::Optional<T> & request)
+ {
+ VerifyOrReturn(request.HasValue());
+ ComplexArgumentParser::Finalize(request.Value());
+ }
+
+ template <typename T>
+ static void Finalize(chip::app::DataModel::Nullable<T> & request)
+ {
+ VerifyOrReturn(!request.IsNull());
+ ComplexArgumentParser::Finalize(request.Value());
+ }
+
+ static void Finalize(chip::ByteSpan & request)
+ {
+ VerifyOrReturn(request.data() != nullptr);
+ chip::Platform::MemoryFree(reinterpret_cast<void *>(const_cast<uint8_t *>(request.data())));
+ }
+
+ static void Finalize(chip::CharSpan & request)
+ {
+ VerifyOrReturn(request.data() != nullptr);
+ chip::Platform::MemoryFree(reinterpret_cast<void *>(const_cast<char *>(request.data())));
+ }
+
+ template <typename T>
+ static void Finalize(chip::app::DataModel::List<T> & request)
+ {
+ VerifyOrReturn(request.data() != nullptr);
+
+ size_t size = request.size();
+ auto data = const_cast<typename std::remove_const<T>::type *>(request.data());
+ for (size_t i = 0; i < size; i++)
+ {
+ Finalize(data[i]);
+ }
+
+ chip::Platform::MemoryFree(reinterpret_cast<void *>(data));
+ }
+
+#include <zap-generated/cluster/ComplexArgumentParser.h>
+};
+
+class ComplexArgument
+{
+public:
+ virtual ~ComplexArgument() {}
+
+ virtual CHIP_ERROR Parse(const char * label, const char * json) = 0;
+
+ virtual void Reset() = 0;
+};
+
+template <typename T>
+class TypedComplexArgument : public ComplexArgument
+{
+public:
+ TypedComplexArgument() {}
+ TypedComplexArgument(T * request) : mRequest(request) {}
+ ~TypedComplexArgument()
+ {
+ if (mRequest != nullptr)
+ {
+ ComplexArgumentParser::Finalize(*mRequest);
+ }
+ }
+
+ void SetArgument(T * request) { mRequest = request; };
+
+ CHIP_ERROR Parse(const char * label, const char * json)
+ {
+ Json::Value value;
+ if (strcmp(kNullString, json) == 0)
+ {
+ value = Json::nullValue;
+ }
+ else if (!JsonParser::ParseComplexArgument(label, json, value))
+ {
+ return CHIP_ERROR_INVALID_ARGUMENT;
+ }
+
+ return ComplexArgumentParser::Setup(label, *mRequest, value);
+ }
+
+ void Reset() { *mRequest = T(); }
+
+private:
+ T * mRequest;
+};
diff --git a/examples/fabric-admin/commands/clusters/CustomArgument.h b/examples/fabric-admin/commands/clusters/CustomArgument.h
new file mode 100644
index 0000000..3769c00
--- /dev/null
+++ b/examples/fabric-admin/commands/clusters/CustomArgument.h
@@ -0,0 +1,297 @@
+/*
+ * 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 <commands/common/HexConversion.h>
+#include <lib/support/BytesToHex.h>
+#include <lib/support/CHIPMemString.h>
+#include <lib/support/SafeInt.h>
+
+#include <string>
+
+#include "JsonParser.h"
+
+namespace {
+static constexpr char kPayloadHexPrefix[] = "hex:";
+static constexpr char kPayloadSignedPrefix[] = "s:";
+static constexpr char kPayloadUnsignedPrefix[] = "u:";
+static constexpr char kPayloadFloatPrefix[] = "f:";
+static constexpr char kPayloadDoublePrefix[] = "d:";
+static constexpr size_t kPayloadHexPrefixLen = ArraySize(kPayloadHexPrefix) - 1; // ignore null character
+static constexpr size_t kPayloadSignedPrefixLen = ArraySize(kPayloadSignedPrefix) - 1; // ignore null character
+static constexpr size_t kPayloadUnsignedPrefixLen = ArraySize(kPayloadUnsignedPrefix) - 1; // ignore null character
+static constexpr size_t kPayloadFloatPrefixLen = ArraySize(kPayloadFloatPrefix) - 1; // ignore null character
+static constexpr size_t kPayloadDoublePrefixLen = ArraySize(kPayloadDoublePrefix) - 1; // ignore null character
+} // namespace
+
+class CustomArgumentParser
+{
+public:
+ static CHIP_ERROR Put(chip::TLV::TLVWriter * writer, chip::TLV::Tag tag, Json::Value & value)
+ {
+ if (value.isObject())
+ {
+ return CustomArgumentParser::PutObject(writer, tag, value);
+ }
+
+ if (value.isArray())
+ {
+ return CustomArgumentParser::PutArray(writer, tag, value);
+ }
+
+ if (value.isString())
+ {
+ if (IsOctetString(value))
+ {
+ return CustomArgumentParser::PutOctetString(writer, tag, value);
+ }
+ if (IsUnsignedNumberPrefix(value))
+ {
+ return CustomArgumentParser::PutUnsignedFromString(writer, tag, value);
+ }
+ if (IsSignedNumberPrefix(value))
+ {
+ return CustomArgumentParser::PutSignedFromString(writer, tag, value);
+ }
+ if (IsFloatNumberPrefix(value))
+ {
+ return CustomArgumentParser::PutFloatFromString(writer, tag, value);
+ }
+ if (IsDoubleNumberPrefix(value))
+ {
+ return CustomArgumentParser::PutDoubleFromString(writer, tag, value);
+ }
+
+ return CustomArgumentParser::PutCharString(writer, tag, value);
+ }
+
+ if (value.isNull())
+ {
+ return chip::app::DataModel::Encode(*writer, tag, chip::app::DataModel::Nullable<uint8_t>());
+ }
+
+ if (value.isBool())
+ {
+ return chip::app::DataModel::Encode(*writer, tag, value.asBool());
+ }
+
+ if (value.isUInt())
+ {
+ return chip::app::DataModel::Encode(*writer, tag, value.asLargestUInt());
+ }
+
+ if (value.isInt())
+ {
+ return chip::app::DataModel::Encode(*writer, tag, value.asLargestInt());
+ }
+
+ if (value.isNumeric())
+ {
+ return chip::app::DataModel::Encode(*writer, tag, value.asDouble());
+ }
+
+ return CHIP_ERROR_NOT_IMPLEMENTED;
+ }
+
+private:
+ static CHIP_ERROR PutArray(chip::TLV::TLVWriter * writer, chip::TLV::Tag tag, Json::Value & value)
+ {
+ chip::TLV::TLVType outer;
+ ReturnErrorOnFailure(writer->StartContainer(tag, chip::TLV::kTLVType_Array, outer));
+
+ Json::ArrayIndex size = value.size();
+
+ for (Json::ArrayIndex i = 0; i < size; i++)
+ {
+ ReturnErrorOnFailure(CustomArgumentParser::Put(writer, chip::TLV::AnonymousTag(), value[i]));
+ }
+
+ return writer->EndContainer(outer);
+ }
+
+ static CHIP_ERROR PutObject(chip::TLV::TLVWriter * writer, chip::TLV::Tag tag, Json::Value & value)
+ {
+ chip::TLV::TLVType outer;
+ ReturnErrorOnFailure(writer->StartContainer(tag, chip::TLV::kTLVType_Structure, outer));
+
+ for (auto const & id : value.getMemberNames())
+ {
+ auto index = std::stoul(id, nullptr, 0);
+ VerifyOrReturnError(chip::CanCastTo<uint8_t>(index), CHIP_ERROR_INVALID_ARGUMENT);
+ ReturnErrorOnFailure(CustomArgumentParser::Put(writer, chip::TLV::ContextTag(static_cast<uint8_t>(index)), value[id]));
+ }
+
+ return writer->EndContainer(outer);
+ }
+
+ static CHIP_ERROR PutOctetString(chip::TLV::TLVWriter * writer, chip::TLV::Tag tag, Json::Value & value)
+ {
+ const char * hexData = value.asCString() + kPayloadHexPrefixLen;
+ size_t hexDataLen = strlen(hexData);
+ chip::Platform::ScopedMemoryBuffer<uint8_t> buffer;
+
+ size_t octetCount;
+ ReturnErrorOnFailure(HexToBytes(
+ chip::CharSpan(hexData, hexDataLen),
+ [&buffer](size_t allocSize) {
+ buffer.Calloc(allocSize);
+ return buffer.Get();
+ },
+ &octetCount));
+
+ return chip::app::DataModel::Encode(*writer, tag, chip::ByteSpan(buffer.Get(), octetCount));
+ }
+
+ static CHIP_ERROR PutCharString(chip::TLV::TLVWriter * writer, chip::TLV::Tag tag, Json::Value & value)
+ {
+ size_t size = strlen(value.asCString());
+ return chip::app::DataModel::Encode(*writer, tag, chip::CharSpan(value.asCString(), size));
+ }
+
+ static CHIP_ERROR PutUnsignedFromString(chip::TLV::TLVWriter * writer, chip::TLV::Tag tag, Json::Value & value)
+ {
+ char numberAsString[21];
+ chip::Platform::CopyString(numberAsString, value.asCString() + kPayloadUnsignedPrefixLen);
+
+ auto number = std::stoull(numberAsString, nullptr, 0);
+ return chip::app::DataModel::Encode(*writer, tag, static_cast<uint64_t>(number));
+ }
+
+ static CHIP_ERROR PutSignedFromString(chip::TLV::TLVWriter * writer, chip::TLV::Tag tag, Json::Value & value)
+ {
+ char numberAsString[21];
+ chip::Platform::CopyString(numberAsString, value.asCString() + kPayloadSignedPrefixLen);
+
+ auto number = std::stoll(numberAsString, nullptr, 0);
+ return chip::app::DataModel::Encode(*writer, tag, static_cast<int64_t>(number));
+ }
+
+ static CHIP_ERROR PutFloatFromString(chip::TLV::TLVWriter * writer, chip::TLV::Tag tag, Json::Value & value)
+ {
+ char numberAsString[21];
+ chip::Platform::CopyString(numberAsString, value.asCString() + kPayloadFloatPrefixLen);
+
+ auto number = std::stof(numberAsString);
+ return chip::app::DataModel::Encode(*writer, tag, number);
+ }
+
+ static CHIP_ERROR PutDoubleFromString(chip::TLV::TLVWriter * writer, chip::TLV::Tag tag, Json::Value & value)
+ {
+ char numberAsString[21];
+ chip::Platform::CopyString(numberAsString, value.asCString() + kPayloadDoublePrefixLen);
+
+ auto number = std::stod(numberAsString);
+ return chip::app::DataModel::Encode(*writer, tag, number);
+ }
+
+ static bool IsOctetString(Json::Value & value)
+ {
+ return (strncmp(value.asCString(), kPayloadHexPrefix, kPayloadHexPrefixLen) == 0);
+ }
+
+ static bool IsUnsignedNumberPrefix(Json::Value & value)
+ {
+ return (strncmp(value.asCString(), kPayloadUnsignedPrefix, kPayloadUnsignedPrefixLen) == 0);
+ }
+
+ static bool IsSignedNumberPrefix(Json::Value & value)
+ {
+ return (strncmp(value.asCString(), kPayloadSignedPrefix, kPayloadSignedPrefixLen) == 0);
+ }
+
+ static bool IsFloatNumberPrefix(Json::Value & value)
+ {
+ return (strncmp(value.asCString(), kPayloadFloatPrefix, kPayloadFloatPrefixLen) == 0);
+ }
+
+ static bool IsDoubleNumberPrefix(Json::Value & value)
+ {
+ return (strncmp(value.asCString(), kPayloadDoublePrefix, kPayloadDoublePrefixLen) == 0);
+ }
+};
+
+class CustomArgument
+{
+public:
+ ~CustomArgument()
+ {
+ if (mData != nullptr)
+ {
+ chip::Platform::MemoryFree(mData);
+ }
+ }
+
+ CHIP_ERROR Parse(const char * label, const char * json)
+ {
+ Json::Value value;
+ static constexpr char kHexNumPrefix[] = "0x";
+ constexpr size_t kHexNumPrefixLen = ArraySize(kHexNumPrefix) - 1;
+ if (strncmp(json, kPayloadHexPrefix, kPayloadHexPrefixLen) == 0 ||
+ strncmp(json, kPayloadSignedPrefix, kPayloadSignedPrefixLen) == 0 ||
+ strncmp(json, kPayloadUnsignedPrefix, kPayloadUnsignedPrefixLen) == 0 ||
+ strncmp(json, kPayloadFloatPrefix, kPayloadFloatPrefixLen) == 0 ||
+ strncmp(json, kPayloadDoublePrefix, kPayloadDoublePrefixLen) == 0)
+ {
+ value = Json::Value(json);
+ }
+ else if (strncmp(json, kHexNumPrefix, kHexNumPrefixLen) == 0)
+ {
+ // Assume that hex numbers are unsigned. Prepend
+ // kPayloadUnsignedPrefix and then let the rest of the logic handle
+ // things.
+ std::string str(kPayloadUnsignedPrefix);
+ str += json;
+ value = Json::Value(str);
+ }
+ else if (!JsonParser::ParseCustomArgument(label, json, value))
+ {
+ return CHIP_ERROR_INVALID_ARGUMENT;
+ }
+
+ mData = static_cast<uint8_t *>(chip::Platform::MemoryCalloc(sizeof(uint8_t), mDataMaxLen));
+ VerifyOrReturnError(mData != nullptr, CHIP_ERROR_NO_MEMORY);
+
+ chip::TLV::TLVWriter writer;
+ writer.Init(mData, mDataMaxLen);
+
+ ReturnErrorOnFailure(CustomArgumentParser::Put(&writer, chip::TLV::AnonymousTag(), value));
+
+ mDataLen = writer.GetLengthWritten();
+ return writer.Finalize();
+ }
+
+ CHIP_ERROR Encode(chip::TLV::TLVWriter & writer, chip::TLV::Tag tag) const
+ {
+ chip::TLV::TLVReader reader;
+ reader.Init(mData, mDataLen);
+ ReturnErrorOnFailure(reader.Next());
+
+ return writer.CopyElement(tag, reader);
+ }
+
+ // We trust our consumers to do the encoding of our data correctly, so don't
+ // need to know whether we are being encoded for a write.
+ static constexpr bool kIsFabricScoped = false;
+
+private:
+ uint8_t * mData = nullptr;
+ uint32_t mDataLen = 0;
+ static constexpr uint32_t mDataMaxLen = 4096;
+};
diff --git a/examples/fabric-admin/commands/clusters/DataModelLogger.h b/examples/fabric-admin/commands/clusters/DataModelLogger.h
new file mode 100644
index 0000000..ee64975
--- /dev/null
+++ b/examples/fabric-admin/commands/clusters/DataModelLogger.h
@@ -0,0 +1,190 @@
+/*
+ * 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 <string>
+
+#include <app-common/zap-generated/cluster-objects.h>
+#include <app/ConcreteAttributePath.h>
+#include <app/ConcreteCommandPath.h>
+#include <app/EventHeader.h>
+#include <app/MessageDef/StatusIB.h>
+#include <app/data-model/DecodableList.h>
+#include <commands/common/RemoteDataModelLogger.h>
+#include <lib/support/BytesToHex.h>
+
+class DataModelLogger
+{
+public:
+ static CHIP_ERROR LogAttribute(const chip::app::ConcreteDataAttributePath & path, chip::TLV::TLVReader * data);
+ static CHIP_ERROR LogCommand(const chip::app::ConcreteCommandPath & path, chip::TLV::TLVReader * data);
+ static CHIP_ERROR LogEvent(const chip::app::EventHeader & header, chip::TLV::TLVReader * data);
+
+private:
+ static CHIP_ERROR LogValue(const char * label, size_t indent, bool value)
+ {
+ DataModelLogger::LogString(label, indent, value ? "TRUE" : "FALSE");
+ return CHIP_NO_ERROR;
+ }
+
+ static CHIP_ERROR LogValue(const char * label, size_t indent, chip::CharSpan value)
+ {
+ DataModelLogger::LogString(label, indent, std::string(value.data(), value.size()));
+ return CHIP_NO_ERROR;
+ }
+
+ static CHIP_ERROR LogValue(const char * label, size_t indent, chip::ByteSpan value)
+ {
+ // CHIP_CONFIG_LOG_MESSAGE_MAX_SIZE includes various prefixes we don't
+ // control (timestamps, process ids, etc). Let's assume (hope?) that
+ // those prefixes use up no more than half the total available space.
+ // Right now it looks like the prefixes are 45 chars out of a 255 char
+ // buffer.
+ char buffer[CHIP_CONFIG_LOG_MESSAGE_MAX_SIZE / 2];
+ size_t prefixSize = ComputePrefixSize(label, indent);
+ if (prefixSize > ArraySize(buffer))
+ {
+ DataModelLogger::LogString("", 0, "Prefix is too long to fit in buffer");
+ return CHIP_ERROR_INTERNAL;
+ }
+
+ const size_t availableSize = ArraySize(buffer) - prefixSize;
+ // Each byte ends up as two hex characters.
+ const size_t bytesPerLogCall = availableSize / 2;
+ std::string labelStr(label);
+ while (value.size() > bytesPerLogCall)
+ {
+ ReturnErrorOnFailure(
+ chip::Encoding::BytesToUppercaseHexString(value.data(), bytesPerLogCall, &buffer[0], ArraySize(buffer)));
+ LogString(labelStr, indent, buffer);
+ value = value.SubSpan(bytesPerLogCall);
+ // For the second and following lines, make it clear that they are
+ // continuation lines by replacing the label with "....".
+ labelStr.replace(labelStr.begin(), labelStr.end(), labelStr.size(), '.');
+ }
+ ReturnErrorOnFailure(chip::Encoding::BytesToUppercaseHexString(value.data(), value.size(), &buffer[0], ArraySize(buffer)));
+ LogString(labelStr, indent, buffer);
+
+ return CHIP_NO_ERROR;
+ }
+
+ template <typename X,
+ typename std::enable_if_t<
+ std::is_integral<X>::value && !std::is_same<std::remove_cv_t<std::remove_reference_t<X>>, bool>::value, int> = 0>
+ static CHIP_ERROR LogValue(const char * label, size_t indent, X value)
+ {
+ DataModelLogger::LogString(label, indent, std::to_string(value));
+ return CHIP_NO_ERROR;
+ }
+
+ template <typename X, typename std::enable_if_t<std::is_floating_point<X>::value, int> = 0>
+ static CHIP_ERROR LogValue(const char * label, size_t indent, X value)
+ {
+ DataModelLogger::LogString(label, indent, std::to_string(value));
+ return CHIP_NO_ERROR;
+ }
+
+ template <typename X, typename std::enable_if_t<std::is_enum<X>::value, int> = 0>
+ static CHIP_ERROR LogValue(const char * label, size_t indent, X value)
+ {
+ return DataModelLogger::LogValue(label, indent, chip::to_underlying(value));
+ }
+
+ template <typename X>
+ static CHIP_ERROR LogValue(const char * label, size_t indent, chip::BitFlags<X> value)
+ {
+ return DataModelLogger::LogValue(label, indent, value.Raw());
+ }
+
+ template <typename T>
+ static CHIP_ERROR LogValue(const char * label, size_t indent, const chip::app::DataModel::DecodableList<T> & value)
+ {
+ size_t count = 0;
+ ReturnErrorOnFailure(value.ComputeSize(&count));
+ DataModelLogger::LogString(label, indent, std::to_string(count) + " entries");
+
+ auto iter = value.begin();
+ size_t i = 0;
+ while (iter.Next())
+ {
+ ++i;
+ std::string itemLabel = std::string("[") + std::to_string(i) + "]";
+ ReturnErrorOnFailure(DataModelLogger::LogValue(itemLabel.c_str(), indent + 1, iter.GetValue()));
+ }
+ if (iter.GetStatus() != CHIP_NO_ERROR)
+ {
+ DataModelLogger::LogString(indent + 1, "List truncated due to invalid value");
+ }
+ return iter.GetStatus();
+ }
+
+ template <typename T>
+ static CHIP_ERROR LogValue(const char * label, size_t indent, const chip::app::DataModel::Nullable<T> & value)
+ {
+ if (value.IsNull())
+ {
+ DataModelLogger::LogString(label, indent, "null");
+ return CHIP_NO_ERROR;
+ }
+
+ return DataModelLogger::LogValue(label, indent, value.Value());
+ }
+
+ template <typename T>
+ static CHIP_ERROR LogValue(const char * label, size_t indent, const chip::Optional<T> & value)
+ {
+ if (value.HasValue())
+ {
+ return DataModelLogger::LogValue(label, indent, value.Value());
+ }
+
+ return CHIP_NO_ERROR;
+ }
+
+#include <zap-generated/cluster/logging/DataModelLogger.h>
+
+ static void LogString(size_t indent, const std::string string) { LogString("", indent, string); }
+
+ static void LogString(const std::string label, size_t indent, const std::string string)
+ {
+ std::string prefix = ComputePrefix(label, indent);
+
+ ChipLogProgress(NotSpecified, "%s%s", prefix.c_str(), string.c_str());
+ }
+
+private:
+ static std::string ComputePrefix(const std::string label, size_t indent)
+ {
+ std::string prefix;
+ for (size_t i = 0; i < indent; ++i)
+ {
+ prefix.append(" ");
+ }
+ if (label.size() > 0)
+ {
+ prefix.append(label);
+ prefix.append(":");
+ }
+ prefix.append(" ");
+
+ return prefix;
+ }
+
+ static size_t ComputePrefixSize(const std::string label, size_t indent) { return ComputePrefix(label, indent).size(); }
+};
diff --git a/examples/fabric-admin/commands/clusters/JsonParser.h b/examples/fabric-admin/commands/clusters/JsonParser.h
new file mode 100644
index 0000000..0871e76
--- /dev/null
+++ b/examples/fabric-admin/commands/clusters/JsonParser.h
@@ -0,0 +1,166 @@
+/*
+ * Copyright (c) 2024 Project CHIP Authors
+ * All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+#pragma once
+
+#include "../common/CustomStringPrefix.h"
+
+#include <json/json.h>
+#include <lib/core/Optional.h>
+
+#include <memory>
+#include <sstream>
+#include <string>
+#include <vector>
+
+class JsonParser
+{
+public:
+ // Returns whether the parse succeeded.
+ static bool ParseComplexArgument(const char * label, const char * json, Json::Value & value)
+ {
+ return Parse(label, json, /* strictRoot = */ true, value);
+ }
+
+ // Returns whether the parse succeeded.
+ static bool ParseCustomArgument(const char * label, const char * json, Json::Value & value)
+ {
+ return Parse(label, json, /* strictRoot = */ false, value);
+ }
+
+private:
+ static bool Parse(const char * label, const char * json, bool strictRoot, Json::Value & value)
+ {
+ Json::CharReaderBuilder readerBuilder;
+ readerBuilder.settings_["strictRoot"] = strictRoot;
+ readerBuilder.settings_["allowSingleQuotes"] = true;
+ readerBuilder.settings_["failIfExtra"] = true;
+ readerBuilder.settings_["rejectDupKeys"] = true;
+
+ auto reader = std::unique_ptr<Json::CharReader>(readerBuilder.newCharReader());
+ std::string errors;
+ if (reader->parse(json, json + strlen(json), &value, &errors))
+ {
+ return true;
+ }
+
+ // The CharReader API allows us to set failIfExtra, unlike Reader, but does
+ // not allow us to get structured errors. We get to try to manually undo
+ // the work it did to create a string from the structured errors it had.
+ ChipLogError(NotSpecified, "Error parsing JSON for %s:", label);
+
+ // For each error "errors" has the following:
+ //
+ // 1) A line starting with "* " that has line/column info
+ // 2) A line with the error message.
+ // 3) An optional line with some extra info.
+ //
+ // We keep track of the last error column, in case the error message
+ // reporting needs it.
+ std::istringstream stream(errors);
+ std::string error;
+ chip::Optional<unsigned> errorColumn;
+ while (getline(stream, error))
+ {
+ if (error.rfind("* ", 0) == 0)
+ {
+ // Flush out any pending error location.
+ LogErrorLocation(errorColumn, json);
+
+ // The format of this line is:
+ //
+ // * Line N, Column M
+ //
+ // Unfortunately it does not indicate end of error, so we can only
+ // show its start.
+ unsigned errorLine; // ignored in practice
+ if (sscanf(error.c_str(), "* Line %u, Column %u", &errorLine, &errorColumn.Emplace()) != 2)
+ {
+ ChipLogError(NotSpecified, "Unexpected location string: %s\n", error.c_str());
+ // We don't know how to make sense of this thing anymore.
+ break;
+ }
+ if (errorColumn.Value() == 0)
+ {
+ ChipLogError(NotSpecified, "Expected error column to be at least 1");
+ // We don't know how to make sense of this thing anymore.
+ break;
+ }
+ // We are using our column numbers as offsets, so want them to be
+ // 0-based.
+ --errorColumn.Value();
+ }
+ else
+ {
+ ChipLogError(NotSpecified, " %s", error.c_str());
+ if (error == " Missing ',' or '}' in object declaration" && errorColumn.HasValue() && errorColumn.Value() > 0 &&
+ json[errorColumn.Value() - 1] == '0' && (json[errorColumn.Value()] == 'x' || json[errorColumn.Value()] == 'X'))
+ {
+ // Log the error location marker before showing the NOTE
+ // message.
+ LogErrorLocation(errorColumn, json);
+ ChipLogError(NotSpecified,
+ "NOTE: JSON does not allow hex syntax beginning with 0x for numbers. Try putting the hex number "
+ "in quotes (like {\"name\": \"0x100\"}).");
+ }
+ }
+ }
+
+ // Write out the marker for our last error.
+ LogErrorLocation(errorColumn, json);
+
+ return false;
+ }
+
+private:
+ static void LogErrorLocation(chip::Optional<unsigned> & errorColumn, const char * json)
+ {
+#if CHIP_ERROR_LOGGING
+ if (!errorColumn.HasValue())
+ {
+ return;
+ }
+
+ const char * sourceText = json;
+ unsigned error_start = errorColumn.Value();
+ // The whole JSON string might be too long to fit in our log
+ // messages. Just include 30 chars before the error.
+ constexpr ptrdiff_t kMaxContext = 30;
+ std::string errorMarker;
+ if (error_start > kMaxContext)
+ {
+ sourceText += (error_start - kMaxContext);
+ error_start = kMaxContext;
+ ChipLogError(NotSpecified, "... %s", sourceText);
+ // Add markers corresponding to the "... " above.
+ errorMarker += "----";
+ }
+ else
+ {
+ ChipLogError(NotSpecified, "%s", sourceText);
+ }
+ for (unsigned i = 0; i < error_start; ++i)
+ {
+ errorMarker += "-";
+ }
+ errorMarker += "^";
+ ChipLogError(NotSpecified, "%s", errorMarker.c_str());
+ errorColumn.ClearValue();
+#endif // CHIP_ERROR_LOGGING
+ }
+};
diff --git a/examples/fabric-admin/commands/clusters/ModelCommand.cpp b/examples/fabric-admin/commands/clusters/ModelCommand.cpp
new file mode 100644
index 0000000..8f379db
--- /dev/null
+++ b/examples/fabric-admin/commands/clusters/ModelCommand.cpp
@@ -0,0 +1,105 @@
+/*
+ * 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 "ModelCommand.h"
+
+#include <app/InteractionModelEngine.h>
+#include <app/icd/client/DefaultICDClientStorage.h>
+#include <inttypes.h>
+
+using namespace ::chip;
+
+CHIP_ERROR ModelCommand::RunCommand()
+{
+
+ if (IsGroupId(mDestinationId))
+ {
+ FabricIndex fabricIndex = CurrentCommissioner().GetFabricIndex();
+ ChipLogProgress(chipTool, "Sending command to group 0x%x", GroupIdFromNodeId(mDestinationId));
+
+ return SendGroupCommand(GroupIdFromNodeId(mDestinationId), fabricIndex);
+ }
+
+ ChipLogProgress(NotSpecified, "Sending command to node " ChipLogFormatX64, ChipLogValueX64(mDestinationId));
+ CheckPeerICDType();
+
+ CommissioneeDeviceProxy * commissioneeDeviceProxy = nullptr;
+ if (CHIP_NO_ERROR == CurrentCommissioner().GetDeviceBeingCommissioned(mDestinationId, &commissioneeDeviceProxy))
+ {
+ return SendCommand(commissioneeDeviceProxy, mEndPointId);
+ }
+
+ return CurrentCommissioner().GetConnectedDevice(mDestinationId, &mOnDeviceConnectedCallback,
+ &mOnDeviceConnectionFailureCallback);
+}
+
+void ModelCommand::OnDeviceConnectedFn(void * context, chip::Messaging::ExchangeManager & exchangeMgr,
+ const chip::SessionHandle & sessionHandle)
+{
+ ModelCommand * command = reinterpret_cast<ModelCommand *>(context);
+ VerifyOrReturn(command != nullptr, ChipLogError(NotSpecified, "OnDeviceConnectedFn: context is null"));
+
+ chip::OperationalDeviceProxy device(&exchangeMgr, sessionHandle);
+ CHIP_ERROR err = command->SendCommand(&device, command->mEndPointId);
+ VerifyOrReturn(CHIP_NO_ERROR == err, command->SetCommandExitStatus(err));
+}
+
+void ModelCommand::OnDeviceConnectionFailureFn(void * context, const chip::ScopedNodeId & peerId, CHIP_ERROR err)
+{
+ LogErrorOnFailure(err);
+
+ ModelCommand * command = reinterpret_cast<ModelCommand *>(context);
+ VerifyOrReturn(command != nullptr, ChipLogError(NotSpecified, "OnDeviceConnectionFailureFn: context is null"));
+ command->SetCommandExitStatus(err);
+}
+
+void ModelCommand::Shutdown()
+{
+ mOnDeviceConnectedCallback.Cancel();
+ mOnDeviceConnectionFailureCallback.Cancel();
+
+ CHIPCommand::Shutdown();
+}
+
+void ModelCommand::CheckPeerICDType()
+{
+ if (mIsPeerLIT.HasValue())
+ {
+ ChipLogProgress(NotSpecified, "Peer ICD type is set to %s", mIsPeerLIT.Value() == 1 ? "LIT-ICD" : "non LIT-ICD");
+ return;
+ }
+
+ app::ICDClientInfo info;
+ auto destinationPeerId = chip::ScopedNodeId(mDestinationId, CurrentCommissioner().GetFabricIndex());
+ auto iter = CHIPCommand::sICDClientStorage.IterateICDClientInfo();
+ if (iter == nullptr)
+ {
+ return;
+ }
+ app::DefaultICDClientStorage::ICDClientInfoIteratorWrapper clientInfoIteratorWrapper(iter);
+
+ while (iter->Next(info))
+ {
+ if (ScopedNodeId(info.peer_node.GetNodeId(), info.peer_node.GetFabricIndex()) == destinationPeerId)
+ {
+ ChipLogProgress(NotSpecified, "Peer is a registered LIT ICD.");
+ mIsPeerLIT.SetValue(true);
+ return;
+ }
+ }
+}
diff --git a/examples/fabric-admin/commands/clusters/ModelCommand.h b/examples/fabric-admin/commands/clusters/ModelCommand.h
new file mode 100644
index 0000000..c14d3c9
--- /dev/null
+++ b/examples/fabric-admin/commands/clusters/ModelCommand.h
@@ -0,0 +1,91 @@
+/*
+ * 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
+
+#ifdef CONFIG_USE_LOCAL_STORAGE
+#include <controller/ExamplePersistentStorage.h>
+#endif // CONFIG_USE_LOCAL_STORAGE
+
+#include "../common/CHIPCommand.h"
+#include <lib/core/CHIPEncoding.h>
+
+class ModelCommand : public CHIPCommand
+{
+public:
+ ModelCommand(const char * commandName, CredentialIssuerCommands * credsIssuerConfig, bool supportsMultipleEndpoints = false) :
+ CHIPCommand(commandName, credsIssuerConfig), mOnDeviceConnectedCallback(OnDeviceConnectedFn, this),
+ mOnDeviceConnectionFailureCallback(OnDeviceConnectionFailureFn, this), mSupportsMultipleEndpoints(supportsMultipleEndpoints)
+ {}
+
+ void AddArguments(bool skipEndpoints = false)
+ {
+ AddArgument(
+ "destination-id", 0, UINT64_MAX, &mDestinationId,
+ "64-bit node or group identifier.\n Group identifiers are detected by being in the 0xFFFF'FFFF'FFFF'xxxx range.");
+ if (skipEndpoints == false)
+ {
+ if (mSupportsMultipleEndpoints)
+ {
+ AddArgument("endpoint-ids", 0, UINT16_MAX, &mEndPointId,
+ "Comma-separated list of endpoint ids (e.g. \"1\" or \"1,2,3\").\n Allowed to be 0xFFFF to indicate a "
+ "wildcard endpoint.");
+ }
+ else
+ {
+ AddArgument("endpoint-id-ignored-for-group-commands", 0, UINT16_MAX, &mEndPointId,
+ "Endpoint the command is targeted at.");
+ }
+ }
+ AddArgument(
+ "lit-icd-peer", 0, 1, &mIsPeerLIT,
+ "Whether to treat the peer as a LIT ICD. false: Always no, true: Always yes, (not set): Yes if the peer is registered "
+ "to this controller.");
+ AddArgument("timeout", 0, UINT16_MAX, &mTimeout);
+ }
+
+ /////////// CHIPCommand Interface /////////
+ CHIP_ERROR RunCommand() override;
+ chip::System::Clock::Timeout GetWaitDuration() const override { return chip::System::Clock::Seconds16(mTimeout.ValueOr(20)); }
+
+ virtual CHIP_ERROR SendCommand(chip::DeviceProxy * device, std::vector<chip::EndpointId> endPointIds) = 0;
+
+ virtual CHIP_ERROR SendGroupCommand(chip::GroupId groupId, chip::FabricIndex fabricIndex) { return CHIP_ERROR_BAD_REQUEST; };
+
+ void Shutdown() override;
+
+protected:
+ bool IsPeerLIT() { return mIsPeerLIT.ValueOr(false); }
+
+ chip::Optional<uint16_t> mTimeout;
+
+private:
+ chip::NodeId mDestinationId;
+ std::vector<chip::EndpointId> mEndPointId;
+ chip::Optional<bool> mIsPeerLIT;
+
+ void CheckPeerICDType();
+
+ static void OnDeviceConnectedFn(void * context, chip::Messaging::ExchangeManager & exchangeMgr,
+ const chip::SessionHandle & sessionHandle);
+ static void OnDeviceConnectionFailureFn(void * context, const chip::ScopedNodeId & peerId, CHIP_ERROR error);
+
+ chip::Callback::Callback<chip::OnDeviceConnected> mOnDeviceConnectedCallback;
+ chip::Callback::Callback<chip::OnDeviceConnectionFailure> mOnDeviceConnectionFailureCallback;
+ const bool mSupportsMultipleEndpoints;
+};
diff --git a/examples/fabric-admin/commands/clusters/ReportCommand.h b/examples/fabric-admin/commands/clusters/ReportCommand.h
new file mode 100644
index 0000000..4e9dbd0
--- /dev/null
+++ b/examples/fabric-admin/commands/clusters/ReportCommand.h
@@ -0,0 +1,551 @@
+/*
+ * 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/tests/suites/commands/interaction_model/InteractionModel.h>
+
+#include "DataModelLogger.h"
+#include "ModelCommand.h"
+
+class ReportCommand : public InteractionModelReports, public ModelCommand, public chip::app::ReadClient::Callback
+{
+public:
+ ReportCommand(const char * commandName, CredentialIssuerCommands * credsIssuerConfig) :
+ InteractionModelReports(this), ModelCommand(commandName, credsIssuerConfig, /* supportsMultipleEndpoints = */ true)
+ {}
+
+ /////////// ReadClient Callback Interface /////////
+ void OnAttributeData(const chip::app::ConcreteDataAttributePath & path, chip::TLV::TLVReader * data,
+ const chip::app::StatusIB & status) override
+ {
+ CHIP_ERROR error = status.ToChipError();
+ if (CHIP_NO_ERROR != error)
+ {
+ LogErrorOnFailure(RemoteDataModelLogger::LogErrorAsJSON(path, status));
+
+ ChipLogError(NotSpecified, "Response Failure: %s", chip::ErrorStr(error));
+ mError = error;
+ return;
+ }
+
+ if (data == nullptr)
+ {
+ ChipLogError(NotSpecified, "Response Failure: No Data");
+ mError = CHIP_ERROR_INTERNAL;
+ return;
+ }
+
+ LogErrorOnFailure(RemoteDataModelLogger::LogAttributeAsJSON(path, data));
+
+ error = DataModelLogger::LogAttribute(path, data);
+ if (CHIP_NO_ERROR != error)
+ {
+ ChipLogError(NotSpecified, "Response Failure: Can not decode Data");
+ mError = error;
+ return;
+ }
+ }
+
+ void OnEventData(const chip::app::EventHeader & eventHeader, chip::TLV::TLVReader * data,
+ const chip::app::StatusIB * status) override
+ {
+ if (status != nullptr)
+ {
+ CHIP_ERROR error = status->ToChipError();
+ if (CHIP_NO_ERROR != error)
+ {
+ LogErrorOnFailure(RemoteDataModelLogger::LogErrorAsJSON(eventHeader, *status));
+
+ ChipLogError(NotSpecified, "Response Failure: %s", chip::ErrorStr(error));
+ mError = error;
+ return;
+ }
+ }
+
+ if (data == nullptr)
+ {
+ ChipLogError(NotSpecified, "Response Failure: No Data");
+ mError = CHIP_ERROR_INTERNAL;
+ return;
+ }
+
+ LogErrorOnFailure(RemoteDataModelLogger::LogEventAsJSON(eventHeader, data));
+
+ CHIP_ERROR error = DataModelLogger::LogEvent(eventHeader, data);
+ if (CHIP_NO_ERROR != error)
+ {
+ ChipLogError(NotSpecified, "Response Failure: Can not decode Data");
+ mError = error;
+ return;
+ }
+ }
+
+ void OnError(CHIP_ERROR error) override
+ {
+ LogErrorOnFailure(RemoteDataModelLogger::LogErrorAsJSON(error));
+
+ ChipLogProgress(NotSpecified, "Error: %s", chip::ErrorStr(error));
+ mError = error;
+ }
+
+ void OnDeallocatePaths(chip::app::ReadPrepareParams && aReadPrepareParams) override
+ {
+ InteractionModelReports::OnDeallocatePaths(std::move(aReadPrepareParams));
+ }
+
+ void Shutdown() override
+ {
+ // We don't shut down InteractionModelReports here; we leave it for
+ // Cleanup to handle.
+ mError = CHIP_NO_ERROR;
+ ModelCommand::Shutdown();
+ }
+
+ void Cleanup() override { InteractionModelReports::Shutdown(); }
+
+protected:
+ // Use a 3x-longer-than-default timeout because wildcard reads can take a
+ // while.
+ chip::System::Clock::Timeout GetWaitDuration() const override
+ {
+ return mTimeout.HasValue() ? chip::System::Clock::Seconds16(mTimeout.Value()) : (ModelCommand::GetWaitDuration() * 3);
+ }
+
+ CHIP_ERROR mError = CHIP_NO_ERROR;
+};
+
+class ReadCommand : public ReportCommand
+{
+protected:
+ ReadCommand(const char * commandName, CredentialIssuerCommands * credsIssuerConfig) :
+ ReportCommand(commandName, credsIssuerConfig)
+ {}
+
+ void OnDone(chip::app::ReadClient * aReadClient) override
+ {
+ InteractionModelReports::CleanupReadClient(aReadClient);
+ SetCommandExitStatus(mError);
+ }
+};
+
+class SubscribeCommand : public ReportCommand
+{
+protected:
+ SubscribeCommand(const char * commandName, CredentialIssuerCommands * credsIssuerConfig) :
+ ReportCommand(commandName, credsIssuerConfig)
+ {}
+
+ void OnSubscriptionEstablished(chip::SubscriptionId subscriptionId) override
+ {
+ mSubscriptionEstablished = true;
+ SetCommandExitStatus(CHIP_NO_ERROR);
+ }
+
+ void OnDone(chip::app::ReadClient * aReadClient) override
+ {
+ InteractionModelReports::CleanupReadClient(aReadClient);
+
+ if (!mSubscriptionEstablished)
+ {
+ SetCommandExitStatus(mError);
+ }
+ // else we must be getting here from Cleanup(), which means we have
+ // already done our exit status thing.
+ }
+
+ void Shutdown() override
+ {
+ mSubscriptionEstablished = false;
+ ReportCommand::Shutdown();
+ }
+
+ // For subscriptions we always defer interactive cleanup. Either our
+ // ReadClients will terminate themselves (in which case they will be removed
+ // from our list anyway), or they should hang around until shutdown.
+ bool DeferInteractiveCleanup() override { return true; }
+
+private:
+ bool mSubscriptionEstablished = false;
+};
+
+class ReadAttribute : public ReadCommand
+{
+public:
+ ReadAttribute(CredentialIssuerCommands * credsIssuerConfig) : ReadCommand("read-by-id", credsIssuerConfig)
+ {
+ AddArgument("cluster-ids", 0, UINT32_MAX, &mClusterIds,
+ "Comma-separated list of cluster ids to read from (e.g. \"6\" or \"8,0x201\").\n Allowed to be 0xFFFFFFFF to "
+ "indicate a wildcard cluster.");
+ AddAttributeIdArgument();
+ AddCommonArguments();
+ ReadCommand::AddArguments();
+ }
+
+ ReadAttribute(chip::ClusterId clusterId, CredentialIssuerCommands * credsIssuerConfig) :
+ ReadCommand("read-by-id", credsIssuerConfig), mClusterIds(1, clusterId)
+ {
+ AddAttributeIdArgument();
+ AddCommonArguments();
+ ReadCommand::AddArguments();
+ }
+
+ ReadAttribute(chip::ClusterId clusterId, const char * attributeName, chip::AttributeId attributeId,
+ CredentialIssuerCommands * credsIssuerConfig) :
+ ReadCommand("read", credsIssuerConfig),
+ mClusterIds(1, clusterId), mAttributeIds(1, attributeId)
+ {
+ AddArgument("attr-name", attributeName);
+ AddCommonArguments();
+ ReadCommand::AddArguments();
+ }
+
+ ~ReadAttribute() {}
+
+ CHIP_ERROR SendCommand(chip::DeviceProxy * device, std::vector<chip::EndpointId> endpointIds) override
+ {
+ return ReadCommand::ReadAttribute(device, endpointIds, mClusterIds, mAttributeIds);
+ }
+
+private:
+ void AddAttributeIdArgument()
+ {
+ AddArgument("attribute-ids", 0, UINT32_MAX, &mAttributeIds,
+ "Comma-separated list of attribute ids to read (e.g. \"0\" or \"1,0xFFFC,0xFFFD\").\n Allowed to be "
+ "0xFFFFFFFF to indicate a wildcard attribute.");
+ }
+
+ void AddCommonArguments()
+ {
+ AddArgument("fabric-filtered", 0, 1, &mFabricFiltered,
+ "Boolean indicating whether to do a fabric-filtered read. Defaults to true.");
+ AddArgument("data-version", 0, UINT32_MAX, &mDataVersions,
+ "Comma-separated list of data versions for the clusters being read.");
+ }
+
+ std::vector<chip::ClusterId> mClusterIds;
+ std::vector<chip::AttributeId> mAttributeIds;
+};
+
+class SubscribeAttribute : public SubscribeCommand
+{
+public:
+ SubscribeAttribute(CredentialIssuerCommands * credsIssuerConfig) : SubscribeCommand("subscribe-by-id", credsIssuerConfig)
+ {
+ AddArgument("cluster-ids", 0, UINT32_MAX, &mClusterIds,
+ "Comma-separated list of cluster ids to subscribe to (e.g. \"6\" or \"8,0x201\").\n Allowed to be 0xFFFFFFFF "
+ "to indicate a wildcard cluster.");
+ AddAttributeIdArgument();
+ AddCommonArguments();
+ SubscribeCommand::AddArguments();
+ }
+
+ SubscribeAttribute(chip::ClusterId clusterId, CredentialIssuerCommands * credsIssuerConfig) :
+ SubscribeCommand("subscribe-by-id", credsIssuerConfig), mClusterIds(1, clusterId)
+ {
+ AddAttributeIdArgument();
+ AddCommonArguments();
+ SubscribeCommand::AddArguments();
+ }
+
+ SubscribeAttribute(chip::ClusterId clusterId, const char * attributeName, chip::AttributeId attributeId,
+ CredentialIssuerCommands * credsIssuerConfig) :
+ SubscribeCommand("subscribe", credsIssuerConfig),
+ mClusterIds(1, clusterId), mAttributeIds(1, attributeId)
+ {
+ AddArgument("attr-name", attributeName);
+ AddCommonArguments();
+ SubscribeCommand::AddArguments();
+ }
+
+ ~SubscribeAttribute() {}
+
+ CHIP_ERROR SendCommand(chip::DeviceProxy * device, std::vector<chip::EndpointId> endpointIds) override
+ {
+ SubscribeCommand::SetPeerLIT(IsPeerLIT());
+ return SubscribeCommand::SubscribeAttribute(device, endpointIds, mClusterIds, mAttributeIds);
+ }
+
+private:
+ void AddAttributeIdArgument()
+ {
+ AddArgument("attribute-ids", 0, UINT32_MAX, &mAttributeIds,
+ "Comma-separated list of attribute ids to subscribe to (e.g. \"0\" or \"1,0xFFFC,0xFFFD\").\n Allowed to be "
+ "0xFFFFFFFF to indicate a wildcard attribute.");
+ }
+
+ void AddCommonArguments()
+ {
+ AddArgument("min-interval", 0, UINT16_MAX, &mMinInterval,
+ "Server should not send a new report if less than this number of seconds has elapsed since the last report.");
+ AddArgument("max-interval", 0, UINT16_MAX, &mMaxInterval,
+ "Server must send a report if this number of seconds has elapsed since the last report.");
+ AddArgument("fabric-filtered", 0, 1, &mFabricFiltered,
+ "Boolean indicating whether to do a fabric-filtered subscription. Defaults to true.");
+ AddArgument("data-version", 0, UINT32_MAX, &mDataVersions,
+ "Comma-separated list of data versions for the clusters being subscribed to.");
+ AddArgument("keepSubscriptions", 0, 1, &mKeepSubscriptions,
+ "Boolean indicating whether to keep existing subscriptions when creating the new one. Defaults to false.");
+ AddArgument("auto-resubscribe", 0, 1, &mAutoResubscribe,
+ "Boolean indicating whether the subscription should auto-resubscribe. Defaults to false.");
+ }
+
+ std::vector<chip::ClusterId> mClusterIds;
+ std::vector<chip::AttributeId> mAttributeIds;
+};
+
+class ReadEvent : public ReadCommand
+{
+public:
+ ReadEvent(CredentialIssuerCommands * credsIssuerConfig) : ReadCommand("read-event-by-id", credsIssuerConfig)
+ {
+ AddArgument("cluster-id", 0, UINT32_MAX, &mClusterIds);
+ AddArgument("event-id", 0, UINT32_MAX, &mEventIds);
+ AddArgument("fabric-filtered", 0, 1, &mFabricFiltered);
+ AddArgument("event-min", 0, UINT64_MAX, &mEventNumber);
+ ReadCommand::AddArguments();
+ }
+
+ ReadEvent(chip::ClusterId clusterId, CredentialIssuerCommands * credsIssuerConfig) :
+ ReadCommand("read-event-by-id", credsIssuerConfig), mClusterIds(1, clusterId)
+ {
+ AddArgument("event-id", 0, UINT32_MAX, &mEventIds);
+ AddArgument("fabric-filtered", 0, 1, &mFabricFiltered);
+ AddArgument("event-min", 0, UINT64_MAX, &mEventNumber);
+ ReadCommand::AddArguments();
+ }
+
+ ReadEvent(chip::ClusterId clusterId, const char * eventName, chip::EventId eventId,
+ CredentialIssuerCommands * credsIssuerConfig) :
+ ReadCommand("read-event", credsIssuerConfig),
+ mClusterIds(1, clusterId), mEventIds(1, eventId)
+ {
+ AddArgument("event-name", eventName);
+ AddArgument("fabric-filtered", 0, 1, &mFabricFiltered);
+ AddArgument("event-min", 0, UINT64_MAX, &mEventNumber);
+ ReadCommand::AddArguments();
+ }
+
+ ~ReadEvent() {}
+
+ CHIP_ERROR SendCommand(chip::DeviceProxy * device, std::vector<chip::EndpointId> endpointIds) override
+ {
+ return ReadCommand::ReadEvent(device, endpointIds, mClusterIds, mEventIds);
+ }
+
+private:
+ std::vector<chip::ClusterId> mClusterIds;
+ std::vector<chip::EventId> mEventIds;
+};
+
+class SubscribeEvent : public SubscribeCommand
+{
+public:
+ SubscribeEvent(CredentialIssuerCommands * credsIssuerConfig) : SubscribeCommand("subscribe-event-by-id", credsIssuerConfig)
+ {
+ AddArgument("cluster-id", 0, UINT32_MAX, &mClusterIds);
+ AddArgument("event-id", 0, UINT32_MAX, &mEventIds);
+ AddCommonArguments();
+ SubscribeCommand::AddArguments();
+ }
+
+ SubscribeEvent(chip::ClusterId clusterId, CredentialIssuerCommands * credsIssuerConfig) :
+ SubscribeCommand("subscribe-event-by-id", credsIssuerConfig), mClusterIds(1, clusterId)
+ {
+ AddArgument("event-id", 0, UINT32_MAX, &mEventIds);
+ AddCommonArguments();
+ SubscribeCommand::AddArguments();
+ }
+
+ SubscribeEvent(chip::ClusterId clusterId, const char * eventName, chip::EventId eventId,
+ CredentialIssuerCommands * credsIssuerConfig) :
+ SubscribeCommand("subscribe-event", credsIssuerConfig),
+ mClusterIds(1, clusterId), mEventIds(1, eventId)
+ {
+ AddArgument("event-name", eventName, "Event name.");
+ AddCommonArguments();
+ SubscribeCommand::AddArguments();
+ }
+
+ void AddCommonArguments()
+ {
+ AddArgument("min-interval", 0, UINT16_MAX, &mMinInterval,
+ "The requested minimum interval between reports. Sets MinIntervalFloor in the Subscribe Request.");
+ AddArgument("max-interval", 0, UINT16_MAX, &mMaxInterval,
+ "The requested maximum interval between reports. Sets MaxIntervalCeiling in the Subscribe Request.");
+ AddArgument("fabric-filtered", 0, 1, &mFabricFiltered);
+ AddArgument("event-min", 0, UINT64_MAX, &mEventNumber);
+ AddArgument("keepSubscriptions", 0, 1, &mKeepSubscriptions,
+ "false - Terminate existing subscriptions from initiator.\n true - Leave existing subscriptions in place.");
+ AddArgument(
+ "is-urgent", 0, 1, &mIsUrgents,
+ "Sets isUrgent in the Subscribe Request.\n"
+ " The queueing of any urgent event SHALL force an immediate generation of reports containing all events queued "
+ "leading up to (and including) the urgent event in question.\n"
+ " This argument takes a comma separated list of true/false values.\n"
+ " If the number of paths exceeds the number of entries provided to is-urgent, then isUrgent will be false for the "
+ "extra paths.");
+ AddArgument("auto-resubscribe", 0, 1, &mAutoResubscribe,
+ "Boolean indicating whether the subscription should auto-resubscribe. Defaults to false.");
+ }
+
+ ~SubscribeEvent() {}
+
+ CHIP_ERROR SendCommand(chip::DeviceProxy * device, std::vector<chip::EndpointId> endpointIds) override
+ {
+ SubscribeCommand::SetPeerLIT(IsPeerLIT());
+ return SubscribeCommand::SubscribeEvent(device, endpointIds, mClusterIds, mEventIds);
+ }
+
+private:
+ std::vector<chip::ClusterId> mClusterIds;
+ std::vector<chip::EventId> mEventIds;
+};
+
+class ReadNone : public ReadCommand
+{
+public:
+ ReadNone(CredentialIssuerCommands * credsIssuerConfig) : ReadCommand("read-none", credsIssuerConfig)
+ {
+ AddArgument("fabric-filtered", 0, 1, &mFabricFiltered,
+ "Boolean indicating whether to do a fabric-filtered read. Defaults to true.");
+ AddArgument("data-versions", 0, UINT32_MAX, &mDataVersions,
+ "Comma-separated list of data versions for the clusters being read.");
+ AddArgument("event-min", 0, UINT64_MAX, &mEventNumber);
+ ReadCommand::AddArguments(true /* skipEndpoints */);
+ }
+
+ ~ReadNone() {}
+
+ void OnDone(chip::app::ReadClient * aReadClient) override
+ {
+ InteractionModelReports::CleanupReadClient(aReadClient);
+ SetCommandExitStatus(mError);
+ }
+
+ CHIP_ERROR SendCommand(chip::DeviceProxy * device, std::vector<chip::EndpointId> endpointIds) override
+ {
+ return ReadCommand::ReadNone(device);
+ }
+};
+
+class ReadAll : public ReadCommand
+{
+public:
+ ReadAll(CredentialIssuerCommands * credsIssuerConfig) : ReadCommand("read-all", credsIssuerConfig)
+ {
+ AddArgument("cluster-ids", 0, UINT32_MAX, &mClusterIds,
+ "Comma-separated list of cluster ids to read from (e.g. \"6\" or \"8,0x201\").\n Allowed to be 0xFFFFFFFF to "
+ "indicate a wildcard cluster.");
+ AddArgument("attribute-ids", 0, UINT32_MAX, &mAttributeIds,
+ "Comma-separated list of attribute ids to read (e.g. \"0\" or \"1,0xFFFC,0xFFFD\").\n Allowed to be "
+ "0xFFFFFFFF to indicate a wildcard attribute.");
+ AddArgument("event-ids", 0, UINT32_MAX, &mEventIds,
+ "Comma-separated list of event ids to read (e.g. \"0\" or \"1,2,3\").\n Allowed to be "
+ "0xFFFFFFFF to indicate a wildcard event.");
+ AddArgument("fabric-filtered", 0, 1, &mFabricFiltered,
+ "Boolean indicating whether to do a fabric-filtered read. Defaults to true.");
+ AddArgument("data-versions", 0, UINT32_MAX, &mDataVersions,
+ "Comma-separated list of data versions for the clusters being read.");
+ AddArgument("event-min", 0, UINT64_MAX, &mEventNumber);
+ ReadCommand::AddArguments();
+ }
+
+ ~ReadAll() {}
+
+ void OnDone(chip::app::ReadClient * aReadClient) override
+ {
+ InteractionModelReports::CleanupReadClient(aReadClient);
+ SetCommandExitStatus(mError);
+ }
+
+ CHIP_ERROR SendCommand(chip::DeviceProxy * device, std::vector<chip::EndpointId> endpointIds) override
+ {
+ return ReadCommand::ReadAll(device, endpointIds, mClusterIds, mAttributeIds, mEventIds);
+ }
+
+private:
+ std::vector<chip::ClusterId> mClusterIds;
+ std::vector<chip::AttributeId> mAttributeIds;
+ std::vector<chip::EventId> mEventIds;
+};
+
+class SubscribeNone : public SubscribeCommand
+{
+public:
+ SubscribeNone(CredentialIssuerCommands * credsIssuerConfig) : SubscribeCommand("subscribe-none", credsIssuerConfig)
+ {
+ AddArgument("min-interval", 0, UINT16_MAX, &mMinInterval,
+ "The requested minimum interval between reports. Sets MinIntervalFloor in the Subscribe Request.");
+ AddArgument("max-interval", 0, UINT16_MAX, &mMaxInterval,
+ "The requested maximum interval between reports. Sets MaxIntervalCeiling in the Subscribe Request.");
+ AddArgument("fabric-filtered", 0, 1, &mFabricFiltered,
+ "Boolean indicating whether to do a fabric-filtered read. Defaults to true.");
+ AddArgument("event-min", 0, UINT64_MAX, &mEventNumber);
+ AddArgument("keepSubscriptions", 0, 1, &mKeepSubscriptions,
+ "false - Terminate existing subscriptions from initiator.\n true - Leave existing subscriptions in place.");
+ SubscribeCommand::AddArguments(true /* skipEndpoints */);
+ }
+
+ ~SubscribeNone() {}
+
+ CHIP_ERROR SendCommand(chip::DeviceProxy * device, std::vector<chip::EndpointId> endpointIds) override
+ {
+ return SubscribeCommand::SubscribeNone(device);
+ }
+};
+
+class SubscribeAll : public SubscribeCommand
+{
+public:
+ SubscribeAll(CredentialIssuerCommands * credsIssuerConfig) : SubscribeCommand("subscribe-all", credsIssuerConfig)
+ {
+ AddArgument("cluster-ids", 0, UINT32_MAX, &mClusterIds,
+ "Comma-separated list of cluster ids to read from (e.g. \"6\" or \"8,0x201\").\n Allowed to be 0xFFFFFFFF to "
+ "indicate a wildcard cluster.");
+ AddArgument("attribute-ids", 0, UINT32_MAX, &mAttributeIds,
+ "Comma-separated list of attribute ids to read (e.g. \"0\" or \"1,0xFFFC,0xFFFD\").\n Allowed to be "
+ "0xFFFFFFFF to indicate a wildcard attribute.");
+ AddArgument("event-ids", 0, UINT32_MAX, &mEventIds,
+ "Comma-separated list of event ids to read (e.g. \"0\" or \"1,2,3\").\n Allowed to be "
+ "0xFFFFFFFF to indicate a wildcard event.");
+ AddArgument("min-interval", 0, UINT16_MAX, &mMinInterval,
+ "The requested minimum interval between reports. Sets MinIntervalFloor in the Subscribe Request.");
+ AddArgument("max-interval", 0, UINT16_MAX, &mMaxInterval,
+ "The requested maximum interval between reports. Sets MaxIntervalCeiling in the Subscribe Request.");
+ AddArgument("fabric-filtered", 0, 1, &mFabricFiltered,
+ "Boolean indicating whether to do a fabric-filtered read. Defaults to true.");
+ AddArgument("event-min", 0, UINT64_MAX, &mEventNumber);
+ AddArgument("keepSubscriptions", 0, 1, &mKeepSubscriptions,
+ "false - Terminate existing subscriptions from initiator.\n true - Leave existing subscriptions in place.");
+ SubscribeCommand::AddArguments();
+ }
+
+ ~SubscribeAll() {}
+
+ CHIP_ERROR SendCommand(chip::DeviceProxy * device, std::vector<chip::EndpointId> endpointIds) override
+ {
+ SubscribeCommand::SetPeerLIT(IsPeerLIT());
+ return SubscribeCommand::SubscribeAll(device, endpointIds, mClusterIds, mAttributeIds, mEventIds);
+ }
+
+private:
+ std::vector<chip::ClusterId> mClusterIds;
+ std::vector<chip::AttributeId> mAttributeIds;
+ std::vector<chip::EventId> mEventIds;
+};
diff --git a/examples/fabric-admin/commands/clusters/SubscriptionsCommands.h b/examples/fabric-admin/commands/clusters/SubscriptionsCommands.h
new file mode 100644
index 0000000..625e5a7
--- /dev/null
+++ b/examples/fabric-admin/commands/clusters/SubscriptionsCommands.h
@@ -0,0 +1,108 @@
+/*
+ * 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 <lib/core/CHIPError.h>
+
+#include <commands/common/CHIPCommand.h>
+#include <commands/common/Commands.h>
+
+class ShutdownSubscription : public CHIPCommand
+{
+public:
+ ShutdownSubscription(CredentialIssuerCommands * credsIssuerConfig) :
+ CHIPCommand("shutdown-one", credsIssuerConfig,
+ "Shut down a single subscription, identified by its subscription id and target node id.")
+ {
+ AddArgument("subscription-id", 0, UINT32_MAX, &mSubscriptionId);
+ AddArgument("node-id", 0, UINT64_MAX, &mNodeId,
+ "The node id, scoped to the commissioner name the command is running under.");
+ }
+
+ /////////// CHIPCommand Interface /////////
+ CHIP_ERROR RunCommand() override
+ {
+ CHIP_ERROR err = chip::app::InteractionModelEngine::GetInstance()->ShutdownSubscription(
+ chip::ScopedNodeId(mNodeId, CurrentCommissioner().GetFabricIndex()), mSubscriptionId);
+ SetCommandExitStatus(err);
+ return CHIP_NO_ERROR;
+ }
+ chip::System::Clock::Timeout GetWaitDuration() const override { return chip::System::Clock::Seconds16(10); }
+
+private:
+ chip::SubscriptionId mSubscriptionId;
+ chip::NodeId mNodeId;
+};
+
+class ShutdownSubscriptionsForNode : public CHIPCommand
+{
+public:
+ ShutdownSubscriptionsForNode(CredentialIssuerCommands * credsIssuerConfig) :
+ CHIPCommand("shutdown-all-for-node", credsIssuerConfig, "Shut down all subscriptions targeting a given node.")
+ {
+ AddArgument("node-id", 0, UINT64_MAX, &mNodeId,
+ "The node id, scoped to the commissioner name the command is running under.");
+ }
+
+ /////////// CHIPCommand Interface /////////
+ CHIP_ERROR RunCommand() override
+ {
+ chip::app::InteractionModelEngine::GetInstance()->ShutdownSubscriptions(CurrentCommissioner().GetFabricIndex(), mNodeId);
+
+ SetCommandExitStatus(CHIP_NO_ERROR);
+ return CHIP_NO_ERROR;
+ }
+ chip::System::Clock::Timeout GetWaitDuration() const override { return chip::System::Clock::Seconds16(10); }
+
+private:
+ chip::NodeId mNodeId;
+};
+
+class ShutdownAllSubscriptions : public CHIPCommand
+{
+public:
+ ShutdownAllSubscriptions(CredentialIssuerCommands * credsIssuerConfig) :
+ CHIPCommand("shutdown-all", credsIssuerConfig, "Shut down all subscriptions to all nodes.")
+ {}
+
+ /////////// CHIPCommand Interface /////////
+ CHIP_ERROR RunCommand() override
+ {
+ chip::app::InteractionModelEngine::GetInstance()->ShutdownAllSubscriptions();
+
+ SetCommandExitStatus(CHIP_NO_ERROR);
+ return CHIP_NO_ERROR;
+ }
+ chip::System::Clock::Timeout GetWaitDuration() const override { return chip::System::Clock::Seconds16(10); }
+
+private:
+};
+
+void registerCommandsSubscriptions(Commands & commands, CredentialIssuerCommands * credsIssuerConfig)
+{
+ const char * clusterName = "Subscriptions";
+
+ commands_list clusterCommands = {
+ make_unique<ShutdownSubscription>(credsIssuerConfig), //
+ make_unique<ShutdownSubscriptionsForNode>(credsIssuerConfig), //
+ make_unique<ShutdownAllSubscriptions>(credsIssuerConfig), //
+ };
+
+ commands.RegisterCommandSet(clusterName, clusterCommands, "Commands for shutting down subscriptions.");
+}
diff --git a/examples/fabric-admin/commands/clusters/WriteAttributeCommand.h b/examples/fabric-admin/commands/clusters/WriteAttributeCommand.h
new file mode 100644
index 0000000..8424e95
--- /dev/null
+++ b/examples/fabric-admin/commands/clusters/WriteAttributeCommand.h
@@ -0,0 +1,286 @@
+/*
+ * 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/tests/suites/commands/interaction_model/InteractionModel.h>
+
+#include "DataModelLogger.h"
+#include "ModelCommand.h"
+
+inline constexpr char kWriteCommandKey[] = "write";
+inline constexpr char kWriteByIdCommandKey[] = "write-by-id";
+inline constexpr char kForceWriteCommandKey[] = "force-write";
+
+enum class WriteCommandType
+{
+ kWrite, // regular, writable attributes
+ kForceWrite, // forced writes, send a write command on something expected to fail
+};
+
+template <class T = std::vector<CustomArgument *>>
+class WriteAttribute : public InteractionModelWriter, public ModelCommand, public chip::app::WriteClient::Callback
+{
+public:
+ WriteAttribute(CredentialIssuerCommands * credsIssuerConfig) :
+ InteractionModelWriter(this), ModelCommand(kWriteByIdCommandKey, credsIssuerConfig)
+ {
+ AddArgumentClusterIds();
+ AddArgumentAttributeIds();
+ AddArgumentAttributeValues();
+ AddArguments();
+ }
+
+ WriteAttribute(chip::ClusterId clusterId, CredentialIssuerCommands * credsIssuerConfig) :
+ InteractionModelWriter(this), ModelCommand(kWriteByIdCommandKey, credsIssuerConfig), mClusterIds(1, clusterId)
+ {
+ AddArgumentAttributeIds();
+ AddArgumentAttributeValues();
+ AddArguments();
+ }
+
+ template <typename minType, typename maxType>
+ WriteAttribute(chip::ClusterId clusterId, const char * attributeName, minType minValue, maxType maxValue,
+ chip::AttributeId attributeId, WriteCommandType commandType, CredentialIssuerCommands * credsIssuerConfig) :
+ WriteAttribute(clusterId, attributeId, commandType, credsIssuerConfig)
+ {
+ AddArgumentAttributeName(attributeName);
+ AddArgumentAttributeValues(static_cast<int64_t>(minValue), static_cast<uint64_t>(maxValue));
+ AddArguments();
+ }
+
+ WriteAttribute(chip::ClusterId clusterId, const char * attributeName, float minValue, float maxValue,
+ chip::AttributeId attributeId, WriteCommandType commandType, CredentialIssuerCommands * credsIssuerConfig) :
+ WriteAttribute(clusterId, attributeId, commandType, credsIssuerConfig)
+ {
+ AddArgumentAttributeName(attributeName);
+ AddArgumentAttributeValues(minValue, maxValue);
+ AddArguments();
+ }
+
+ WriteAttribute(chip::ClusterId clusterId, const char * attributeName, double minValue, double maxValue,
+ chip::AttributeId attributeId, WriteCommandType commandType, CredentialIssuerCommands * credsIssuerConfig) :
+ WriteAttribute(clusterId, attributeId, commandType, credsIssuerConfig)
+ {
+ AddArgumentAttributeName(attributeName);
+ AddArgumentAttributeValues(minValue, maxValue);
+ AddArguments();
+ }
+
+ WriteAttribute(chip::ClusterId clusterId, const char * attributeName, chip::AttributeId attributeId,
+ WriteCommandType commandType, CredentialIssuerCommands * credsIssuerConfig) :
+ WriteAttribute(clusterId, attributeId, commandType, credsIssuerConfig)
+ {
+ AddArgumentAttributeName(attributeName);
+ AddArgumentAttributeValues();
+ AddArguments();
+ }
+
+ WriteAttribute(chip::ClusterId clusterId, const char * attributeName, chip::AttributeId attributeId,
+ TypedComplexArgument<T> & attributeParser, WriteCommandType commandType,
+ CredentialIssuerCommands * credsIssuerConfig) :
+ WriteAttribute(clusterId, attributeId, commandType, credsIssuerConfig)
+ {
+ AddArgumentAttributeName(attributeName);
+ AddArgumentAttributeValues(attributeParser);
+ AddArguments();
+ }
+
+ ~WriteAttribute() {}
+
+ CHIP_ERROR SendCommand(chip::DeviceProxy * device, std::vector<chip::EndpointId> endpointIds) override
+ {
+ return WriteAttribute::SendCommand(device, endpointIds, mClusterIds, mAttributeIds, mAttributeValues);
+ }
+
+ CHIP_ERROR SendGroupCommand(chip::GroupId groupId, chip::FabricIndex fabricIndex) override
+ {
+ return WriteAttribute::SendGroupCommand(groupId, fabricIndex, mClusterIds, mAttributeIds, mAttributeValues);
+ }
+
+ /////////// WriteClient Callback Interface /////////
+ void OnResponse(const chip::app::WriteClient * client, const chip::app::ConcreteDataAttributePath & path,
+ chip::app::StatusIB status) override
+ {
+ CHIP_ERROR error = status.ToChipError();
+ if (CHIP_NO_ERROR != error)
+ {
+ LogErrorOnFailure(RemoteDataModelLogger::LogErrorAsJSON(path, status));
+
+ ChipLogError(NotSpecified, "Response Failure: %s", chip::ErrorStr(error));
+ mError = error;
+ }
+ }
+
+ void OnError(const chip::app::WriteClient * client, CHIP_ERROR error) override
+ {
+ LogErrorOnFailure(RemoteDataModelLogger::LogErrorAsJSON(error));
+
+ ChipLogProgress(NotSpecified, "Error: %s", chip::ErrorStr(error));
+ mError = error;
+ }
+
+ void OnDone(chip::app::WriteClient * client) override
+ {
+ InteractionModelWriter::Shutdown();
+ SetCommandExitStatus(mError);
+ }
+
+ CHIP_ERROR SendCommand(chip::DeviceProxy * device, std::vector<chip::EndpointId> endpointIds,
+ std::vector<chip::ClusterId> clusterIds, std::vector<chip::AttributeId> attributeIds, const T & values)
+ {
+ return InteractionModelWriter::WriteAttribute(device, endpointIds, clusterIds, attributeIds, values);
+ }
+
+ CHIP_ERROR SendGroupCommand(chip::GroupId groupId, chip::FabricIndex fabricIndex, std::vector<chip::ClusterId> clusterIds,
+ std::vector<chip::AttributeId> attributeIds, const T & value)
+ {
+ ChipLogDetail(NotSpecified, "Sending Write Attribute to Group %u, on Fabric %x, for cluster %u with attributeId %u",
+ groupId, fabricIndex, clusterIds.at(0), attributeIds.at(0));
+ chip::Optional<chip::DataVersion> dataVersion = chip::NullOptional;
+ if (mDataVersions.HasValue())
+ {
+ dataVersion.SetValue(mDataVersions.Value().at(0));
+ }
+
+ return InteractionModelWriter::WriteGroupAttribute(groupId, fabricIndex, clusterIds.at(0), attributeIds.at(0), value,
+ dataVersion);
+ }
+
+ void Shutdown() override
+ {
+ mError = CHIP_NO_ERROR;
+ ModelCommand::Shutdown();
+ }
+
+protected:
+ WriteAttribute(const char * attributeName, CredentialIssuerCommands * credsIssuerConfig) :
+ InteractionModelWriter(this), ModelCommand(kWriteCommandKey, credsIssuerConfig)
+ {
+ // Subclasses are responsible for calling AddArguments.
+ }
+
+ void AddArgumentClusterIds()
+ {
+ AddArgument("cluster-ids", 0, UINT32_MAX, &mClusterIds,
+ "Comma-separated list of cluster ids to write to (e.g. \"6\" or \"6,0x201\").");
+ }
+
+ void AddArgumentAttributeIds()
+ {
+ AddArgument("attribute-ids", 0, UINT32_MAX, &mAttributeIds,
+ "Comma-separated list of attribute ids to write (e.g. \"16385\" or \"16385,0x4002\").");
+ }
+
+ void AddArgumentAttributeName(const char * attributeName)
+ {
+ AddArgument("attribute-name", attributeName, "The attribute name to write.");
+ }
+
+ template <typename U = T, std::enable_if_t<std::is_same<U, std::vector<CustomArgument *>>::value, int> = 0>
+ static const char * GetAttributeValuesDescription()
+ {
+ return "Semicolon-separated list of attribute values to write. Each value is represented as follows, depending on the "
+ "type:\n"
+ " * struct: a JSON-encoded object, with field ids as keys.\n"
+ " * list: a JSON-encoded array of values.\n"
+ " * null: A literal null.\n"
+ " * boolean: A literal true or false.\n"
+ " * unsigned integer: One of:\n"
+ " a) The number directly, as decimal.\n"
+ " b) The number directly, as 0x followed by hex digits. (Only for the toplevel value, not inside structs or "
+ "lists.)\n"
+ " c) A string starting with \"u:\" followed by decimal digits\n"
+ " * signed integer: One of:\n"
+ " a) The number directly, if it's negative.\n"
+ " c) A string starting with \"s:\" followed by decimal digits\n"
+ " * single-precision float: A string starting with \"f:\" followed by the number.\n"
+ " * double-precision float: One of:\n"
+ " a) The number directly, if it's not an integer.\n"
+ " b) A string starting with \"d:\" followed by the number.\n"
+ " * octet string: A string starting with \"hex:\" followed by the hex encoding of the bytes.\n"
+ " * string: A string with the characters.\n"
+ "\n"
+ " Example values: '10;20', '10;\"u:20\"', '\"hex:aabbcc\";\"hello\"'.";
+ }
+
+ static const char * GetTypedAttributeValuesDescription() { return "Comma-separated list of attribute values to write."; }
+
+ template <typename U = T, std::enable_if_t<!std::is_same<U, std::vector<CustomArgument *>>::value, int> = 0>
+ static const char * GetAttributeValuesDescription()
+ {
+ return GetTypedAttributeValuesDescription();
+ }
+
+ template <typename minType, typename maxType>
+ void AddArgumentAttributeValues(minType minValue, maxType maxValue)
+ {
+ AddArgument("attribute-values", minValue, maxValue, &mAttributeValues, GetTypedAttributeValuesDescription());
+ }
+
+ void AddArgumentAttributeValues() { AddArgument("attribute-values", &mAttributeValues, GetAttributeValuesDescription()); }
+
+ void AddArgumentAttributeValues(TypedComplexArgument<T> & attributeParser)
+ {
+ attributeParser.SetArgument(&mAttributeValues);
+ AddArgument("attribute-values", &attributeParser, GetTypedAttributeValuesDescription());
+ }
+
+ void AddArguments()
+ {
+ AddArgument("timedInteractionTimeoutMs", 0, UINT16_MAX, &mTimedInteractionTimeoutMs,
+ "If provided, do a timed write with the given timed interaction timeout. See \"7.6.10. Timed Interaction\" in "
+ "the Matter specification.");
+ AddArgument("busyWaitForMs", 0, UINT16_MAX, &mBusyWaitForMs,
+ "If provided, block the main thread processing for the given time right after sending a command.");
+ AddArgument("data-version", 0, UINT32_MAX, &mDataVersions,
+ "Comma-separated list of data versions for the clusters being written.");
+ AddArgument("suppressResponse", 0, 1, &mSuppressResponse);
+ AddArgument("repeat-count", 1, UINT16_MAX, &mRepeatCount);
+ AddArgument("repeat-delay-ms", 0, UINT16_MAX, &mRepeatDelayInMs);
+ ModelCommand::AddArguments();
+ }
+
+private:
+ // This constructor is private as it is not intended to be used from outside the class.
+ WriteAttribute(chip::ClusterId clusterId, chip::AttributeId attributeId, WriteCommandType commandType,
+ CredentialIssuerCommands * credsIssuerConfig) :
+ InteractionModelWriter(this),
+ ModelCommand(commandType == WriteCommandType::kWrite ? kWriteCommandKey : kForceWriteCommandKey, credsIssuerConfig),
+ mClusterIds(1, clusterId), mAttributeIds(1, attributeId)
+ {}
+
+ std::vector<chip::ClusterId> mClusterIds;
+ std::vector<chip::AttributeId> mAttributeIds;
+
+ CHIP_ERROR mError = CHIP_NO_ERROR;
+ T mAttributeValues;
+};
+
+template <class T>
+class WriteAttributeAsComplex : public WriteAttribute<T>
+{
+public:
+ WriteAttributeAsComplex(chip::ClusterId clusterId, const char * attributeName, chip::AttributeId attributeId,
+ WriteCommandType commandType, CredentialIssuerCommands * credsIssuerConfig) :
+ WriteAttribute<T>(clusterId, attributeName, attributeId, mAttributeParser, commandType, credsIssuerConfig)
+ {}
+
+private:
+ TypedComplexArgument<T> mAttributeParser;
+};
diff --git a/examples/fabric-admin/commands/common/CHIPCommand.cpp b/examples/fabric-admin/commands/common/CHIPCommand.cpp
new file mode 100644
index 0000000..982c857
--- /dev/null
+++ b/examples/fabric-admin/commands/common/CHIPCommand.cpp
@@ -0,0 +1,651 @@
+/*
+ * 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 "CHIPCommand.h"
+
+#include <controller/CHIPDeviceControllerFactory.h>
+#include <credentials/attestation_verifier/FileAttestationTrustStore.h>
+#include <lib/core/CHIPConfig.h>
+#include <lib/core/CHIPVendorIdentifiers.hpp>
+#include <lib/support/CodeUtils.h>
+#include <lib/support/ScopedBuffer.h>
+#include <lib/support/TestGroupData.h>
+#include <platform/LockTracker.h>
+
+#include <string>
+
+#if CHIP_CONFIG_TRANSPORT_TRACE_ENABLED
+#include "TraceDecoder.h"
+#include "TraceHandlers.h"
+#endif // CHIP_CONFIG_TRANSPORT_TRACE_ENABLED
+
+std::map<CHIPCommand::CommissionerIdentity, std::unique_ptr<chip::Controller::DeviceCommissioner>> CHIPCommand::mCommissioners;
+std::set<CHIPCommand *> CHIPCommand::sDeferredCleanups;
+
+using DeviceControllerFactory = chip::Controller::DeviceControllerFactory;
+
+constexpr chip::FabricId kIdentityNullFabricId = chip::kUndefinedFabricId;
+constexpr chip::FabricId kIdentityAlphaFabricId = 1;
+constexpr chip::FabricId kIdentityBetaFabricId = 2;
+constexpr chip::FabricId kIdentityGammaFabricId = 3;
+constexpr chip::FabricId kIdentityOtherFabricId = 4;
+constexpr char kPAATrustStorePathVariable[] = "FABRICSYNC_PAA_TRUST_STORE_PATH";
+constexpr char kCDTrustStorePathVariable[] = "FABRICSYNC_CD_TRUST_STORE_PATH";
+
+const chip::Credentials::AttestationTrustStore * CHIPCommand::sTrustStore = nullptr;
+chip::Credentials::GroupDataProviderImpl CHIPCommand::sGroupDataProvider{ kMaxGroupsPerFabric, kMaxGroupKeysPerFabric };
+// All fabrics share the same ICD client storage.
+chip::app::DefaultICDClientStorage CHIPCommand::sICDClientStorage;
+chip::Crypto::RawKeySessionKeystore CHIPCommand::sSessionKeystore;
+chip::app::DefaultCheckInDelegate CHIPCommand::sCheckInDelegate;
+chip::app::CheckInHandler CHIPCommand::sCheckInHandler;
+
+namespace {
+
+CHIP_ERROR GetAttestationTrustStore(const char * paaTrustStorePath, const chip::Credentials::AttestationTrustStore ** trustStore)
+{
+ if (paaTrustStorePath == nullptr)
+ {
+ paaTrustStorePath = getenv(kPAATrustStorePathVariable);
+ }
+
+ if (paaTrustStorePath == nullptr)
+ {
+ *trustStore = chip::Credentials::GetTestAttestationTrustStore();
+ return CHIP_NO_ERROR;
+ }
+
+ static chip::Credentials::FileAttestationTrustStore attestationTrustStore{ paaTrustStorePath };
+
+ if (paaTrustStorePath != nullptr && attestationTrustStore.paaCount() == 0)
+ {
+ ChipLogError(NotSpecified, "No PAAs found in path: %s", paaTrustStorePath);
+ ChipLogError(NotSpecified,
+ "Please specify a valid path containing trusted PAA certificates using "
+ "the argument [--paa-trust-store-path paa/file/path] "
+ "or environment variable [%s=paa/file/path]",
+ kPAATrustStorePathVariable);
+ return CHIP_ERROR_INVALID_ARGUMENT;
+ }
+
+ *trustStore = &attestationTrustStore;
+ return CHIP_NO_ERROR;
+}
+
+} // namespace
+
+CHIP_ERROR CHIPCommand::MaybeSetUpStack()
+{
+ if (IsInteractive())
+ {
+ return CHIP_NO_ERROR;
+ }
+
+ StartTracing();
+
+#if (CHIP_DEVICE_LAYER_TARGET_LINUX || CHIP_DEVICE_LAYER_TARGET_TIZEN) && CHIP_DEVICE_CONFIG_ENABLE_CHIPOBLE
+ // By default, Linux device is configured as a BLE peripheral while the controller needs a BLE central.
+ ReturnLogErrorOnFailure(chip::DeviceLayer::Internal::BLEMgrImpl().ConfigureBle(mBleAdapterId.ValueOr(0), true));
+#endif
+
+ ReturnLogErrorOnFailure(mDefaultStorage.Init(nullptr, GetStorageDirectory().ValueOr(nullptr)));
+ ReturnLogErrorOnFailure(mOperationalKeystore.Init(&mDefaultStorage));
+ ReturnLogErrorOnFailure(mOpCertStore.Init(&mDefaultStorage));
+
+ // fabric-admin uses a non-persistent keystore.
+ // ICD storage lifetime is currently tied to the fabric-admin's lifetime. Since fabric-admin interactive mode is currently used
+ // for ICD commissioning and check-in validation, this temporary storage meets the test requirements.
+ // TODO: Implement persistent ICD storage for the fabric-admin.
+ ReturnLogErrorOnFailure(sICDClientStorage.Init(&mDefaultStorage, &sSessionKeystore));
+
+ chip::Controller::FactoryInitParams factoryInitParams;
+
+ factoryInitParams.fabricIndependentStorage = &mDefaultStorage;
+ factoryInitParams.operationalKeystore = &mOperationalKeystore;
+ factoryInitParams.opCertStore = &mOpCertStore;
+ factoryInitParams.enableServerInteractions = NeedsOperationalAdvertising();
+ factoryInitParams.sessionKeystore = &sSessionKeystore;
+
+ // Init group data provider that will be used for all group keys and IPKs for the
+ // fabric-admin-configured fabrics. This is OK to do once since the fabric tables
+ // and the DeviceControllerFactory all "share" in the same underlying data.
+ // Different commissioner implementations may want to use alternate implementations
+ // of GroupDataProvider for injection through factoryInitParams.
+ sGroupDataProvider.SetStorageDelegate(&mDefaultStorage);
+ sGroupDataProvider.SetSessionKeystore(factoryInitParams.sessionKeystore);
+ ReturnLogErrorOnFailure(sGroupDataProvider.Init());
+ chip::Credentials::SetGroupDataProvider(&sGroupDataProvider);
+ factoryInitParams.groupDataProvider = &sGroupDataProvider;
+
+ uint16_t port = mDefaultStorage.GetListenPort();
+ if (port != 0)
+ {
+ // Make sure different commissioners run on different ports.
+ port = static_cast<uint16_t>(port + CurrentCommissionerId());
+ }
+ factoryInitParams.listenPort = port;
+ ReturnLogErrorOnFailure(DeviceControllerFactory::GetInstance().Init(factoryInitParams));
+
+ auto systemState = chip::Controller::DeviceControllerFactory::GetInstance().GetSystemState();
+ VerifyOrReturnError(nullptr != systemState, CHIP_ERROR_INCORRECT_STATE);
+
+ ReturnErrorOnFailure(GetAttestationTrustStore(mPaaTrustStorePath.ValueOr(nullptr), &sTrustStore));
+
+ auto engine = chip::app::InteractionModelEngine::GetInstance();
+ VerifyOrReturnError(engine != nullptr, CHIP_ERROR_INCORRECT_STATE);
+ ReturnLogErrorOnFailure(sCheckInDelegate.Init(&sICDClientStorage, engine));
+ ReturnLogErrorOnFailure(sCheckInHandler.Init(DeviceControllerFactory::GetInstance().GetSystemState()->ExchangeMgr(),
+ &sICDClientStorage, &sCheckInDelegate, engine));
+
+ CommissionerIdentity nullIdentity{ kIdentityNull, chip::kUndefinedNodeId };
+ ReturnLogErrorOnFailure(InitializeCommissioner(nullIdentity, kIdentityNullFabricId));
+
+ // After initializing first commissioner, add the additional CD certs once
+ {
+ const char * cdTrustStorePath = mCDTrustStorePath.ValueOr(nullptr);
+ if (cdTrustStorePath == nullptr)
+ {
+ cdTrustStorePath = getenv(kCDTrustStorePathVariable);
+ }
+
+ auto additionalCdCerts =
+ chip::Credentials::LoadAllX509DerCerts(cdTrustStorePath, chip::Credentials::CertificateValidationMode::kPublicKeyOnly);
+ if (cdTrustStorePath != nullptr && additionalCdCerts.size() == 0)
+ {
+ ChipLogError(NotSpecified, "Warning: no CD signing certs found in path: %s, only defaults will be used",
+ cdTrustStorePath);
+ ChipLogError(NotSpecified,
+ "Please specify a path containing trusted CD verifying key certificates using "
+ "the argument [--cd-trust-store-path cd/file/path] "
+ "or environment variable [%s=cd/file/path]",
+ kCDTrustStorePathVariable);
+ }
+ ReturnErrorOnFailure(mCredIssuerCmds->AddAdditionalCDVerifyingCerts(additionalCdCerts));
+ }
+ bool allowTestCdSigningKey = !mOnlyAllowTrustedCdKeys.ValueOr(false);
+ mCredIssuerCmds->SetCredentialIssuerOption(CredentialIssuerCommands::CredentialIssuerOptions::kAllowTestCdSigningKey,
+ allowTestCdSigningKey);
+
+ return CHIP_NO_ERROR;
+}
+
+void CHIPCommand::MaybeTearDownStack()
+{
+ if (IsInteractive())
+ {
+ return;
+ }
+
+ //
+ // We can call DeviceController::Shutdown() safely without grabbing the stack lock
+ // since the CHIP thread and event queue have been stopped, preventing any thread
+ // races.
+ //
+ for (auto & commissioner : mCommissioners)
+ {
+ ShutdownCommissioner(commissioner.first);
+ }
+
+ StopTracing();
+}
+
+CHIP_ERROR CHIPCommand::EnsureCommissionerForIdentity(std::string identity)
+{
+ chip::NodeId nodeId;
+ ReturnErrorOnFailure(GetIdentityNodeId(identity, &nodeId));
+ CommissionerIdentity lookupKey{ identity, nodeId };
+ if (mCommissioners.find(lookupKey) != mCommissioners.end())
+ {
+ return CHIP_NO_ERROR;
+ }
+
+ // Need to initialize the commissioner.
+ chip::FabricId fabricId;
+ if (identity == kIdentityAlpha)
+ {
+ fabricId = kIdentityAlphaFabricId;
+ }
+ else if (identity == kIdentityBeta)
+ {
+ fabricId = kIdentityBetaFabricId;
+ }
+ else if (identity == kIdentityGamma)
+ {
+ fabricId = kIdentityGammaFabricId;
+ }
+ else
+ {
+ fabricId = strtoull(identity.c_str(), nullptr, 0);
+ if (fabricId < kIdentityOtherFabricId)
+ {
+ ChipLogError(NotSpecified, "Invalid identity: %s", identity.c_str());
+ return CHIP_ERROR_INVALID_ARGUMENT;
+ }
+ }
+
+ return InitializeCommissioner(lookupKey, fabricId);
+}
+
+CHIP_ERROR CHIPCommand::Run()
+{
+ ReturnErrorOnFailure(MaybeSetUpStack());
+
+ CHIP_ERROR err = StartWaiting(GetWaitDuration());
+
+ if (IsInteractive())
+ {
+ bool timedOut;
+ // Give it 2 hours to run our cleanup; that should never get hit in practice.
+ CHIP_ERROR cleanupErr = RunOnMatterQueue(RunCommandCleanup, chip::System::Clock::Seconds16(7200), &timedOut);
+ VerifyOrDie(cleanupErr == CHIP_NO_ERROR);
+ VerifyOrDie(!timedOut);
+ }
+ else
+ {
+ CleanupAfterRun();
+ }
+
+ MaybeTearDownStack();
+
+ return err;
+}
+
+void CHIPCommand::StartTracing()
+{
+ if (mTraceTo.HasValue())
+ {
+ for (const auto & destination : mTraceTo.Value())
+ {
+ mTracingSetup.EnableTracingFor(destination.c_str());
+ }
+ }
+
+#if CHIP_CONFIG_TRANSPORT_TRACE_ENABLED
+ chip::trace::InitTrace();
+
+ if (mTraceFile.HasValue())
+ {
+ chip::trace::AddTraceStream(new chip::trace::TraceStreamFile(mTraceFile.Value()));
+ }
+ else if (mTraceLog.HasValue() && mTraceLog.Value())
+ {
+ chip::trace::AddTraceStream(new chip::trace::TraceStreamLog());
+ }
+
+ if (mTraceDecode.HasValue() && mTraceDecode.Value())
+ {
+ chip::trace::TraceDecoderOptions options;
+ // The interaction model protocol is already logged, so just disable logging those.
+ options.mEnableProtocolInteractionModelResponse = false;
+ chip::trace::TraceDecoder * decoder = new chip::trace::TraceDecoder();
+ decoder->SetOptions(options);
+ chip::trace::AddTraceStream(decoder);
+ }
+#endif // CHIP_CONFIG_TRANSPORT_TRACE_ENABLED
+}
+
+void CHIPCommand::StopTracing()
+{
+ mTracingSetup.StopTracing();
+
+#if CHIP_CONFIG_TRANSPORT_TRACE_ENABLED
+ chip::trace::DeInitTrace();
+#endif // CHIP_CONFIG_TRANSPORT_TRACE_ENABLED
+}
+
+void CHIPCommand::SetIdentity(const char * identity)
+{
+ std::string name = std::string(identity);
+ if (name.compare(kIdentityAlpha) != 0 && name.compare(kIdentityBeta) != 0 && name.compare(kIdentityGamma) != 0 &&
+ name.compare(kIdentityNull) != 0 && strtoull(name.c_str(), nullptr, 0) < kIdentityOtherFabricId)
+ {
+ ChipLogError(NotSpecified, "Unknown commissioner name: %s. Supported names are [%s, %s, %s, 4, 5...]", name.c_str(),
+ kIdentityAlpha, kIdentityBeta, kIdentityGamma);
+ chipDie();
+ }
+
+ mCommissionerName.SetValue(const_cast<char *>(identity));
+}
+
+std::string CHIPCommand::GetIdentity()
+{
+ std::string name = mCommissionerName.HasValue() ? mCommissionerName.Value() : kIdentityAlpha;
+ if (name.compare(kIdentityAlpha) != 0 && name.compare(kIdentityBeta) != 0 && name.compare(kIdentityGamma) != 0 &&
+ name.compare(kIdentityNull) != 0)
+ {
+ chip::FabricId fabricId = strtoull(name.c_str(), nullptr, 0);
+ if (fabricId >= kIdentityOtherFabricId)
+ {
+ // normalize name since it is used in persistent storage
+
+ char s[24];
+ sprintf(s, "%lx", fabricId);
+
+ name = s;
+ }
+ else
+ {
+ ChipLogError(NotSpecified, "Unknown commissioner name: %s. Supported names are [%s, %s, %s, 4, 5...]", name.c_str(),
+ kIdentityAlpha, kIdentityBeta, kIdentityGamma);
+ chipDie();
+ }
+ }
+
+ return name;
+}
+
+CHIP_ERROR CHIPCommand::GetIdentityNodeId(std::string identity, chip::NodeId * nodeId)
+{
+ if (mCommissionerNodeId.HasValue())
+ {
+ *nodeId = mCommissionerNodeId.Value();
+ return CHIP_NO_ERROR;
+ }
+
+ if (identity == kIdentityNull)
+ {
+ *nodeId = chip::kUndefinedNodeId;
+ return CHIP_NO_ERROR;
+ }
+
+ ReturnLogErrorOnFailure(mCommissionerStorage.Init(identity.c_str(), GetStorageDirectory().ValueOr(nullptr)));
+
+ *nodeId = mCommissionerStorage.GetLocalNodeId();
+
+ return CHIP_NO_ERROR;
+}
+
+CHIP_ERROR CHIPCommand::GetIdentityRootCertificate(std::string identity, chip::ByteSpan & span)
+{
+ if (identity == kIdentityNull)
+ {
+ return CHIP_ERROR_NOT_FOUND;
+ }
+
+ chip::NodeId nodeId;
+ VerifyOrDie(GetIdentityNodeId(identity, &nodeId) == CHIP_NO_ERROR);
+ CommissionerIdentity lookupKey{ identity, nodeId };
+ auto item = mCommissioners.find(lookupKey);
+
+ span = chip::ByteSpan(item->first.mRCAC, item->first.mRCACLen);
+ return CHIP_NO_ERROR;
+}
+
+chip::FabricId CHIPCommand::CurrentCommissionerId()
+{
+ chip::FabricId id;
+
+ std::string name = GetIdentity();
+ if (name.compare(kIdentityAlpha) == 0)
+ {
+ id = kIdentityAlphaFabricId;
+ }
+ else if (name.compare(kIdentityBeta) == 0)
+ {
+ id = kIdentityBetaFabricId;
+ }
+ else if (name.compare(kIdentityGamma) == 0)
+ {
+ id = kIdentityGammaFabricId;
+ }
+ else if (name.compare(kIdentityNull) == 0)
+ {
+ id = kIdentityNullFabricId;
+ }
+ else if ((id = strtoull(name.c_str(), nullptr, 0)) < kIdentityOtherFabricId)
+ {
+ VerifyOrDieWithMsg(false, NotSpecified, "Unknown commissioner name: %s. Supported names are [%s, %s, %s, 4, 5...]",
+ name.c_str(), kIdentityAlpha, kIdentityBeta, kIdentityGamma);
+ }
+
+ return id;
+}
+
+chip::Controller::DeviceCommissioner & CHIPCommand::CurrentCommissioner()
+{
+ return GetCommissioner(GetIdentity());
+}
+
+chip::Controller::DeviceCommissioner & CHIPCommand::GetCommissioner(std::string identity)
+{
+ // We don't have a great way to handle commissioner setup failures here.
+ // This only matters for commands (like TestCommand) that involve multiple
+ // identities.
+ VerifyOrDie(EnsureCommissionerForIdentity(identity) == CHIP_NO_ERROR);
+
+ chip::NodeId nodeId;
+ VerifyOrDie(GetIdentityNodeId(identity, &nodeId) == CHIP_NO_ERROR);
+ CommissionerIdentity lookupKey{ identity, nodeId };
+ auto item = mCommissioners.find(lookupKey);
+ VerifyOrDie(item != mCommissioners.end());
+ return *item->second;
+}
+
+void CHIPCommand::ShutdownCommissioner(const CommissionerIdentity & key)
+{
+ mCommissioners[key].get()->Shutdown();
+}
+
+CHIP_ERROR CHIPCommand::InitializeCommissioner(CommissionerIdentity & identity, chip::FabricId fabricId)
+{
+ std::unique_ptr<ChipDeviceCommissioner> commissioner = std::make_unique<ChipDeviceCommissioner>();
+ chip::Controller::SetupParams commissionerParams;
+
+ ReturnLogErrorOnFailure(mCredIssuerCmds->SetupDeviceAttestation(commissionerParams, sTrustStore));
+
+ chip::Crypto::P256Keypair ephemeralKey;
+
+ if (fabricId != chip::kUndefinedFabricId)
+ {
+
+ // TODO - OpCreds should only be generated for pairing command
+ // store the credentials in persistent storage, and
+ // generate when not available in the storage.
+ ReturnLogErrorOnFailure(mCommissionerStorage.Init(identity.mName.c_str(), GetStorageDirectory().ValueOr(nullptr)));
+ if (mUseMaxSizedCerts.HasValue())
+ {
+ auto option = CredentialIssuerCommands::CredentialIssuerOptions::kMaximizeCertificateSizes;
+ mCredIssuerCmds->SetCredentialIssuerOption(option, mUseMaxSizedCerts.Value());
+ }
+
+ ReturnLogErrorOnFailure(mCredIssuerCmds->InitializeCredentialsIssuer(mCommissionerStorage));
+
+ chip::MutableByteSpan nocSpan(identity.mNOC);
+ chip::MutableByteSpan icacSpan(identity.mICAC);
+ chip::MutableByteSpan rcacSpan(identity.mRCAC);
+
+ ReturnLogErrorOnFailure(ephemeralKey.Initialize(chip::Crypto::ECPKeyTarget::ECDSA));
+
+ ReturnLogErrorOnFailure(mCredIssuerCmds->GenerateControllerNOCChain(identity.mLocalNodeId, fabricId,
+ mCommissionerStorage.GetCommissionerCATs(),
+ ephemeralKey, rcacSpan, icacSpan, nocSpan));
+
+ identity.mRCACLen = rcacSpan.size();
+ identity.mICACLen = icacSpan.size();
+ identity.mNOCLen = nocSpan.size();
+
+ commissionerParams.operationalKeypair = &ephemeralKey;
+ commissionerParams.controllerRCAC = rcacSpan;
+ commissionerParams.controllerICAC = icacSpan;
+ commissionerParams.controllerNOC = nocSpan;
+ commissionerParams.permitMultiControllerFabrics = true;
+ commissionerParams.enableServerInteractions = NeedsOperationalAdvertising();
+ }
+
+ // TODO: Initialize IPK epoch key in ExampleOperationalCredentials issuer rather than relying on DefaultIpkValue
+ commissionerParams.operationalCredentialsDelegate = mCredIssuerCmds->GetCredentialIssuer();
+ commissionerParams.controllerVendorId = mCommissionerVendorId.ValueOr(chip::VendorId::TestVendor1);
+
+ ReturnLogErrorOnFailure(DeviceControllerFactory::GetInstance().SetupCommissioner(commissionerParams, *(commissioner.get())));
+
+ if (identity.mName != kIdentityNull)
+ {
+ // Initialize Group Data, including IPK
+ chip::FabricIndex fabricIndex = commissioner->GetFabricIndex();
+ uint8_t compressed_fabric_id[sizeof(uint64_t)];
+ chip::MutableByteSpan compressed_fabric_id_span(compressed_fabric_id);
+ ReturnLogErrorOnFailure(commissioner->GetCompressedFabricIdBytes(compressed_fabric_id_span));
+
+ ReturnLogErrorOnFailure(chip::GroupTesting::InitData(&sGroupDataProvider, fabricIndex, compressed_fabric_id_span));
+
+ // Configure the default IPK for all fabrics used by CHIP-tool. The epoch
+ // key is the same, but the derived keys will be different for each fabric.
+ chip::ByteSpan defaultIpk = chip::GroupTesting::DefaultIpkValue::GetDefaultIpk();
+ ReturnLogErrorOnFailure(
+ chip::Credentials::SetSingleIpkEpochKey(&sGroupDataProvider, fabricIndex, defaultIpk, compressed_fabric_id_span));
+ }
+
+ CHIPCommand::sICDClientStorage.UpdateFabricList(commissioner->GetFabricIndex());
+
+ mCommissioners[identity] = std::move(commissioner);
+
+ return CHIP_NO_ERROR;
+}
+
+void CHIPCommand::RunQueuedCommand(intptr_t commandArg)
+{
+ auto * command = reinterpret_cast<CHIPCommand *>(commandArg);
+ CHIP_ERROR err = command->EnsureCommissionerForIdentity(command->GetIdentity());
+ if (err == CHIP_NO_ERROR)
+ {
+ err = command->RunCommand();
+ }
+
+ if (err != CHIP_NO_ERROR)
+ {
+ command->SetCommandExitStatus(err);
+ }
+}
+
+void CHIPCommand::RunCommandCleanup(intptr_t commandArg)
+{
+ auto * command = reinterpret_cast<CHIPCommand *>(commandArg);
+ command->CleanupAfterRun();
+ command->StopWaiting();
+}
+
+void CHIPCommand::CleanupAfterRun()
+{
+ assertChipStackLockedByCurrentThread();
+ bool deferCleanup = (IsInteractive() && DeferInteractiveCleanup());
+
+ Shutdown();
+
+ if (deferCleanup)
+ {
+ sDeferredCleanups.insert(this);
+ }
+ else
+ {
+ Cleanup();
+ }
+}
+
+CHIP_ERROR CHIPCommand::RunOnMatterQueue(MatterWorkCallback callback, chip::System::Clock::Timeout timeout, bool * timedOut)
+{
+ {
+ std::lock_guard<std::mutex> lk(cvWaitingForResponseMutex);
+ mWaitingForResponse = true;
+ }
+
+ auto err = chip::DeviceLayer::PlatformMgr().ScheduleWork(callback, reinterpret_cast<intptr_t>(this));
+ if (CHIP_NO_ERROR != err)
+ {
+ {
+ std::lock_guard<std::mutex> lk(cvWaitingForResponseMutex);
+ mWaitingForResponse = false;
+ }
+ return err;
+ }
+
+ auto waitingUntil = std::chrono::system_clock::now() + std::chrono::duration_cast<std::chrono::seconds>(timeout);
+ {
+ std::unique_lock<std::mutex> lk(cvWaitingForResponseMutex);
+ *timedOut = !cvWaitingForResponse.wait_until(lk, waitingUntil, [this]() { return !this->mWaitingForResponse; });
+ }
+
+ return CHIP_NO_ERROR;
+}
+
+#if !CONFIG_USE_SEPARATE_EVENTLOOP
+static void OnResponseTimeout(chip::System::Layer *, void * appState)
+{
+ (reinterpret_cast<CHIPCommand *>(appState))->SetCommandExitStatus(CHIP_ERROR_TIMEOUT);
+}
+#endif // !CONFIG_USE_SEPARATE_EVENTLOOP
+
+CHIP_ERROR CHIPCommand::StartWaiting(chip::System::Clock::Timeout duration)
+{
+#if CONFIG_USE_SEPARATE_EVENTLOOP
+ // ServiceEvents() calls StartEventLoopTask(), which is paired with the StopEventLoopTask() below.
+ if (!IsInteractive())
+ {
+ ReturnLogErrorOnFailure(DeviceControllerFactory::GetInstance().ServiceEvents());
+ }
+
+ if (duration.count() == 0)
+ {
+ mCommandExitStatus = RunCommand();
+ }
+ else
+ {
+ bool timedOut;
+ CHIP_ERROR err = RunOnMatterQueue(RunQueuedCommand, duration, &timedOut);
+ if (CHIP_NO_ERROR != err)
+ {
+ return err;
+ }
+ if (timedOut)
+ {
+ mCommandExitStatus = CHIP_ERROR_TIMEOUT;
+ }
+ }
+ if (!IsInteractive())
+ {
+ LogErrorOnFailure(chip::DeviceLayer::PlatformMgr().StopEventLoopTask());
+ }
+#else
+ chip::DeviceLayer::PlatformMgr().ScheduleWork(RunQueuedCommand, reinterpret_cast<intptr_t>(this));
+ ReturnLogErrorOnFailure(chip::DeviceLayer::SystemLayer().StartTimer(duration, OnResponseTimeout, this));
+ chip::DeviceLayer::PlatformMgr().RunEventLoop();
+#endif // CONFIG_USE_SEPARATE_EVENTLOOP
+
+ return mCommandExitStatus;
+}
+
+void CHIPCommand::StopWaiting()
+{
+#if CONFIG_USE_SEPARATE_EVENTLOOP
+ {
+ std::lock_guard<std::mutex> lk(cvWaitingForResponseMutex);
+ mWaitingForResponse = false;
+ }
+ cvWaitingForResponse.notify_all();
+#else // CONFIG_USE_SEPARATE_EVENTLOOP
+ LogErrorOnFailure(chip::DeviceLayer::PlatformMgr().StopEventLoopTask());
+#endif // CONFIG_USE_SEPARATE_EVENTLOOP
+}
+
+void CHIPCommand::ExecuteDeferredCleanups(intptr_t ignored)
+{
+ for (auto * cmd : sDeferredCleanups)
+ {
+ cmd->Cleanup();
+ }
+ sDeferredCleanups.clear();
+}
diff --git a/examples/fabric-admin/commands/common/CHIPCommand.h b/examples/fabric-admin/commands/common/CHIPCommand.h
new file mode 100644
index 0000000..856a4da
--- /dev/null
+++ b/examples/fabric-admin/commands/common/CHIPCommand.h
@@ -0,0 +1,264 @@
+/*
+ * 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
+
+#ifdef CONFIG_USE_LOCAL_STORAGE
+#include <controller/ExamplePersistentStorage.h>
+#endif // CONFIG_USE_LOCAL_STORAGE
+
+#include "Command.h"
+
+#include <TracingCommandLineArgument.h>
+#include <app/icd/client/CheckInHandler.h>
+#include <app/icd/client/DefaultCheckInDelegate.h>
+#include <app/icd/client/DefaultICDClientStorage.h>
+#include <commands/common/CredentialIssuerCommands.h>
+#include <commands/example/ExampleCredentialIssuerCommands.h>
+#include <credentials/GroupDataProviderImpl.h>
+#include <credentials/PersistentStorageOpCertStore.h>
+#include <crypto/PersistentStorageOperationalKeystore.h>
+#include <crypto/RawKeySessionKeystore.h>
+
+#include <string>
+
+inline constexpr char kIdentityAlpha[] = "alpha";
+inline constexpr char kIdentityBeta[] = "beta";
+inline constexpr char kIdentityGamma[] = "gamma";
+// The null fabric commissioner is a commissioner that isn't on a fabric.
+// This is a legal configuration in which the commissioner delegates
+// operational communication and invocation of the commssioning complete
+// command to a separate on-fabric administrator node.
+//
+// The null-fabric-commissioner identity is provided here to demonstrate the
+// commissioner portion of such an architecture. The null-fabric-commissioner
+// can carry a commissioning flow up until the point of operational channel
+// (CASE) communcation.
+inline constexpr char kIdentityNull[] = "null-fabric-commissioner";
+
+class CHIPCommand : public Command
+{
+public:
+ using ChipDeviceCommissioner = ::chip::Controller::DeviceCommissioner;
+ using ChipDeviceController = ::chip::Controller::DeviceController;
+ using IPAddress = ::chip::Inet::IPAddress;
+ using NodeId = ::chip::NodeId;
+ using PeerId = ::chip::PeerId;
+ using PeerAddress = ::chip::Transport::PeerAddress;
+
+ static constexpr uint16_t kMaxGroupsPerFabric = 50;
+ static constexpr uint16_t kMaxGroupKeysPerFabric = 25;
+
+ CHIPCommand(const char * commandName, CredentialIssuerCommands * credIssuerCmds, const char * helpText = nullptr) :
+ Command(commandName, helpText), mCredIssuerCmds(credIssuerCmds)
+ {
+ AddArgument("paa-trust-store-path", &mPaaTrustStorePath,
+ "Path to directory holding PAA certificate information. Can be absolute or relative to the current working "
+ "directory.");
+ AddArgument("cd-trust-store-path", &mCDTrustStorePath,
+ "Path to directory holding CD certificate information. Can be absolute or relative to the current working "
+ "directory.");
+ AddArgument("commissioner-name", &mCommissionerName,
+ "Name of fabric to use. Valid values are \"alpha\", \"beta\", \"gamma\", and integers greater than or equal to "
+ "4. The default if not specified is \"alpha\".");
+ AddArgument("commissioner-nodeid", 0, UINT64_MAX, &mCommissionerNodeId,
+ "The node id to use for fabric-admin. If not provided, kTestControllerNodeId (112233, 0x1B669) will be used.");
+ AddArgument("use-max-sized-certs", 0, 1, &mUseMaxSizedCerts,
+ "Maximize the size of operational certificates. If not provided or 0 (\"false\"), normally sized operational "
+ "certificates are generated.");
+ AddArgument("only-allow-trusted-cd-keys", 0, 1, &mOnlyAllowTrustedCdKeys,
+ "Only allow trusted CD verifying keys (disallow test keys). If not provided or 0 (\"false\"), untrusted CD "
+ "verifying keys are allowed. If 1 (\"true\"), test keys are disallowed.");
+#if CHIP_CONFIG_TRANSPORT_TRACE_ENABLED
+ AddArgument("trace_file", &mTraceFile);
+ AddArgument("trace_log", 0, 1, &mTraceLog);
+ AddArgument("trace_decode", 0, 1, &mTraceDecode);
+#endif // CHIP_CONFIG_TRANSPORT_TRACE_ENABLED
+ AddArgument("trace-to", &mTraceTo, "Trace destinations, comma-separated (" SUPPORTED_COMMAND_LINE_TRACING_TARGETS ")");
+ AddArgument("ble-adapter", 0, UINT16_MAX, &mBleAdapterId);
+ AddArgument("storage-directory", &mStorageDirectory,
+ "Directory to place fabric-admin's storage files in. Defaults to $TMPDIR, with fallback to /tmp");
+ AddArgument(
+ "commissioner-vendor-id", 0, UINT16_MAX, &mCommissionerVendorId,
+ "The vendor id to use for fabric-admin. If not provided, chip::VendorId::TestVendor1 (65521, 0xFFF1) will be used.");
+ }
+
+ /////////// Command Interface /////////
+ CHIP_ERROR Run() override;
+
+ void SetCommandExitStatus(CHIP_ERROR status)
+ {
+ mCommandExitStatus = status;
+ // In interactive mode the stack is not shut down once a command is ended.
+ // That means calling `ErrorStr(err)` from the main thread when command
+ // completion is signaled may race since `ErrorStr` uses a static sErrorStr
+ // buffer for computing the error string. Call it here instead.
+ if (IsInteractive() && CHIP_NO_ERROR != status)
+ {
+ ChipLogError(NotSpecified, "Run command failure: %s", chip::ErrorStr(status));
+ }
+ StopWaiting();
+ }
+
+protected:
+ // Will be called in a setting in which it's safe to touch the CHIP
+ // stack. The rules for Run() are as follows:
+ //
+ // 1) If error is returned, Run() must not call SetCommandExitStatus.
+ // 2) If success is returned Run() must either have called
+ // SetCommandExitStatus() or scheduled async work that will do that.
+ virtual CHIP_ERROR RunCommand() = 0;
+
+ // Get the wait duration, in seconds, before the command times out.
+ virtual chip::System::Clock::Timeout GetWaitDuration() const = 0;
+
+ // Shut down the command. After a Shutdown call the command object is ready
+ // to be used for another command invocation.
+ virtual void Shutdown() { ResetArguments(); }
+
+ // Clean up any resources allocated by the command. Some commands may hold
+ // on to resources after Shutdown(), but Cleanup() will guarantee those are
+ // cleaned up.
+ virtual void Cleanup() {}
+
+ // If true, skip calling Cleanup() when in interactive mode, so the command
+ // can keep doing work as needed. Cleanup() will be called when quitting
+ // interactive mode. This method will be called before Shutdown, so it can
+ // use member values that Shutdown will normally reset.
+ virtual bool DeferInteractiveCleanup() { return false; }
+
+ // If true, the controller will be created with server capabilities enabled,
+ // such as advertising operational nodes over DNS-SD and accepting incoming
+ // CASE sessions.
+ virtual bool NeedsOperationalAdvertising() { return mAdvertiseOperational; }
+
+ // Execute any deferred cleanups. Used when exiting interactive mode.
+ static void ExecuteDeferredCleanups(intptr_t ignored);
+
+#ifdef CONFIG_USE_LOCAL_STORAGE
+ PersistentStorage mDefaultStorage;
+ // TODO: It's pretty weird that we re-init mCommissionerStorage for every
+ // identity without shutting it down or something in between...
+ PersistentStorage mCommissionerStorage;
+#endif // CONFIG_USE_LOCAL_STORAGE
+ chip::PersistentStorageOperationalKeystore mOperationalKeystore;
+ chip::Credentials::PersistentStorageOpCertStore mOpCertStore;
+ static chip::Crypto::RawKeySessionKeystore sSessionKeystore;
+
+ static chip::Credentials::GroupDataProviderImpl sGroupDataProvider;
+ static chip::app::DefaultICDClientStorage sICDClientStorage;
+ static chip::app::DefaultCheckInDelegate sCheckInDelegate;
+ static chip::app::CheckInHandler sCheckInHandler;
+ CredentialIssuerCommands * mCredIssuerCmds;
+
+ std::string GetIdentity();
+ CHIP_ERROR GetIdentityNodeId(std::string identity, chip::NodeId * nodeId);
+ CHIP_ERROR GetIdentityRootCertificate(std::string identity, chip::ByteSpan & span);
+ void SetIdentity(const char * name);
+
+ // This method returns the commissioner instance to be used for running the command.
+ // The default commissioner instance name is "alpha", but it can be overridden by passing
+ // --identity "instance name" when running a command.
+ ChipDeviceCommissioner & CurrentCommissioner();
+
+ ChipDeviceCommissioner & GetCommissioner(std::string identity);
+
+private:
+ CHIP_ERROR MaybeSetUpStack();
+ void MaybeTearDownStack();
+
+ CHIP_ERROR EnsureCommissionerForIdentity(std::string identity);
+
+ // Commissioners are keyed by name and local node id.
+ struct CommissionerIdentity
+ {
+ bool operator<(const CommissionerIdentity & other) const
+ {
+ return mName < other.mName || (mName == other.mName && mLocalNodeId < other.mLocalNodeId);
+ }
+ std::string mName;
+ chip::NodeId mLocalNodeId;
+ uint8_t mRCAC[chip::Controller::kMaxCHIPDERCertLength] = {};
+ uint8_t mICAC[chip::Controller::kMaxCHIPDERCertLength] = {};
+ uint8_t mNOC[chip::Controller::kMaxCHIPDERCertLength] = {};
+
+ size_t mRCACLen;
+ size_t mICACLen;
+ size_t mNOCLen;
+ };
+
+ // InitializeCommissioner uses various members, so can't be static. This is
+ // obviously a little odd, since the commissioners are then shared across
+ // multiple commands in interactive mode...
+ CHIP_ERROR InitializeCommissioner(CommissionerIdentity & identity, chip::FabricId fabricId);
+ void ShutdownCommissioner(const CommissionerIdentity & key);
+ chip::FabricId CurrentCommissionerId();
+
+ static std::map<CommissionerIdentity, std::unique_ptr<ChipDeviceCommissioner>> mCommissioners;
+ static std::set<CHIPCommand *> sDeferredCleanups;
+
+ chip::Optional<char *> mCommissionerName;
+ chip::Optional<chip::NodeId> mCommissionerNodeId;
+ chip::Optional<chip::VendorId> mCommissionerVendorId;
+ chip::Optional<uint16_t> mBleAdapterId;
+ chip::Optional<char *> mPaaTrustStorePath;
+ chip::Optional<char *> mCDTrustStorePath;
+ chip::Optional<bool> mUseMaxSizedCerts;
+ chip::Optional<bool> mOnlyAllowTrustedCdKeys;
+
+ // Cached trust store so commands other than the original startup command
+ // can spin up commissioners as needed.
+ static const chip::Credentials::AttestationTrustStore * sTrustStore;
+
+ static void RunQueuedCommand(intptr_t commandArg);
+ typedef decltype(RunQueuedCommand) MatterWorkCallback;
+ static void RunCommandCleanup(intptr_t commandArg);
+
+ // Do cleanup after a commmand is done running. Must happen with the
+ // Matter stack locked.
+ void CleanupAfterRun();
+
+ // Run the given callback on the Matter thread. Return whether we managed
+ // to successfully dispatch it to the Matter thread. If we did, *timedOut
+ // will be set to whether we timed out or whether our mWaitingForResponse
+ // got set to false by the callback itself.
+ CHIP_ERROR RunOnMatterQueue(MatterWorkCallback callback, chip::System::Clock::Timeout timeout, bool * timedOut);
+
+ CHIP_ERROR mCommandExitStatus = CHIP_ERROR_INTERNAL;
+
+ CHIP_ERROR StartWaiting(chip::System::Clock::Timeout seconds);
+ void StopWaiting();
+
+#if CONFIG_USE_SEPARATE_EVENTLOOP
+ std::condition_variable cvWaitingForResponse;
+ std::mutex cvWaitingForResponseMutex;
+ bool mWaitingForResponse{ true };
+#endif // CONFIG_USE_SEPARATE_EVENTLOOP
+
+ void StartTracing();
+ void StopTracing();
+
+#if CHIP_CONFIG_TRANSPORT_TRACE_ENABLED
+ chip::Optional<char *> mTraceFile;
+ chip::Optional<bool> mTraceLog;
+ chip::Optional<bool> mTraceDecode;
+#endif // CHIP_CONFIG_TRANSPORT_TRACE_ENABLED
+
+ chip::CommandLineApp::TracingSetup mTracingSetup;
+ chip::Optional<std::vector<std::string>> mTraceTo;
+};
diff --git a/examples/fabric-admin/commands/common/Command.cpp b/examples/fabric-admin/commands/common/Command.cpp
new file mode 100644
index 0000000..7e56891
--- /dev/null
+++ b/examples/fabric-admin/commands/common/Command.cpp
@@ -0,0 +1,1088 @@
+/*
+ * 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 "Command.h"
+#include "CustomStringPrefix.h"
+#include "HexConversion.h"
+#include "platform/PlatformManager.h"
+
+#include <functional>
+#include <netdb.h>
+#include <sstream>
+#include <string>
+#include <sys/socket.h>
+#include <sys/types.h>
+
+#include <math.h> // For INFINITY
+
+#include <lib/core/CHIPSafeCasts.h>
+#include <lib/support/BytesToHex.h>
+#include <lib/support/CHIPMem.h>
+#include <lib/support/CodeUtils.h>
+#include <lib/support/SafeInt.h>
+#include <lib/support/ScopedBuffer.h>
+#include <lib/support/StringSplitter.h>
+#include <lib/support/logging/CHIPLogging.h>
+
+constexpr char kOptionalArgumentPrefix[] = "--";
+constexpr size_t kOptionalArgumentPrefixLength = 2;
+
+bool Command::InitArguments(int argc, char ** argv)
+{
+ bool isValidCommand = false;
+
+ size_t argvExtraArgsCount = (size_t) argc;
+ size_t mandatoryArgsCount = 0;
+ size_t optionalArgsCount = 0;
+ for (auto & arg : mArgs)
+ {
+ if (arg.isOptional())
+ {
+ optionalArgsCount++;
+ }
+ else
+ {
+ mandatoryArgsCount++;
+ argvExtraArgsCount--;
+ }
+ }
+
+ VerifyOrExit((size_t) (argc) >= mandatoryArgsCount && (argvExtraArgsCount == 0 || (argvExtraArgsCount && optionalArgsCount)),
+ ChipLogError(NotSpecified, "InitArgs: Wrong arguments number: %d instead of %u", argc,
+ static_cast<unsigned int>(mandatoryArgsCount)));
+
+ // Initialize mandatory arguments
+ for (size_t i = 0; i < mandatoryArgsCount; i++)
+ {
+ char * arg = argv[i];
+ if (!InitArgument(i, arg))
+ {
+ ExitNow();
+ }
+ }
+
+ // Initialize optional arguments
+ // Optional arguments expect a name and a value, so i is increased by 2 on every step.
+ for (size_t i = mandatoryArgsCount; i < (size_t) argc; i += 2)
+ {
+ bool found = false;
+ for (size_t j = mandatoryArgsCount; j < mandatoryArgsCount + optionalArgsCount; j++)
+ {
+ // optional arguments starts with kOptionalArgumentPrefix
+ if (strlen(argv[i]) <= kOptionalArgumentPrefixLength &&
+ strncmp(argv[i], kOptionalArgumentPrefix, kOptionalArgumentPrefixLength) != 0)
+ {
+ continue;
+ }
+
+ if (strcmp(argv[i] + strlen(kOptionalArgumentPrefix), mArgs[j].name) == 0)
+ {
+ found = true;
+
+ VerifyOrExit((size_t) argc > (i + 1),
+ ChipLogError(NotSpecified, "InitArgs: Optional argument %s missing value.", argv[i]));
+ if (!InitArgument(j, argv[i + 1]))
+ {
+ ExitNow();
+ }
+ }
+ }
+ VerifyOrExit(found, ChipLogError(NotSpecified, "InitArgs: Optional argument %s does not exist.", argv[i]));
+ }
+
+ isValidCommand = true;
+
+exit:
+ return isValidCommand;
+}
+
+static bool ParseAddressWithInterface(const char * addressString, Command::AddressWithInterface * address)
+{
+ 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->address = ::chip::Inet::IPAddress::FromSockAddr(*addr);
+ address->interfaceId = ::chip::Inet::InterfaceId(addr->sin6_scope_id);
+ }
+#if INET_CONFIG_ENABLE_IPV4
+ else if (result->ai_family == AF_INET)
+ {
+ address->address = ::chip::Inet::IPAddress::FromSockAddr(*reinterpret_cast<struct sockaddr_in *>(result->ai_addr));
+ address->interfaceId = chip::Inet::InterfaceId::Null();
+ }
+#endif // INET_CONFIG_ENABLE_IPV4
+ else
+ {
+ ChipLogError(NotSpecified, "Unsupported address: %s", addressString);
+ return false;
+ }
+
+ return true;
+}
+
+// The callback should return whether the argument is valid, for the non-null
+// case. It can't directly write to isValidArgument (by closing over it)
+// because in the nullable-and-null case we need to do that from this function,
+// via the return value.
+template <typename T>
+bool HandleNullableOptional(Argument & arg, char * argValue, std::function<bool(T * value)> callback)
+{
+ if (arg.isOptional())
+ {
+ if (arg.isNullable())
+ {
+ arg.value = &(reinterpret_cast<chip::Optional<chip::app::DataModel::Nullable<T>> *>(arg.value)->Emplace());
+ }
+ else
+ {
+ arg.value = &(reinterpret_cast<chip::Optional<T> *>(arg.value)->Emplace());
+ }
+ }
+
+ if (arg.isNullable())
+ {
+ auto * nullable = reinterpret_cast<chip::app::DataModel::Nullable<T> *>(arg.value);
+ if (argValue != nullptr && strncmp(argValue, "null", 4) == 0)
+ {
+ nullable->SetNull();
+ return true;
+ }
+
+ arg.value = &(nullable->SetNonNull());
+ }
+
+ return callback(reinterpret_cast<T *>(arg.value));
+}
+
+bool Command::InitArgument(size_t argIndex, char * argValue)
+{
+ bool isValidArgument = false;
+ bool isHexNotation = strncmp(argValue, "0x", 2) == 0 || strncmp(argValue, "0X", 2) == 0;
+
+ Argument arg = mArgs.at(argIndex);
+
+ // We have two places where we handle uint8_t-typed args (actual int8u and
+ // bool args), so declare the handler function here so it can be reused.
+ auto uint8Handler = [&](uint8_t * value) {
+ // stringstream treats uint8_t as char, which is not what we want here.
+ uint16_t tmpValue;
+ std::stringstream ss;
+ isHexNotation ? (ss << std::hex << argValue) : (ss << argValue);
+ ss >> tmpValue;
+ if (chip::CanCastTo<uint8_t>(tmpValue))
+ {
+ *value = static_cast<uint8_t>(tmpValue);
+
+ uint64_t min = chip::CanCastTo<uint64_t>(arg.min) ? static_cast<uint64_t>(arg.min) : 0;
+ uint64_t max = arg.max;
+ return (!ss.fail() && ss.eof() && *value >= min && *value <= max);
+ }
+
+ return false;
+ };
+
+ switch (arg.type)
+ {
+ case ArgumentType::Complex: {
+ // Complex arguments may be optional, but they are not currently supported via the <chip::Optional> class.
+ // Instead, they must be explicitly specified as optional using the kOptional flag,
+ // and the base TypedComplexArgument<T> class is still referenced.
+ auto complexArgument = static_cast<ComplexArgument *>(arg.value);
+ return CHIP_NO_ERROR == complexArgument->Parse(arg.name, argValue);
+ }
+
+ case ArgumentType::Custom: {
+ auto customArgument = static_cast<CustomArgument *>(arg.value);
+ return CHIP_NO_ERROR == customArgument->Parse(arg.name, argValue);
+ }
+
+ case ArgumentType::VectorString: {
+ std::vector<std::string> vectorArgument;
+
+ chip::StringSplitter splitter(argValue, ',');
+ chip::CharSpan value;
+
+ while (splitter.Next(value))
+ {
+ vectorArgument.push_back(std::string(value.data(), value.size()));
+ }
+
+ if (arg.flags == Argument::kOptional)
+ {
+ auto argument = static_cast<chip::Optional<std::vector<std::string>> *>(arg.value);
+ argument->SetValue(vectorArgument);
+ }
+ else
+ {
+ auto argument = static_cast<std::vector<std::string> *>(arg.value);
+ *argument = vectorArgument;
+ }
+ return true;
+ }
+ case ArgumentType::VectorBool: {
+ // Currently only chip::Optional<std::vector<bool>> is supported.
+ if (arg.flags != Argument::kOptional)
+ {
+ return false;
+ }
+
+ std::vector<bool> vectorArgument;
+ std::stringstream ss(argValue);
+ while (ss.good())
+ {
+ std::string valueAsString;
+ getline(ss, valueAsString, ',');
+
+ if (strcasecmp(valueAsString.c_str(), "true") == 0)
+ {
+ vectorArgument.push_back(true);
+ }
+ else if (strcasecmp(valueAsString.c_str(), "false") == 0)
+ {
+ vectorArgument.push_back(false);
+ }
+ else
+ {
+ return false;
+ }
+ }
+
+ auto optionalArgument = static_cast<chip::Optional<std::vector<bool>> *>(arg.value);
+ optionalArgument->SetValue(vectorArgument);
+ return true;
+ }
+
+ case ArgumentType::Vector16:
+ case ArgumentType::Vector32: {
+ std::vector<uint64_t> values;
+ uint64_t min = chip::CanCastTo<uint64_t>(arg.min) ? static_cast<uint64_t>(arg.min) : 0;
+ uint64_t max = arg.max;
+
+ std::stringstream ss(argValue);
+ while (ss.good())
+ {
+ std::string valueAsString;
+ getline(ss, valueAsString, ',');
+ isHexNotation = strncmp(valueAsString.c_str(), "0x", 2) == 0 || strncmp(valueAsString.c_str(), "0X", 2) == 0;
+
+ std::stringstream subss;
+ isHexNotation ? subss << std::hex << valueAsString : subss << valueAsString;
+
+ uint64_t value;
+ subss >> value;
+ VerifyOrReturnError(!subss.fail() && subss.eof() && value >= min && value <= max, false);
+ values.push_back(value);
+ }
+
+ if (arg.type == ArgumentType::Vector16)
+ {
+ auto vectorArgument = static_cast<std::vector<uint16_t> *>(arg.value);
+ for (uint64_t v : values)
+ {
+ vectorArgument->push_back(static_cast<uint16_t>(v));
+ }
+ }
+ else if (arg.type == ArgumentType::Vector32 && arg.flags != Argument::kOptional)
+ {
+ auto vectorArgument = static_cast<std::vector<uint32_t> *>(arg.value);
+ for (uint64_t v : values)
+ {
+ vectorArgument->push_back(static_cast<uint32_t>(v));
+ }
+ }
+ else if (arg.type == ArgumentType::Vector32 && arg.flags == Argument::kOptional)
+ {
+ std::vector<uint32_t> vectorArgument;
+ for (uint64_t v : values)
+ {
+ vectorArgument.push_back(static_cast<uint32_t>(v));
+ }
+
+ auto optionalArgument = static_cast<chip::Optional<std::vector<uint32_t>> *>(arg.value);
+ optionalArgument->SetValue(vectorArgument);
+ }
+ else
+ {
+ return false;
+ }
+
+ return true;
+ }
+
+ case ArgumentType::VectorCustom: {
+ auto vectorArgument = static_cast<std::vector<CustomArgument *> *>(arg.value);
+
+ std::stringstream ss(argValue);
+ while (ss.good())
+ {
+ std::string valueAsString;
+ // By default the parameter separator is ";" in order to not collapse with the argument itself if it contains commas
+ // (e.g a struct argument with multiple fields). In case one needs to use ";" it can be overriden with the following
+ // environment variable.
+ static constexpr char kSeparatorVariable[] = "NotSpecified,_CUSTOM_ARGUMENTS_SEPARATOR";
+ char * getenvSeparatorVariableResult = getenv(kSeparatorVariable);
+ getline(ss, valueAsString, getenvSeparatorVariableResult ? getenvSeparatorVariableResult[0] : ';');
+
+ CustomArgument * customArgument = new CustomArgument();
+ vectorArgument->push_back(customArgument);
+ VerifyOrReturnError(CHIP_NO_ERROR == vectorArgument->back()->Parse(arg.name, valueAsString.c_str()), false);
+ }
+
+ return true;
+ }
+
+ case ArgumentType::String: {
+ isValidArgument = HandleNullableOptional<char *>(arg, argValue, [&](auto * value) {
+ *value = argValue;
+ return true;
+ });
+ break;
+ }
+
+ case ArgumentType::CharString: {
+ isValidArgument = HandleNullableOptional<chip::CharSpan>(arg, argValue, [&](auto * value) {
+ *value = chip::Span<const char>(argValue, strlen(argValue));
+ return true;
+ });
+ break;
+ }
+
+ case ArgumentType::OctetString: {
+ isValidArgument = HandleNullableOptional<chip::ByteSpan>(arg, argValue, [&](auto * value) {
+ // We support two ways to pass an octet string argument. If it happens
+ // to be all-ASCII, you can just pass it in. Otherwise you can pass in
+ // "hex:" followed by the hex-encoded bytes.
+ size_t argLen = strlen(argValue);
+
+ if (IsHexString(argValue))
+ {
+ // Hex-encoded. Decode it into a temporary buffer first, so if we
+ // run into errors we can do correct "argument is not valid" logging
+ // that actually shows the value that was passed in. After we
+ // determine it's valid, modify the passed-in value to hold the
+ // right bytes, so we don't need to worry about allocating storage
+ // for this somewhere else. This works because the hex
+ // representation is always longer than the octet string it encodes,
+ // so we have enough space in argValue for the decoded version.
+ chip::Platform::ScopedMemoryBuffer<uint8_t> buffer;
+
+ size_t octetCount;
+ CHIP_ERROR err = HexToBytes(
+ chip::CharSpan(argValue + kHexStringPrefixLen, argLen - kHexStringPrefixLen),
+ [&buffer](size_t allocSize) {
+ buffer.Calloc(allocSize);
+ return buffer.Get();
+ },
+ &octetCount);
+ if (err != CHIP_NO_ERROR)
+ {
+ return false;
+ }
+
+ memcpy(argValue, buffer.Get(), octetCount);
+ *value = chip::ByteSpan(chip::Uint8::from_char(argValue), octetCount);
+ return true;
+ }
+
+ // Just ASCII. Check for the "str:" prefix.
+ if (IsStrString(argValue))
+ {
+ // Skip the prefix
+ argValue += kStrStringPrefixLen;
+ argLen -= kStrStringPrefixLen;
+ }
+ *value = chip::ByteSpan(chip::Uint8::from_char(argValue), argLen);
+ return true;
+ });
+ break;
+ }
+
+ case ArgumentType::Bool: {
+ isValidArgument = HandleNullableOptional<bool>(arg, argValue, [&](auto * value) {
+ // Start with checking for actual boolean values.
+ if (strcasecmp(argValue, "true") == 0)
+ {
+ *value = true;
+ return true;
+ }
+
+ if (strcasecmp(argValue, "false") == 0)
+ {
+ *value = false;
+ return true;
+ }
+
+ // For backwards compat, keep accepting 0 and 1 for now as synonyms
+ // for false and true. Since we set our min to 0 and max to 1 for
+ // booleans, calling uint8Handler does the right thing in terms of
+ // only allowing those two values.
+ uint8_t temp = 0;
+ if (!uint8Handler(&temp))
+ {
+ return false;
+ }
+ *value = (temp == 1);
+ return true;
+ });
+ break;
+ }
+
+ case ArgumentType::Number_uint8: {
+ isValidArgument = HandleNullableOptional<uint8_t>(arg, argValue, uint8Handler);
+ break;
+ }
+
+ case ArgumentType::Number_uint16: {
+ isValidArgument = HandleNullableOptional<uint16_t>(arg, argValue, [&](auto * value) {
+ std::stringstream ss;
+ isHexNotation ? ss << std::hex << argValue : ss << argValue;
+ ss >> *value;
+
+ uint64_t min = chip::CanCastTo<uint64_t>(arg.min) ? static_cast<uint64_t>(arg.min) : 0;
+ uint64_t max = arg.max;
+ return (!ss.fail() && ss.eof() && *value >= min && *value <= max);
+ });
+ break;
+ }
+
+ case ArgumentType::Number_uint32: {
+ isValidArgument = HandleNullableOptional<uint32_t>(arg, argValue, [&](auto * value) {
+ std::stringstream ss;
+ isHexNotation ? ss << std::hex << argValue : ss << argValue;
+ ss >> *value;
+
+ uint64_t min = chip::CanCastTo<uint64_t>(arg.min) ? static_cast<uint64_t>(arg.min) : 0;
+ uint64_t max = arg.max;
+ return (!ss.fail() && ss.eof() && *value >= min && *value <= max);
+ });
+ break;
+ }
+
+ case ArgumentType::Number_uint64: {
+ isValidArgument = HandleNullableOptional<uint64_t>(arg, argValue, [&](auto * value) {
+ std::stringstream ss;
+ isHexNotation ? ss << std::hex << argValue : ss << argValue;
+ ss >> *value;
+
+ uint64_t min = chip::CanCastTo<uint64_t>(arg.min) ? static_cast<uint64_t>(arg.min) : 0;
+ uint64_t max = arg.max;
+ return (!ss.fail() && ss.eof() && *value >= min && *value <= max);
+ });
+ break;
+ }
+
+ case ArgumentType::Number_int8: {
+ isValidArgument = HandleNullableOptional<int8_t>(arg, argValue, [&](auto * value) {
+ // stringstream treats int8_t as char, which is not what we want here.
+ int16_t tmpValue;
+ std::stringstream ss;
+ isHexNotation ? ss << std::hex << argValue : ss << argValue;
+ ss >> tmpValue;
+ if (chip::CanCastTo<int8_t>(tmpValue))
+ {
+ *value = static_cast<int8_t>(tmpValue);
+
+ int64_t min = arg.min;
+ int64_t max = chip::CanCastTo<int64_t>(arg.max) ? static_cast<int64_t>(arg.max) : INT64_MAX;
+ return (!ss.fail() && ss.eof() && *value >= min && *value <= max);
+ }
+
+ return false;
+ });
+ break;
+ }
+
+ case ArgumentType::Number_int16: {
+ isValidArgument = HandleNullableOptional<int16_t>(arg, argValue, [&](auto * value) {
+ std::stringstream ss;
+ isHexNotation ? ss << std::hex << argValue : ss << argValue;
+ ss >> *value;
+
+ int64_t min = arg.min;
+ int64_t max = chip::CanCastTo<int64_t>(arg.max) ? static_cast<int64_t>(arg.max) : INT64_MAX;
+ return (!ss.fail() && ss.eof() && *value >= min && *value <= max);
+ });
+ break;
+ }
+
+ case ArgumentType::Number_int32: {
+ isValidArgument = HandleNullableOptional<int32_t>(arg, argValue, [&](auto * value) {
+ std::stringstream ss;
+ isHexNotation ? ss << std::hex << argValue : ss << argValue;
+ ss >> *value;
+
+ int64_t min = arg.min;
+ int64_t max = chip::CanCastTo<int64_t>(arg.max) ? static_cast<int64_t>(arg.max) : INT64_MAX;
+ return (!ss.fail() && ss.eof() && *value >= min && *value <= max);
+ });
+ break;
+ }
+
+ case ArgumentType::Number_int64: {
+ isValidArgument = HandleNullableOptional<int64_t>(arg, argValue, [&](auto * value) {
+ std::stringstream ss;
+ isHexNotation ? ss << std::hex << argValue : ss << argValue;
+ ss >> *value;
+
+ int64_t min = arg.min;
+ int64_t max = chip::CanCastTo<int64_t>(arg.max) ? static_cast<int64_t>(arg.max) : INT64_MAX;
+ return (!ss.fail() && ss.eof() && *value >= min && *value <= max);
+ });
+ break;
+ }
+
+ case ArgumentType::Float: {
+ isValidArgument = HandleNullableOptional<float>(arg, argValue, [&](auto * value) {
+ if (strcmp(argValue, "Infinity") == 0)
+ {
+ *value = INFINITY;
+ return true;
+ }
+
+ if (strcmp(argValue, "-Infinity") == 0)
+ {
+ *value = -INFINITY;
+ return true;
+ }
+
+ std::stringstream ss;
+ ss << argValue;
+ ss >> *value;
+ return (!ss.fail() && ss.eof());
+ });
+ break;
+ }
+
+ case ArgumentType::Double: {
+ isValidArgument = HandleNullableOptional<double>(arg, argValue, [&](auto * value) {
+ if (strcmp(argValue, "Infinity") == 0)
+ {
+ *value = INFINITY;
+ return true;
+ }
+
+ if (strcmp(argValue, "-Infinity") == 0)
+ {
+ *value = -INFINITY;
+ return true;
+ }
+
+ std::stringstream ss;
+ ss << argValue;
+ ss >> *value;
+ return (!ss.fail() && ss.eof());
+ });
+ break;
+ }
+
+ case ArgumentType::Address: {
+ isValidArgument = HandleNullableOptional<AddressWithInterface>(
+ arg, argValue, [&](auto * value) { return ParseAddressWithInterface(argValue, value); });
+ break;
+ }
+ }
+
+ if (!isValidArgument)
+ {
+ ChipLogError(NotSpecified, "InitArgs: Invalid argument %s: %s", arg.name, argValue);
+ }
+
+ return isValidArgument;
+}
+
+void Command::AddArgument(const char * name, const char * value, const char * desc)
+{
+ ReadOnlyGlobalCommandArgument arg;
+ arg.name = name;
+ arg.value = value;
+ arg.desc = desc;
+
+ mReadOnlyGlobalCommandArgument.SetValue(arg);
+}
+
+size_t Command::AddArgument(const char * name, char ** value, const char * desc, uint8_t flags)
+{
+ Argument arg;
+ arg.type = ArgumentType::String;
+ arg.name = name;
+ arg.value = reinterpret_cast<void *>(value);
+ arg.flags = flags;
+ arg.desc = desc;
+
+ return AddArgumentToList(std::move(arg));
+}
+
+size_t Command::AddArgument(const char * name, chip::CharSpan * value, const char * desc, uint8_t flags)
+{
+ Argument arg;
+ arg.type = ArgumentType::CharString;
+ arg.name = name;
+ arg.value = reinterpret_cast<void *>(value);
+ arg.flags = flags;
+ arg.desc = desc;
+
+ return AddArgumentToList(std::move(arg));
+}
+
+size_t Command::AddArgument(const char * name, chip::ByteSpan * value, const char * desc, uint8_t flags)
+{
+ Argument arg;
+ arg.type = ArgumentType::OctetString;
+ arg.name = name;
+ arg.value = reinterpret_cast<void *>(value);
+ arg.flags = flags;
+ arg.desc = desc;
+
+ return AddArgumentToList(std::move(arg));
+}
+
+size_t Command::AddArgument(const char * name, AddressWithInterface * out, const char * desc, uint8_t flags)
+{
+ Argument arg;
+ arg.type = ArgumentType::Address;
+ arg.name = name;
+ arg.value = reinterpret_cast<void *>(out);
+ arg.flags = flags;
+ arg.desc = desc;
+
+ return AddArgumentToList(std::move(arg));
+}
+
+size_t Command::AddArgument(const char * name, int64_t min, uint64_t max, std::vector<uint16_t> * value, const char * desc)
+{
+ Argument arg;
+ arg.type = ArgumentType::Vector16;
+ arg.name = name;
+ arg.value = static_cast<void *>(value);
+ arg.min = min;
+ arg.max = max;
+ arg.flags = 0;
+ arg.desc = desc;
+
+ return AddArgumentToList(std::move(arg));
+}
+
+size_t Command::AddArgument(const char * name, int64_t min, uint64_t max, std::vector<uint32_t> * value, const char * desc)
+{
+ Argument arg;
+ arg.type = ArgumentType::Vector32;
+ arg.name = name;
+ arg.value = static_cast<void *>(value);
+ arg.min = min;
+ arg.max = max;
+ arg.flags = 0;
+ arg.desc = desc;
+
+ return AddArgumentToList(std::move(arg));
+}
+
+size_t Command::AddArgument(const char * name, int64_t min, uint64_t max, chip::Optional<std::vector<uint32_t>> * value,
+ const char * desc)
+{
+ Argument arg;
+ arg.type = ArgumentType::Vector32;
+ arg.name = name;
+ arg.value = static_cast<void *>(value);
+ arg.min = min;
+ arg.max = max;
+ arg.flags = Argument::kOptional;
+ arg.desc = desc;
+
+ return AddArgumentToList(std::move(arg));
+}
+
+size_t Command::AddArgument(const char * name, int64_t min, uint64_t max, chip::Optional<std::vector<bool>> * value,
+ const char * desc)
+{
+ Argument arg;
+ arg.type = ArgumentType::VectorBool;
+ arg.name = name;
+ arg.value = static_cast<void *>(value);
+ arg.min = min;
+ arg.max = max;
+ arg.flags = Argument::kOptional;
+ arg.desc = desc;
+
+ return AddArgumentToList(std::move(arg));
+}
+
+size_t Command::AddArgument(const char * name, ComplexArgument * value, const char * desc, uint8_t flags)
+{
+ Argument arg;
+ arg.type = ArgumentType::Complex;
+ arg.name = name;
+ arg.value = static_cast<void *>(value);
+ arg.flags = flags;
+ arg.desc = desc;
+
+ return AddArgumentToList(std::move(arg));
+}
+
+size_t Command::AddArgument(const char * name, CustomArgument * value, const char * desc)
+{
+ Argument arg;
+ arg.type = ArgumentType::Custom;
+ arg.name = name;
+ arg.value = const_cast<void *>(reinterpret_cast<const void *>(value));
+ arg.flags = 0;
+ arg.desc = desc;
+
+ return AddArgumentToList(std::move(arg));
+}
+
+size_t Command::AddArgument(const char * name, std::vector<CustomArgument *> * value, const char * desc)
+{
+ Argument arg;
+ arg.type = ArgumentType::VectorCustom;
+ arg.name = name;
+ arg.value = static_cast<void *>(value);
+ arg.flags = 0;
+ arg.desc = desc;
+
+ return AddArgumentToList(std::move(arg));
+}
+
+size_t Command::AddArgument(const char * name, float min, float max, float * out, const char * desc, uint8_t flags)
+{
+ Argument arg;
+ arg.type = ArgumentType::Float;
+ arg.name = name;
+ arg.value = reinterpret_cast<void *>(out);
+ arg.flags = flags;
+ arg.desc = desc;
+ // Ignore min/max for now; they're always +-Infinity anyway.
+
+ return AddArgumentToList(std::move(arg));
+}
+
+size_t Command::AddArgument(const char * name, double min, double max, double * out, const char * desc, uint8_t flags)
+{
+ Argument arg;
+ arg.type = ArgumentType::Double;
+ arg.name = name;
+ arg.value = reinterpret_cast<void *>(out);
+ arg.flags = flags;
+ arg.desc = desc;
+ // Ignore min/max for now; they're always +-Infinity anyway.
+
+ return AddArgumentToList(std::move(arg));
+}
+
+size_t Command::AddArgument(const char * name, int64_t min, uint64_t max, void * out, ArgumentType type, const char * desc,
+ uint8_t flags)
+{
+ Argument arg;
+ arg.type = type;
+ arg.name = name;
+ arg.value = out;
+ arg.min = min;
+ arg.max = max;
+ arg.flags = flags;
+ arg.desc = desc;
+
+ return AddArgumentToList(std::move(arg));
+}
+
+size_t Command::AddArgument(const char * name, int64_t min, uint64_t max, void * out, const char * desc, uint8_t flags)
+{
+ Argument arg;
+ arg.type = ArgumentType::Number_uint8;
+ arg.name = name;
+ arg.value = out;
+ arg.min = min;
+ arg.max = max;
+ arg.flags = flags;
+ arg.desc = desc;
+
+ return AddArgumentToList(std::move(arg));
+}
+
+size_t Command::AddArgument(const char * name, std::vector<std::string> * value, const char * desc)
+{
+ Argument arg;
+ arg.type = ArgumentType::VectorString;
+ arg.name = name;
+ arg.value = static_cast<void *>(value);
+ arg.flags = 0;
+ arg.desc = desc;
+
+ return AddArgumentToList(std::move(arg));
+}
+
+size_t Command::AddArgument(const char * name, chip::Optional<std::vector<std::string>> * value, const char * desc)
+{
+ Argument arg;
+ arg.type = ArgumentType::VectorString;
+ arg.name = name;
+ arg.value = static_cast<void *>(value);
+ arg.flags = Argument::kOptional;
+ arg.desc = desc;
+
+ return AddArgumentToList(std::move(arg));
+}
+
+const char * Command::GetArgumentName(size_t index) const
+{
+ if (index < mArgs.size())
+ {
+ return mArgs.at(index).name;
+ }
+
+ return nullptr;
+}
+
+const char * Command::GetArgumentDescription(size_t index) const
+{
+ if (index < mArgs.size())
+ {
+ return mArgs.at(index).desc;
+ }
+
+ return nullptr;
+}
+
+const char * Command::GetReadOnlyGlobalCommandArgument() const
+{
+ if (GetAttribute())
+ {
+ return GetAttribute();
+ }
+
+ if (GetEvent())
+ {
+ return GetEvent();
+ }
+
+ return nullptr;
+}
+
+const char * Command::GetAttribute() const
+{
+ if (mReadOnlyGlobalCommandArgument.HasValue())
+ {
+ return mReadOnlyGlobalCommandArgument.Value().value;
+ }
+
+ return nullptr;
+}
+
+const char * Command::GetEvent() const
+{
+ if (mReadOnlyGlobalCommandArgument.HasValue())
+ {
+ return mReadOnlyGlobalCommandArgument.Value().value;
+ }
+
+ return nullptr;
+}
+
+size_t Command::AddArgumentToList(Argument && argument)
+{
+ if (argument.isOptional() || mArgs.empty() || !mArgs.back().isOptional())
+ {
+ // Safe to just append.
+ mArgs.emplace_back(std::move(argument));
+ return mArgs.size();
+ }
+
+ // We're inserting a non-optional arg but we already have something optional
+ // in the list. Insert before the first optional arg.
+ for (auto cur = mArgs.cbegin(), end = mArgs.cend(); cur != end; ++cur)
+ {
+ if ((*cur).isOptional())
+ {
+ mArgs.emplace(cur, std::move(argument));
+ return mArgs.size();
+ }
+ }
+
+ // Never reached.
+ VerifyOrDie(false);
+ return 0;
+}
+
+namespace {
+template <typename T>
+void ResetOptionalArg(const Argument & arg)
+{
+ VerifyOrDie(arg.isOptional());
+
+ if (arg.isNullable())
+ {
+ reinterpret_cast<chip::Optional<chip::app::DataModel::Nullable<T>> *>(arg.value)->ClearValue();
+ }
+ else
+ {
+ reinterpret_cast<chip::Optional<T> *>(arg.value)->ClearValue();
+ }
+}
+} // anonymous namespace
+
+void Command::ResetArguments()
+{
+ for (const auto & arg : mArgs)
+ {
+ const ArgumentType type = arg.type;
+ if (arg.isOptional())
+ {
+ // Must always clean these up so they don't carry over to the next
+ // command invocation in interactive mode.
+ switch (type)
+ {
+ case ArgumentType::Complex: {
+ // Optional Complex arguments are not currently supported via the <chip::Optional> class.
+ // Instead, they must be explicitly specified as optional using the kOptional flag,
+ // and the base TypedComplexArgument<T> class is referenced.
+ auto argument = static_cast<ComplexArgument *>(arg.value);
+ argument->Reset();
+ break;
+ }
+ case ArgumentType::Custom: {
+ // No optional custom arguments so far.
+ VerifyOrDie(false);
+ break;
+ }
+ case ArgumentType::VectorString: {
+ ResetOptionalArg<std::vector<std::string>>(arg);
+ break;
+ }
+ case ArgumentType::VectorBool: {
+ ResetOptionalArg<std::vector<bool>>(arg);
+ break;
+ }
+ case ArgumentType::Vector16: {
+ // No optional Vector16 arguments so far.
+ VerifyOrDie(false);
+ break;
+ }
+ case ArgumentType::Vector32: {
+ ResetOptionalArg<std::vector<uint32_t>>(arg);
+ break;
+ }
+ case ArgumentType::VectorCustom: {
+ // No optional VectorCustom arguments so far.
+ VerifyOrDie(false);
+ break;
+ }
+ case ArgumentType::String: {
+ ResetOptionalArg<char *>(arg);
+ break;
+ }
+ case ArgumentType::CharString: {
+ ResetOptionalArg<chip::CharSpan>(arg);
+ break;
+ }
+ case ArgumentType::OctetString: {
+ ResetOptionalArg<chip::ByteSpan>(arg);
+ break;
+ }
+ case ArgumentType::Bool: {
+ ResetOptionalArg<bool>(arg);
+ break;
+ }
+ case ArgumentType::Number_uint8: {
+ ResetOptionalArg<uint8_t>(arg);
+ break;
+ }
+ case ArgumentType::Number_uint16: {
+ ResetOptionalArg<uint16_t>(arg);
+ break;
+ }
+ case ArgumentType::Number_uint32: {
+ ResetOptionalArg<uint32_t>(arg);
+ break;
+ }
+ case ArgumentType::Number_uint64: {
+ ResetOptionalArg<uint64_t>(arg);
+ break;
+ }
+ case ArgumentType::Number_int8: {
+ ResetOptionalArg<int8_t>(arg);
+ break;
+ }
+ case ArgumentType::Number_int16: {
+ ResetOptionalArg<int16_t>(arg);
+ break;
+ }
+ case ArgumentType::Number_int32: {
+ ResetOptionalArg<int32_t>(arg);
+ break;
+ }
+ case ArgumentType::Number_int64: {
+ ResetOptionalArg<int64_t>(arg);
+ break;
+ }
+ case ArgumentType::Float: {
+ ResetOptionalArg<float>(arg);
+ break;
+ }
+ case ArgumentType::Double: {
+ ResetOptionalArg<double>(arg);
+ break;
+ }
+ case ArgumentType::Address: {
+ ResetOptionalArg<AddressWithInterface>(arg);
+ break;
+ }
+ }
+ }
+ else
+ {
+ // Some non-optional arguments have state that needs to be cleaned
+ // up too.
+ if (type == ArgumentType::Vector16)
+ {
+ auto vectorArgument = static_cast<std::vector<uint16_t> *>(arg.value);
+ vectorArgument->clear();
+ }
+ else if (type == ArgumentType::Vector32)
+ {
+ auto vectorArgument = static_cast<std::vector<uint32_t> *>(arg.value);
+ vectorArgument->clear();
+ }
+ else if (type == ArgumentType::VectorCustom)
+ {
+ auto vectorArgument = static_cast<std::vector<CustomArgument *> *>(arg.value);
+ for (auto & customArgument : *vectorArgument)
+ {
+ delete customArgument;
+ }
+ vectorArgument->clear();
+ }
+ else if (type == ArgumentType::Complex)
+ {
+ auto argument = static_cast<ComplexArgument *>(arg.value);
+ argument->Reset();
+ }
+ }
+ }
+}
diff --git a/examples/fabric-admin/commands/common/Command.h b/examples/fabric-admin/commands/common/Command.h
new file mode 100644
index 0000000..ec8b51d
--- /dev/null
+++ b/examples/fabric-admin/commands/common/Command.h
@@ -0,0 +1,307 @@
+/*
+ * 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/data-model/Nullable.h>
+#include <commands/clusters/ComplexArgument.h>
+#include <commands/clusters/CustomArgument.h>
+#include <inet/InetInterface.h>
+#include <lib/core/Optional.h>
+#include <lib/support/Span.h>
+#include <lib/support/logging/CHIPLogging.h>
+
+#include <atomic>
+#include <condition_variable>
+#include <memory>
+#include <mutex>
+#include <string>
+#include <vector>
+
+class Command;
+
+template <typename T, typename... Args>
+std::unique_ptr<Command> make_unique(Args &&... args)
+{
+ return std::unique_ptr<Command>(new T(std::forward<Args>(args)...));
+}
+
+struct movable_initializer_list
+{
+ movable_initializer_list(std::unique_ptr<Command> && in) : item(std::move(in)) {}
+ operator std::unique_ptr<Command>() const && { return std::move(item); }
+ mutable std::unique_ptr<Command> item;
+};
+
+typedef std::initializer_list<movable_initializer_list> commands_list;
+
+enum ArgumentType
+{
+ Number_uint8,
+ Number_uint16,
+ Number_uint32,
+ Number_uint64,
+ Number_int8,
+ Number_int16,
+ Number_int32,
+ Number_int64,
+ Float,
+ Double,
+ Bool,
+ String,
+ CharString,
+ OctetString,
+ Address,
+ Complex,
+ Custom,
+ VectorBool,
+ Vector16,
+ Vector32,
+ VectorCustom,
+ VectorString, // comma separated string items
+};
+
+struct Argument
+{
+ const char * name;
+ ArgumentType type;
+ int64_t min;
+ uint64_t max;
+ void * value;
+ uint8_t flags;
+ const char * desc;
+
+ enum
+ {
+ kOptional = (1 << 0),
+ kNullable = (1 << 1),
+ };
+
+ bool isOptional() const { return flags & kOptional; }
+ bool isNullable() const { return flags & kNullable; }
+};
+
+struct ReadOnlyGlobalCommandArgument
+{
+ const char * name;
+ const char * value;
+ const char * desc;
+};
+
+class Command
+{
+public:
+ struct AddressWithInterface
+ {
+ ::chip::Inet::IPAddress address;
+ ::chip::Inet::InterfaceId interfaceId;
+ };
+
+ Command(const char * commandName, const char * helpText = nullptr) : mName(commandName), mHelpText(helpText) {}
+ virtual ~Command() {}
+
+ const char * GetName(void) const { return mName; }
+ const char * GetHelpText() const { return mHelpText; }
+ const char * GetReadOnlyGlobalCommandArgument(void) const;
+ const char * GetAttribute(void) const;
+ const char * GetEvent(void) const;
+ const char * GetArgumentName(size_t index) const;
+ const char * GetArgumentDescription(size_t index) const;
+ bool GetArgumentIsOptional(size_t index) const { return mArgs[index].isOptional(); }
+ size_t GetArgumentsCount(void) const { return mArgs.size(); }
+
+ bool InitArguments(int argc, char ** argv);
+ void AddArgument(const char * name, const char * value, const char * desc = "");
+ /**
+ * @brief
+ * Add a char string command argument
+ *
+ * @param name The name that will be displayed in the command help
+ * @param value A pointer to a `char *` where the argv value will be stored
+ * @param flags
+ * @param desc The description of the argument that will be displayed in the command help
+ * @returns The number of arguments currently added to the command
+ */
+ size_t AddArgument(const char * name, char ** value, const char * desc = "", uint8_t flags = 0);
+
+ /**
+ * Add an octet string command argument
+ */
+ size_t AddArgument(const char * name, chip::ByteSpan * value, const char * desc = "", uint8_t flags = 0);
+ size_t AddArgument(const char * name, chip::Span<const char> * value, const char * desc = "", uint8_t flags = 0);
+ size_t AddArgument(const char * name, AddressWithInterface * out, const char * desc = "", uint8_t flags = 0);
+ // Optional Complex arguments are not currently supported via the <chip::Optional> class.
+ // Instead, they must be explicitly specified as optional using kOptional in the flags parameter,
+ // and the base TypedComplexArgument<T> class is referenced.
+ size_t AddArgument(const char * name, ComplexArgument * value, const char * desc = "", uint8_t flags = 0);
+ size_t AddArgument(const char * name, CustomArgument * value, const char * desc = "");
+ size_t AddArgument(const char * name, int64_t min, uint64_t max, bool * out, const char * desc = "", uint8_t flags = 0)
+ {
+ return AddArgument(name, min, max, reinterpret_cast<void *>(out), Bool, desc, flags);
+ }
+ size_t AddArgument(const char * name, int64_t min, uint64_t max, int8_t * out, const char * desc = "", uint8_t flags = 0)
+ {
+ return AddArgument(name, min, max, reinterpret_cast<void *>(out), Number_int8, desc, flags);
+ }
+ size_t AddArgument(const char * name, int64_t min, uint64_t max, int16_t * out, const char * desc = "", uint8_t flags = 0)
+ {
+ return AddArgument(name, min, max, reinterpret_cast<void *>(out), Number_int16, desc, flags);
+ }
+ size_t AddArgument(const char * name, int64_t min, uint64_t max, int32_t * out, const char * desc = "", uint8_t flags = 0)
+ {
+ return AddArgument(name, min, max, reinterpret_cast<void *>(out), Number_int32, desc, flags);
+ }
+ size_t AddArgument(const char * name, int64_t min, uint64_t max, int64_t * out, const char * desc = "", uint8_t flags = 0)
+ {
+ return AddArgument(name, min, max, reinterpret_cast<void *>(out), Number_int64, desc, flags);
+ }
+ size_t AddArgument(const char * name, int64_t min, uint64_t max, uint8_t * out, const char * desc = "", uint8_t flags = 0)
+ {
+ return AddArgument(name, min, max, reinterpret_cast<void *>(out), Number_uint8, desc, flags);
+ }
+ size_t AddArgument(const char * name, int64_t min, uint64_t max, uint16_t * out, const char * desc = "", uint8_t flags = 0)
+ {
+ return AddArgument(name, min, max, reinterpret_cast<void *>(out), Number_uint16, desc, flags);
+ }
+ size_t AddArgument(const char * name, int64_t min, uint64_t max, uint32_t * out, const char * desc = "", uint8_t flags = 0)
+ {
+ return AddArgument(name, min, max, reinterpret_cast<void *>(out), Number_uint32, desc, flags);
+ }
+ size_t AddArgument(const char * name, int64_t min, uint64_t max, uint64_t * out, const char * desc = "", uint8_t flags = 0)
+ {
+ return AddArgument(name, min, max, reinterpret_cast<void *>(out), Number_uint64, desc, flags);
+ }
+
+ size_t AddArgument(const char * name, float min, float max, float * out, const char * desc = "", uint8_t flags = 0);
+ size_t AddArgument(const char * name, double min, double max, double * out, const char * desc = "", uint8_t flags = 0);
+
+ size_t AddArgument(const char * name, int64_t min, uint64_t max, std::vector<uint16_t> * value, const char * desc = "");
+ size_t AddArgument(const char * name, int64_t min, uint64_t max, std::vector<uint32_t> * value, const char * desc = "");
+ size_t AddArgument(const char * name, std::vector<CustomArgument *> * value, const char * desc = "");
+ size_t AddArgument(const char * name, int64_t min, uint64_t max, chip::Optional<std::vector<bool>> * value,
+ const char * desc = "");
+ size_t AddArgument(const char * name, int64_t min, uint64_t max, chip::Optional<std::vector<uint32_t>> * value,
+ const char * desc = "");
+
+ template <typename T, typename = std::enable_if_t<std::is_enum<T>::value>>
+ size_t AddArgument(const char * name, int64_t min, uint64_t max, T * out, const char * desc = "", uint8_t flags = 0)
+ {
+ return AddArgument(name, min, max, reinterpret_cast<std::underlying_type_t<T> *>(out), desc, flags);
+ }
+
+ template <typename T>
+ size_t AddArgument(const char * name, int64_t min, uint64_t max, chip::BitFlags<T> * out, const char * desc = "",
+ uint8_t flags = 0)
+ {
+ // This is a terrible hack that relies on BitFlags only having the one
+ // mValue member.
+ return AddArgument(name, min, max, reinterpret_cast<T *>(out), desc, flags);
+ }
+
+ template <typename T>
+ size_t AddArgument(const char * name, int64_t min, uint64_t max, chip::BitMask<T> * out, const char * desc = "",
+ uint8_t flags = 0)
+ {
+ // This is a terrible hack that relies on BitMask only having the one
+ // mValue member.
+ return AddArgument(name, min, max, reinterpret_cast<T *>(out), desc, flags);
+ }
+
+ template <typename T>
+ size_t AddArgument(const char * name, chip::Optional<T> * value, const char * desc = "")
+ {
+ return AddArgument(name, reinterpret_cast<T *>(value), desc, Argument::kOptional);
+ }
+
+ template <typename T>
+ size_t AddArgument(const char * name, int64_t min, uint64_t max, chip::Optional<T> * value, const char * desc = "")
+ {
+ return AddArgument(name, min, max, reinterpret_cast<T *>(value), desc, Argument::kOptional);
+ }
+
+ template <typename T>
+ size_t AddArgument(const char * name, chip::app::DataModel::Nullable<T> * value, const char * desc = "", uint8_t flags = 0)
+ {
+ return AddArgument(name, reinterpret_cast<T *>(value), desc, flags | Argument::kNullable);
+ }
+
+ template <typename T>
+ size_t AddArgument(const char * name, int64_t min, uint64_t max, chip::app::DataModel::Nullable<T> * value,
+ const char * desc = "", uint8_t flags = 0)
+ {
+ return AddArgument(name, min, max, reinterpret_cast<T *>(value), desc, flags | Argument::kNullable);
+ }
+
+ size_t AddArgument(const char * name, float min, float max, chip::app::DataModel::Nullable<float> * value,
+ const char * desc = "", uint8_t flags = 0)
+ {
+ return AddArgument(name, min, max, reinterpret_cast<float *>(value), desc, flags | Argument::kNullable);
+ }
+
+ size_t AddArgument(const char * name, double min, double max, chip::app::DataModel::Nullable<double> * value,
+ const char * desc = "", uint8_t flags = 0)
+ {
+ return AddArgument(name, min, max, reinterpret_cast<double *>(value), desc, flags | Argument::kNullable);
+ }
+
+ size_t AddArgument(const char * name, std::vector<std::string> * value, const char * desc);
+ size_t AddArgument(const char * name, chip::Optional<std::vector<std::string>> * value, const char * desc);
+
+ void ResetArguments();
+
+ virtual CHIP_ERROR Run() = 0;
+
+ bool IsInteractive() { return mIsInteractive; }
+
+ CHIP_ERROR RunAsInteractive(const chip::Optional<char *> & interactiveStorageDirectory, bool advertiseOperational)
+ {
+ mStorageDirectory = interactiveStorageDirectory;
+ mIsInteractive = true;
+ mAdvertiseOperational = advertiseOperational;
+ return Run();
+ }
+
+ const chip::Optional<char *> & GetStorageDirectory() const { return mStorageDirectory; }
+
+protected:
+ // mStorageDirectory lives here so we can just set it in RunAsInteractive.
+ chip::Optional<char *> mStorageDirectory;
+
+ // mAdvertiseOperational lives here so we can just set it in
+ // RunAsInteractive; it's only used by CHIPCommand.
+ bool mAdvertiseOperational = false;
+
+private:
+ bool InitArgument(size_t argIndex, char * argValue);
+ size_t AddArgument(const char * name, int64_t min, uint64_t max, void * out, ArgumentType type, const char * desc,
+ uint8_t flags);
+ size_t AddArgument(const char * name, int64_t min, uint64_t max, void * out, const char * desc, uint8_t flags);
+
+ /**
+ * Add the Argument to our list. This preserves the property that all
+ * optional arguments come at the end of the list.
+ */
+ size_t AddArgumentToList(Argument && argument);
+
+ const char * mName = nullptr;
+ const char * mHelpText = nullptr;
+ bool mIsInteractive = false;
+
+ chip::Optional<ReadOnlyGlobalCommandArgument> mReadOnlyGlobalCommandArgument;
+ std::vector<Argument> mArgs;
+};
diff --git a/examples/fabric-admin/commands/common/Commands.cpp b/examples/fabric-admin/commands/common/Commands.cpp
new file mode 100644
index 0000000..3742978
--- /dev/null
+++ b/examples/fabric-admin/commands/common/Commands.cpp
@@ -0,0 +1,703 @@
+/*
+ * 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 "Commands.h"
+
+#include "Command.h"
+
+#include <algorithm>
+#include <iomanip>
+#include <sstream>
+#include <string>
+
+#include <lib/support/Base64.h>
+#include <lib/support/CHIPMem.h>
+#include <lib/support/CodeUtils.h>
+#include <platform/CHIPDeviceConfig.h>
+#include <platform/KeyValueStoreManager.h>
+
+#include "../clusters/JsonParser.h"
+
+namespace {
+
+char kInteractiveModeName[] = "";
+constexpr size_t kInteractiveModeArgumentsMaxLength = 32;
+constexpr char kOptionalArgumentPrefix[] = "--";
+constexpr char kJsonClusterKey[] = "cluster";
+constexpr char kJsonCommandKey[] = "command";
+constexpr char kJsonCommandSpecifierKey[] = "command_specifier";
+constexpr char kJsonArgumentsKey[] = "arguments";
+
+#if !CHIP_DISABLE_PLATFORM_KVS
+template <typename T>
+struct HasInitWithString
+{
+ template <typename U>
+ static constexpr auto check(U *) -> typename std::is_same<decltype(std::declval<U>().Init("")), CHIP_ERROR>::type;
+
+ template <typename>
+ static constexpr std::false_type check(...);
+
+ typedef decltype(check<std::remove_reference_t<T>>(nullptr)) type;
+
+public:
+ static constexpr bool value = type::value;
+};
+
+// Template so we can do conditional enabling
+template <typename T, std::enable_if_t<HasInitWithString<T>::value, int> = 0>
+static void UseStorageDirectory(T & storageManagerImpl, const char * storageDirectory)
+{
+ std::string platformKVS = std::string(storageDirectory) + "/chip_tool_kvs";
+ storageManagerImpl.Init(platformKVS.c_str());
+}
+
+template <typename T, std::enable_if_t<!HasInitWithString<T>::value, int> = 0>
+static void UseStorageDirectory(T & storageManagerImpl, const char * storageDirectory)
+{}
+#endif // !CHIP_DISABLE_PLATFORM_KVS
+
+bool GetArgumentsFromJson(Command * command, Json::Value & value, bool optional, std::vector<std::string> & outArgs)
+{
+ auto memberNames = value.getMemberNames();
+
+ std::vector<std::string> args;
+ for (size_t i = 0; i < command->GetArgumentsCount(); i++)
+ {
+ auto argName = command->GetArgumentName(i);
+ auto memberNamesIterator = memberNames.begin();
+ while (memberNamesIterator != memberNames.end())
+ {
+ auto memberName = *memberNamesIterator;
+ if (strcasecmp(argName, memberName.c_str()) != 0)
+ {
+ memberNamesIterator++;
+ continue;
+ }
+
+ if (command->GetArgumentIsOptional(i) != optional)
+ {
+ memberNamesIterator = memberNames.erase(memberNamesIterator);
+ continue;
+ }
+
+ if (optional)
+ {
+ args.push_back(std::string(kOptionalArgumentPrefix) + argName);
+ }
+
+ auto argValue = value[memberName].asString();
+ args.push_back(std::move(argValue));
+ memberNamesIterator = memberNames.erase(memberNamesIterator);
+ break;
+ }
+ }
+
+ if (memberNames.size())
+ {
+ auto memberName = memberNames.front();
+ ChipLogError(NotSpecified, "The argument \"\%s\" is not supported.", memberName.c_str());
+ return false;
+ }
+
+ outArgs = args;
+ return true;
+};
+
+// Check for arguments with a starting '"' but no ending '"': those
+// would indicate that people are using double-quoting, not single
+// quoting, on arguments with spaces.
+static void DetectAndLogMismatchedDoubleQuotes(int argc, char ** argv)
+{
+ for (int curArg = 0; curArg < argc; ++curArg)
+ {
+ char * arg = argv[curArg];
+ if (!arg)
+ {
+ continue;
+ }
+
+ auto len = strlen(arg);
+ if (len == 0)
+ {
+ continue;
+ }
+
+ if (arg[0] == '"' && arg[len - 1] != '"')
+ {
+ ChipLogError(NotSpecified,
+ "Mismatched '\"' detected in argument: '%s'. Use single quotes to delimit arguments with spaces "
+ "in them: 'x y', not \"x y\".",
+ arg);
+ }
+ }
+}
+
+} // namespace
+
+void Commands::Register(const char * commandSetName, commands_list commandsList, const char * helpText, bool isCluster)
+{
+ VerifyOrDieWithMsg(isCluster || helpText != nullptr, NotSpecified, "Non-cluster command sets must have help text");
+ mCommandSets[commandSetName].isCluster = isCluster;
+ mCommandSets[commandSetName].helpText = helpText;
+ for (auto & command : commandsList)
+ {
+ mCommandSets[commandSetName].commands.push_back(std::move(command));
+ }
+}
+
+int Commands::Run(int argc, char ** argv)
+{
+ CHIP_ERROR err = CHIP_NO_ERROR;
+
+ err = chip::Platform::MemoryInit();
+ VerifyOrExit(err == CHIP_NO_ERROR, ChipLogError(Controller, "Init Memory failure: %s", chip::ErrorStr(err)));
+
+#ifdef CONFIG_USE_LOCAL_STORAGE
+ err = mStorage.Init();
+ VerifyOrExit(err == CHIP_NO_ERROR, ChipLogError(Controller, "Init Storage failure: %s", chip::ErrorStr(err)));
+
+ chip::Logging::SetLogFilter(mStorage.GetLoggingLevel());
+#endif // CONFIG_USE_LOCAL_STORAGE
+
+ err = RunCommand(argc, argv);
+ VerifyOrExit(err == CHIP_NO_ERROR, ChipLogError(NotSpecified, "Run command failure: %s", chip::ErrorStr(err)));
+
+exit:
+ return (err == CHIP_NO_ERROR) ? EXIT_SUCCESS : EXIT_FAILURE;
+}
+
+int Commands::RunInteractive(const char * command, const chip::Optional<char *> & storageDirectory, bool advertiseOperational)
+{
+ std::vector<std::string> arguments;
+ VerifyOrReturnValue(DecodeArgumentsFromInteractiveMode(command, arguments), EXIT_FAILURE);
+
+ if (arguments.size() > (kInteractiveModeArgumentsMaxLength - 1 /* for interactive mode name */))
+ {
+ ChipLogError(NotSpecified, "Too many arguments. Ignoring.");
+ arguments.resize(kInteractiveModeArgumentsMaxLength - 1);
+ }
+
+ int argc = 0;
+ char * argv[kInteractiveModeArgumentsMaxLength] = {};
+ argv[argc++] = kInteractiveModeName;
+
+ std::string commandStr;
+ for (auto & arg : arguments)
+ {
+ argv[argc] = new char[arg.size() + 1];
+ strcpy(argv[argc++], arg.c_str());
+ commandStr += arg;
+ commandStr += " ";
+ }
+
+ ChipLogProgress(NotSpecified, "Command: %s", commandStr.c_str());
+ auto err = RunCommand(argc, argv, true, storageDirectory, advertiseOperational);
+
+ // Do not delete arg[0]
+ for (auto i = 1; i < argc; i++)
+ {
+ delete[] argv[i];
+ }
+
+ return (err == CHIP_NO_ERROR) ? EXIT_SUCCESS : EXIT_FAILURE;
+}
+
+CHIP_ERROR Commands::RunCommand(int argc, char ** argv, bool interactive,
+ const chip::Optional<char *> & interactiveStorageDirectory, bool interactiveAdvertiseOperational)
+{
+ Command * command = nullptr;
+
+ if (argc <= 1)
+ {
+ ChipLogError(NotSpecified, "Missing cluster or command set name");
+ ShowCommandSets(argv[0]);
+ return CHIP_ERROR_INVALID_ARGUMENT;
+ }
+
+ auto commandSetIter = GetCommandSet(argv[1]);
+ if (commandSetIter == mCommandSets.end())
+ {
+ ChipLogError(NotSpecified, "Unknown cluster or command set: %s", argv[1]);
+ ShowCommandSets(argv[0]);
+ return CHIP_ERROR_INVALID_ARGUMENT;
+ }
+
+ auto & commandList = commandSetIter->second.commands;
+ auto * helpText = commandSetIter->second.helpText;
+
+ if (argc <= 2)
+ {
+ ChipLogError(NotSpecified, "Missing command name");
+ ShowCommandSet(argv[0], argv[1], commandList, helpText);
+ return CHIP_ERROR_INVALID_ARGUMENT;
+ }
+
+ bool isGlobalCommand = IsGlobalCommand(argv[2]);
+ if (!isGlobalCommand)
+ {
+ command = GetCommand(commandList, argv[2]);
+ if (command == nullptr)
+ {
+ ChipLogError(NotSpecified, "Unknown command: %s", argv[2]);
+ ShowCommandSet(argv[0], argv[1], commandList, helpText);
+ return CHIP_ERROR_INVALID_ARGUMENT;
+ }
+ }
+ else if (IsEventCommand(argv[2]))
+ {
+ if (argc <= 3)
+ {
+ ChipLogError(NotSpecified, "Missing event name");
+ ShowClusterEvents(argv[0], argv[1], argv[2], commandList);
+ return CHIP_ERROR_INVALID_ARGUMENT;
+ }
+
+ command = GetGlobalCommand(commandList, argv[2], argv[3]);
+ if (command == nullptr)
+ {
+ ChipLogError(NotSpecified, "Unknown event: %s", argv[3]);
+ ShowClusterEvents(argv[0], argv[1], argv[2], commandList);
+ return CHIP_ERROR_INVALID_ARGUMENT;
+ }
+ }
+ else
+ {
+ if (argc <= 3)
+ {
+ ChipLogError(NotSpecified, "Missing attribute name");
+ ShowClusterAttributes(argv[0], argv[1], argv[2], commandList);
+ return CHIP_ERROR_INVALID_ARGUMENT;
+ }
+
+ command = GetGlobalCommand(commandList, argv[2], argv[3]);
+ if (command == nullptr)
+ {
+ ChipLogError(NotSpecified, "Unknown attribute: %s", argv[3]);
+ ShowClusterAttributes(argv[0], argv[1], argv[2], commandList);
+ return CHIP_ERROR_INVALID_ARGUMENT;
+ }
+ }
+
+ int argumentsPosition = isGlobalCommand ? 4 : 3;
+ if (!command->InitArguments(argc - argumentsPosition, &argv[argumentsPosition]))
+ {
+ if (interactive)
+ {
+ DetectAndLogMismatchedDoubleQuotes(argc - argumentsPosition, &argv[argumentsPosition]);
+ }
+ ShowCommand(argv[0], argv[1], command);
+ return CHIP_ERROR_INVALID_ARGUMENT;
+ }
+
+ if (interactive)
+ {
+ return command->RunAsInteractive(interactiveStorageDirectory, interactiveAdvertiseOperational);
+ }
+
+ // Now that the command is initialized, get our storage from it as needed
+ // and set up our loging level.
+#ifdef CONFIG_USE_LOCAL_STORAGE
+ CHIP_ERROR err = mStorage.Init(nullptr, command->GetStorageDirectory().ValueOr(nullptr));
+ if (err != CHIP_NO_ERROR)
+ {
+ ChipLogError(Controller, "Init Storage failure: %s", chip::ErrorStr(err));
+ return err;
+ }
+
+ chip::Logging::SetLogFilter(mStorage.GetLoggingLevel());
+
+#if !CHIP_DISABLE_PLATFORM_KVS
+ UseStorageDirectory(chip::DeviceLayer::PersistedStorage::KeyValueStoreMgrImpl(), mStorage.GetDirectory());
+#endif // !CHIP_DISABLE_PLATFORM_KVS
+
+#endif // CONFIG_USE_LOCAL_STORAGE
+
+ return command->Run();
+}
+
+Commands::CommandSetMap::iterator Commands::GetCommandSet(std::string commandSetName)
+{
+ for (auto & commandSet : mCommandSets)
+ {
+ std::string key(commandSet.first);
+ std::transform(key.begin(), key.end(), key.begin(), ::tolower);
+ if (key.compare(commandSetName) == 0)
+ {
+ return mCommandSets.find(commandSet.first);
+ }
+ }
+
+ return mCommandSets.end();
+}
+
+Command * Commands::GetCommand(CommandsVector & commands, std::string commandName)
+{
+ for (auto & command : commands)
+ {
+ if (commandName.compare(command->GetName()) == 0)
+ {
+ return command.get();
+ }
+ }
+
+ return nullptr;
+}
+
+Command * Commands::GetGlobalCommand(CommandsVector & commands, std::string commandName, std::string attributeName)
+{
+ for (auto & command : commands)
+ {
+ if (commandName.compare(command->GetName()) == 0 && attributeName.compare(command->GetAttribute()) == 0)
+ {
+ return command.get();
+ }
+ }
+
+ return nullptr;
+}
+
+bool Commands::IsAttributeCommand(std::string commandName) const
+{
+ return commandName.compare("read") == 0 || commandName.compare("write") == 0 || commandName.compare("force-write") == 0 ||
+ commandName.compare("subscribe") == 0;
+}
+
+bool Commands::IsEventCommand(std::string commandName) const
+{
+ return commandName.compare("read-event") == 0 || commandName.compare("subscribe-event") == 0;
+}
+
+bool Commands::IsGlobalCommand(std::string commandName) const
+{
+ return IsAttributeCommand(commandName) || IsEventCommand(commandName);
+}
+
+void Commands::ShowCommandSetOverview(std::string commandSetName, const CommandSet & commandSet)
+{
+ std::transform(commandSetName.begin(), commandSetName.end(), commandSetName.begin(),
+ [](unsigned char c) { return std::tolower(c); });
+ fprintf(stderr, " | * %-82s|\n", commandSetName.c_str());
+ ShowHelpText(commandSet.helpText);
+}
+
+void Commands::ShowCommandSets(std::string executable)
+{
+ fprintf(stderr, "Usage:\n");
+ fprintf(stderr, " %s cluster_name command_name [param1 param2 ...]\n", executable.c_str());
+ fprintf(stderr, "or:\n");
+ fprintf(stderr, " %s command_set_name command_name [param1 param2 ...]\n", executable.c_str());
+ fprintf(stderr, "\n");
+ // Table of clusters
+ fprintf(stderr, " +-------------------------------------------------------------------------------------+\n");
+ fprintf(stderr, " | Clusters: |\n");
+ fprintf(stderr, " +-------------------------------------------------------------------------------------+\n");
+ for (auto & commandSet : mCommandSets)
+ {
+ if (commandSet.second.isCluster)
+ {
+ ShowCommandSetOverview(commandSet.first, commandSet.second);
+ }
+ }
+ fprintf(stderr, " +-------------------------------------------------------------------------------------+\n");
+ fprintf(stderr, "\n");
+
+ // Table of command sets
+ fprintf(stderr, " +-------------------------------------------------------------------------------------+\n");
+ fprintf(stderr, " | Command sets: |\n");
+ fprintf(stderr, " +-------------------------------------------------------------------------------------+\n");
+ for (auto & commandSet : mCommandSets)
+ {
+ if (!commandSet.second.isCluster)
+ {
+ ShowCommandSetOverview(commandSet.first, commandSet.second);
+ }
+ }
+ fprintf(stderr, " +-------------------------------------------------------------------------------------+\n");
+}
+
+void Commands::ShowCommandSet(std::string executable, std::string commandSetName, CommandsVector & commands, const char * helpText)
+{
+ fprintf(stderr, "Usage:\n");
+ fprintf(stderr, " %s %s command_name [param1 param2 ...]\n", executable.c_str(), commandSetName.c_str());
+
+ if (helpText)
+ {
+ fprintf(stderr, "\n%s\n", helpText);
+ }
+
+ fprintf(stderr, "\n");
+ fprintf(stderr, " +-------------------------------------------------------------------------------------+\n");
+ fprintf(stderr, " | Commands: |\n");
+ fprintf(stderr, " +-------------------------------------------------------------------------------------+\n");
+ bool readCommand = false;
+ bool writeCommand = false;
+ bool writeOverrideCommand = false;
+ bool subscribeCommand = false;
+ bool readEventCommand = false;
+ bool subscribeEventCommand = false;
+ for (auto & command : commands)
+ {
+ bool shouldPrint = true;
+
+ if (IsGlobalCommand(command->GetName()))
+ {
+ if (strcmp(command->GetName(), "read") == 0 && !readCommand)
+ {
+ readCommand = true;
+ }
+ else if (strcmp(command->GetName(), "write") == 0 && !writeCommand)
+ {
+ writeCommand = true;
+ }
+ else if (strcmp(command->GetName(), "force-write") == 0 && !writeOverrideCommand)
+ {
+ writeOverrideCommand = true;
+ }
+ else if (strcmp(command->GetName(), "subscribe") == 0 && !subscribeCommand)
+ {
+ subscribeCommand = true;
+ }
+ else if (strcmp(command->GetName(), "read-event") == 0 && !readEventCommand)
+ {
+ readEventCommand = true;
+ }
+ else if (strcmp(command->GetName(), "subscribe-event") == 0 && !subscribeEventCommand)
+ {
+ subscribeEventCommand = true;
+ }
+ else
+ {
+ shouldPrint = false;
+ }
+ }
+
+ if (shouldPrint)
+ {
+ fprintf(stderr, " | * %-82s|\n", command->GetName());
+ ShowHelpText(command->GetHelpText());
+ }
+ }
+ fprintf(stderr, " +-------------------------------------------------------------------------------------+\n");
+}
+
+void Commands::ShowClusterAttributes(std::string executable, std::string clusterName, std::string commandName,
+ CommandsVector & commands)
+{
+ fprintf(stderr, "Usage:\n");
+ fprintf(stderr, " %s %s %s attribute-name [param1 param2 ...]\n", executable.c_str(), clusterName.c_str(),
+ commandName.c_str());
+ fprintf(stderr, "\n");
+ fprintf(stderr, " +-------------------------------------------------------------------------------------+\n");
+ fprintf(stderr, " | Attributes: |\n");
+ fprintf(stderr, " +-------------------------------------------------------------------------------------+\n");
+ for (auto & command : commands)
+ {
+ if (commandName.compare(command->GetName()) == 0)
+ {
+ fprintf(stderr, " | * %-82s|\n", command->GetAttribute());
+ }
+ }
+ fprintf(stderr, " +-------------------------------------------------------------------------------------+\n");
+}
+
+void Commands::ShowClusterEvents(std::string executable, std::string clusterName, std::string commandName,
+ CommandsVector & commands)
+{
+ fprintf(stderr, "Usage:\n");
+ fprintf(stderr, " %s %s %s event-name [param1 param2 ...]\n", executable.c_str(), clusterName.c_str(), commandName.c_str());
+ fprintf(stderr, "\n");
+ fprintf(stderr, " +-------------------------------------------------------------------------------------+\n");
+ fprintf(stderr, " | Events: |\n");
+ fprintf(stderr, " +-------------------------------------------------------------------------------------+\n");
+ for (auto & command : commands)
+ {
+ if (commandName.compare(command->GetName()) == 0)
+ {
+ fprintf(stderr, " | * %-82s|\n", command->GetEvent());
+ }
+ }
+ fprintf(stderr, " +-------------------------------------------------------------------------------------+\n");
+}
+
+void Commands::ShowCommand(std::string executable, std::string clusterName, Command * command)
+{
+ fprintf(stderr, "Usage:\n");
+
+ std::string arguments;
+ std::string description;
+ arguments += command->GetName();
+
+ if (command->GetReadOnlyGlobalCommandArgument())
+ {
+ arguments += ' ';
+ arguments += command->GetReadOnlyGlobalCommandArgument();
+ }
+
+ size_t argumentsCount = command->GetArgumentsCount();
+ for (size_t i = 0; i < argumentsCount; i++)
+ {
+ std::string arg;
+ bool isOptional = command->GetArgumentIsOptional(i);
+ if (isOptional)
+ {
+ arg += "[--";
+ }
+ arg += command->GetArgumentName(i);
+ if (isOptional)
+ {
+ arg += "]";
+ }
+ arguments += " ";
+ arguments += arg;
+
+ const char * argDescription = command->GetArgumentDescription(i);
+ if ((argDescription != nullptr) && (strlen(argDescription) > 0))
+ {
+ description += "\n";
+ description += arg;
+ description += ":\n ";
+ description += argDescription;
+ description += "\n";
+ }
+ }
+ fprintf(stderr, " %s %s %s\n", executable.c_str(), clusterName.c_str(), arguments.c_str());
+
+ if (command->GetHelpText())
+ {
+ fprintf(stderr, "\n%s\n", command->GetHelpText());
+ }
+
+ if (description.size() > 0)
+ {
+ fprintf(stderr, "%s\n", description.c_str());
+ }
+}
+
+bool Commands::DecodeArgumentsFromInteractiveMode(const char * command, std::vector<std::string> & args)
+{
+ // Remote clients may not know the ordering of arguments, so instead of a strict ordering arguments can
+ // be passed in as a json payload encoded in base64 and are reordered on the fly.
+ return IsJsonString(command) ? DecodeArgumentsFromBase64EncodedJson(command, args)
+ : DecodeArgumentsFromStringStream(command, args);
+}
+
+bool Commands::DecodeArgumentsFromBase64EncodedJson(const char * json, std::vector<std::string> & args)
+{
+ Json::Value jsonValue;
+ bool parsed = JsonParser::ParseCustomArgument(json, json + kJsonStringPrefixLen, jsonValue);
+ VerifyOrReturnValue(parsed, false, ChipLogError(NotSpecified, "Error while parsing json."));
+ VerifyOrReturnValue(jsonValue.isObject(), false, ChipLogError(NotSpecified, "Unexpected json type."));
+ VerifyOrReturnValue(jsonValue.isMember(kJsonClusterKey), false,
+ ChipLogError(NotSpecified, "'%s' key not found in json.", kJsonClusterKey));
+ VerifyOrReturnValue(jsonValue.isMember(kJsonCommandKey), false,
+ ChipLogError(NotSpecified, "'%s' key not found in json.", kJsonCommandKey));
+ VerifyOrReturnValue(jsonValue.isMember(kJsonArgumentsKey), false,
+ ChipLogError(NotSpecified, "'%s' key not found in json.", kJsonArgumentsKey));
+ VerifyOrReturnValue(IsBase64String(jsonValue[kJsonArgumentsKey].asString().c_str()), false,
+ ChipLogError(NotSpecified, "'arguments' is not a base64 string."));
+
+ auto clusterName = jsonValue[kJsonClusterKey].asString();
+ auto commandName = jsonValue[kJsonCommandKey].asString();
+ auto arguments = jsonValue[kJsonArgumentsKey].asString();
+
+ auto clusterIter = GetCommandSet(clusterName);
+ VerifyOrReturnValue(clusterIter != mCommandSets.end(), false,
+ ChipLogError(NotSpecified, "Cluster '%s' is not supported.", clusterName.c_str()));
+
+ auto & commandList = clusterIter->second.commands;
+
+ auto command = GetCommand(commandList, commandName);
+
+ if (jsonValue.isMember(kJsonCommandSpecifierKey) && IsGlobalCommand(commandName))
+ {
+ auto commandSpecifierName = jsonValue[kJsonCommandSpecifierKey].asString();
+ command = GetGlobalCommand(commandList, commandName, commandSpecifierName);
+ }
+ VerifyOrReturnValue(nullptr != command, false, ChipLogError(NotSpecified, "Unknown command."));
+
+ auto encodedData = arguments.c_str();
+ encodedData += kBase64StringPrefixLen;
+
+ size_t encodedDataSize = strlen(encodedData);
+ size_t expectedMaxDecodedSize = BASE64_MAX_DECODED_LEN(encodedDataSize);
+
+ chip::Platform::ScopedMemoryBuffer<uint8_t> decodedData;
+ VerifyOrReturnValue(decodedData.Calloc(expectedMaxDecodedSize + 1 /* for null */), false);
+
+ size_t decodedDataSize = chip::Base64Decode(encodedData, static_cast<uint16_t>(encodedDataSize), decodedData.Get());
+ VerifyOrReturnValue(decodedDataSize != 0, false, ChipLogError(NotSpecified, "Error while decoding base64 data."));
+
+ decodedData.Get()[decodedDataSize] = '\0';
+
+ Json::Value jsonArguments;
+ bool parsedArguments = JsonParser::ParseCustomArgument(encodedData, chip::Uint8::to_char(decodedData.Get()), jsonArguments);
+ VerifyOrReturnValue(parsedArguments, false, ChipLogError(NotSpecified, "Error while parsing json."));
+ VerifyOrReturnValue(jsonArguments.isObject(), false, ChipLogError(NotSpecified, "Unexpected json type, expects and object."));
+
+ std::vector<std::string> mandatoryArguments;
+ std::vector<std::string> optionalArguments;
+ VerifyOrReturnValue(GetArgumentsFromJson(command, jsonArguments, false /* addOptional */, mandatoryArguments), false);
+ VerifyOrReturnValue(GetArgumentsFromJson(command, jsonArguments, true /* addOptional */, optionalArguments), false);
+
+ args.push_back(std::move(clusterName));
+ args.push_back(std::move(commandName));
+ if (jsonValue.isMember(kJsonCommandSpecifierKey))
+ {
+ auto commandSpecifierName = jsonValue[kJsonCommandSpecifierKey].asString();
+ args.push_back(std::move(commandSpecifierName));
+ }
+ args.insert(args.end(), mandatoryArguments.begin(), mandatoryArguments.end());
+ args.insert(args.end(), optionalArguments.begin(), optionalArguments.end());
+
+ return true;
+}
+
+bool Commands::DecodeArgumentsFromStringStream(const char * command, std::vector<std::string> & args)
+{
+ std::string arg;
+ std::stringstream ss(command);
+ while (ss >> std::quoted(arg, '\''))
+ {
+ args.push_back(std::move(arg));
+ }
+
+ return true;
+}
+
+void Commands::ShowHelpText(const char * helpText)
+{
+ if (helpText == nullptr)
+ {
+ return;
+ }
+
+ // We leave 82 chars for command/cluster names. The help text starts
+ // two chars further to the right, so there are 80 chars left
+ // for it.
+ if (strlen(helpText) > 80)
+ {
+ // Add "..." at the end to indicate truncation, and only
+ // show the first 77 chars, since that's what will fit.
+ fprintf(stderr, " | - %.77s...|\n", helpText);
+ }
+ else
+ {
+ fprintf(stderr, " | - %-80s|\n", helpText);
+ }
+}
diff --git a/examples/fabric-admin/commands/common/Commands.h b/examples/fabric-admin/commands/common/Commands.h
new file mode 100644
index 0000000..8638ede
--- /dev/null
+++ b/examples/fabric-admin/commands/common/Commands.h
@@ -0,0 +1,90 @@
+/*
+ * 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
+
+#ifdef CONFIG_USE_LOCAL_STORAGE
+#include <controller/ExamplePersistentStorage.h>
+#endif // CONFIG_USE_LOCAL_STORAGE
+
+#include "Command.h"
+#include <map>
+#include <string>
+
+class Commands
+{
+public:
+ using CommandsVector = ::std::vector<std::unique_ptr<Command>>;
+
+ void RegisterCluster(const char * clusterName, commands_list commandsList)
+ {
+ Register(clusterName, commandsList, nullptr, true);
+ }
+ // Command sets represent fabric-admin functionality that is not actually
+ // XML-defined clusters. All command sets should have help text explaining
+ // what sort of commands one should expect to find in the set.
+ void RegisterCommandSet(const char * commandSetName, commands_list commandsList, const char * helpText)
+ {
+ Register(commandSetName, commandsList, helpText, false);
+ }
+ int Run(int argc, char ** argv);
+ int RunInteractive(const char * command, const chip::Optional<char *> & storageDirectory, bool advertiseOperational);
+
+private:
+ struct CommandSet
+ {
+ CommandsVector commands;
+ bool isCluster = false;
+ const char * helpText = nullptr;
+ };
+ // The tuple contains the commands, whether it's a synthetic cluster, and
+ // the help text for the cluster (which may be null).
+ using CommandSetMap = std::map<std::string, CommandSet>;
+
+ CHIP_ERROR RunCommand(int argc, char ** argv, bool interactive = false,
+ const chip::Optional<char *> & interactiveStorageDirectory = chip::NullOptional,
+ bool interactiveAdvertiseOperational = false);
+
+ CommandSetMap::iterator GetCommandSet(std::string commandSetName);
+ Command * GetCommand(CommandsVector & commands, std::string commandName);
+ Command * GetGlobalCommand(CommandsVector & commands, std::string commandName, std::string attributeName);
+ bool IsAttributeCommand(std::string commandName) const;
+ bool IsEventCommand(std::string commandName) const;
+ bool IsGlobalCommand(std::string commandName) const;
+
+ void ShowCommandSets(std::string executable);
+ static void ShowCommandSetOverview(std::string commandSetName, const CommandSet & commandSet);
+ void ShowCommandSet(std::string executable, std::string commandSetName, CommandsVector & commands, const char * helpText);
+ void ShowClusterAttributes(std::string executable, std::string clusterName, std::string commandName, CommandsVector & commands);
+ void ShowClusterEvents(std::string executable, std::string clusterName, std::string commandName, CommandsVector & commands);
+ void ShowCommand(std::string executable, std::string clusterName, Command * command);
+
+ bool DecodeArgumentsFromInteractiveMode(const char * command, std::vector<std::string> & args);
+ bool DecodeArgumentsFromBase64EncodedJson(const char * encodedData, std::vector<std::string> & args);
+ bool DecodeArgumentsFromStringStream(const char * command, std::vector<std::string> & args);
+
+ // helpText may be null, in which case it's not shown.
+ static void ShowHelpText(const char * helpText);
+
+ void Register(const char * commandSetName, commands_list commandsList, const char * helpText, bool isCluster);
+
+ CommandSetMap mCommandSets;
+#ifdef CONFIG_USE_LOCAL_STORAGE
+ PersistentStorage mStorage;
+#endif // CONFIG_USE_LOCAL_STORAGE
+};
diff --git a/examples/fabric-admin/commands/common/CredentialIssuerCommands.h b/examples/fabric-admin/commands/common/CredentialIssuerCommands.h
new file mode 100644
index 0000000..c36948f
--- /dev/null
+++ b/examples/fabric-admin/commands/common/CredentialIssuerCommands.h
@@ -0,0 +1,117 @@
+/*
+ * Copyright (c) 2024 Project CHIP Authors
+ * All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+#pragma once
+
+#include <app/util/basic-types.h>
+#include <credentials/attestation_verifier/DeviceAttestationVerifier.h>
+#include <lib/core/CASEAuthTag.h>
+#include <lib/core/CHIPCore.h>
+#include <lib/core/CHIPPersistentStorageDelegate.h>
+#include <vector>
+
+namespace chip {
+namespace Controller {
+struct SetupParams;
+class OperationalCredentialsDelegate;
+} // namespace Controller
+} // namespace chip
+
+class CredentialIssuerCommands
+{
+public:
+ virtual ~CredentialIssuerCommands() {}
+
+ /**
+ * @brief
+ * This function is used to initialize the Credentials Issuer, if needed.
+ *
+ * @param[in] storage A reference to the storage, where the Credentials Issuer can optionally use to access the keypair in
+ * storage.
+ *
+ * @return CHIP_ERROR CHIP_NO_ERROR on success, or corresponding error code.
+ */
+ virtual CHIP_ERROR InitializeCredentialsIssuer(chip::PersistentStorageDelegate & storage) = 0;
+
+ /**
+ * @brief
+ * This function is used to setup Device Attestation Singletons and intialize Setup/Commissioning Parameters with a custom
+ * Device Attestation Verifier object.
+ *
+ * @param[in] setupParams A reference to the Setup/Commissioning Parameters, to be initialized with custom Device Attestation
+ * Verifier.
+ * @param[in] trustStore A pointer to the PAA trust store to use to find valid PAA roots.
+ *
+ * @return CHIP_ERROR CHIP_NO_ERROR on success, or corresponding error code.
+ */
+ virtual CHIP_ERROR SetupDeviceAttestation(chip::Controller::SetupParams & setupParams,
+ const chip::Credentials::AttestationTrustStore * trustStore) = 0;
+
+ /**
+ * @brief Add a list of additional non-default CD verifying keys (by certificate)
+ *
+ * Must be called AFTER SetupDeviceAttestation.
+ *
+ * @param additionalCdCerts - vector of X.509 DER verifying cert bodies
+ * @return CHIP_NO_ERROR on succes, another CHIP_ERROR on internal failures.
+ */
+ virtual CHIP_ERROR AddAdditionalCDVerifyingCerts(const std::vector<std::vector<uint8_t>> & additionalCdCerts) = 0;
+
+ virtual chip::Controller::OperationalCredentialsDelegate * GetCredentialIssuer() = 0;
+
+ virtual void SetCredentialIssuerCATValues(chip::CATValues cats) = 0;
+
+ /**
+ * @brief
+ * This function is used to Generate NOC Chain for the Controller/Commissioner. Parameters follow the example implementation,
+ * so some parameters may not translate to the real remote Credentials Issuer policy.
+ *
+ * @param[in] nodeId The desired NodeId for the generated NOC Chain - May be optional/unused in some implementations.
+ * @param[in] fabricId The desired FabricId for the generated NOC Chain - May be optional/unused in some implementations.
+ * @param[in] cats The desired CATs for the generated NOC Chain - May be optional/unused in some implementations.
+ * @param[in] keypair The desired Keypair for the generated NOC Chain - May be optional/unused in some implementations.
+ * @param[in,out] rcac Buffer to hold the Root Certificate of the generated NOC Chain.
+ * @param[in,out] icac Buffer to hold the Intermediate Certificate of the generated NOC Chain.
+ * @param[in,out] noc Buffer to hold the Leaf Certificate of the generated NOC Chain.
+ *
+ * @return CHIP_ERROR CHIP_NO_ERROR on success, or corresponding error code.
+ */
+ virtual CHIP_ERROR GenerateControllerNOCChain(chip::NodeId nodeId, chip::FabricId fabricId, const chip::CATValues & cats,
+ chip::Crypto::P256Keypair & keypair, chip::MutableByteSpan & rcac,
+ chip::MutableByteSpan & icac, chip::MutableByteSpan & noc) = 0;
+
+ // All options must start false
+ enum CredentialIssuerOptions : uint8_t
+ {
+ kMaximizeCertificateSizes = 0, // If set, certificate chains will be maximized for testing via padding
+ kAllowTestCdSigningKey = 1, // If set, allow development/test SDK CD verifying key to be used
+ };
+
+ virtual void SetCredentialIssuerOption(CredentialIssuerOptions option, bool isEnabled)
+ {
+ // Do nothing
+ (void) option;
+ (void) isEnabled;
+ }
+
+ virtual bool GetCredentialIssuerOption(CredentialIssuerOptions option)
+ {
+ // All options always start false
+ return false;
+ }
+};
diff --git a/examples/fabric-admin/commands/common/CustomStringPrefix.h b/examples/fabric-admin/commands/common/CustomStringPrefix.h
new file mode 100644
index 0000000..be84429
--- /dev/null
+++ b/examples/fabric-admin/commands/common/CustomStringPrefix.h
@@ -0,0 +1,55 @@
+/*
+ * Copyright (c) 2024 Project CHIP Authors
+ * All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+#pragma once
+
+#include <string.h>
+
+#include <lib/support/CodeUtils.h>
+
+static constexpr char kJsonStringPrefix[] = "json:";
+inline constexpr size_t kJsonStringPrefixLen = ArraySize(kJsonStringPrefix) - 1; // Don't count the null
+
+static constexpr char kBase64StringPrefix[] = "base64:";
+inline constexpr size_t kBase64StringPrefixLen = ArraySize(kBase64StringPrefix) - 1; // Don't count the null
+
+static constexpr char kHexStringPrefix[] = "hex:";
+inline constexpr size_t kHexStringPrefixLen = ArraySize(kHexStringPrefix) - 1; // Don't count the null
+
+static constexpr char kStrStringPrefix[] = "str:";
+inline constexpr size_t kStrStringPrefixLen = ArraySize(kStrStringPrefix) - 1; // Don't count the null
+
+inline bool IsJsonString(const char * str)
+{
+ return strncmp(str, kJsonStringPrefix, kJsonStringPrefixLen) == 0;
+}
+
+inline bool IsBase64String(const char * str)
+{
+ return strncmp(str, kBase64StringPrefix, kBase64StringPrefixLen) == 0;
+}
+
+inline bool IsHexString(const char * str)
+{
+ return strncmp(str, kHexStringPrefix, kHexStringPrefixLen) == 0;
+}
+
+inline bool IsStrString(const char * str)
+{
+ return strncmp(str, kStrStringPrefix, kStrStringPrefixLen) == 0;
+}
diff --git a/examples/fabric-admin/commands/common/DeviceScanner.cpp b/examples/fabric-admin/commands/common/DeviceScanner.cpp
new file mode 100644
index 0000000..e49eb85
--- /dev/null
+++ b/examples/fabric-admin/commands/common/DeviceScanner.cpp
@@ -0,0 +1,245 @@
+/*
+ * 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 "DeviceScanner.h"
+
+using namespace chip;
+using namespace chip::Dnssd;
+
+#if CONFIG_NETWORK_LAYER_BLE
+using namespace chip::Ble;
+constexpr char kBleKey[] = "BLE";
+#endif // CONFIG_NETWORK_LAYER_BLE
+
+CHIP_ERROR DeviceScanner::Start()
+{
+ mDiscoveredResults.clear();
+
+#if CONFIG_NETWORK_LAYER_BLE
+ ReturnErrorOnFailure(DeviceLayer::PlatformMgrImpl().StartBleScan(this));
+#endif // CONFIG_NETWORK_LAYER_BLE
+
+ ReturnErrorOnFailure(chip::Dnssd::Resolver::Instance().Init(DeviceLayer::UDPEndPointManager()));
+
+ char serviceName[kMaxCommissionableServiceNameSize];
+ auto filter = DiscoveryFilterType::kNone;
+ ReturnErrorOnFailure(MakeServiceTypeName(serviceName, sizeof(serviceName), filter, DiscoveryType::kCommissionableNode));
+
+ return ChipDnssdBrowse(serviceName, DnssdServiceProtocol::kDnssdProtocolUdp, Inet::IPAddressType::kAny,
+ Inet::InterfaceId::Null(), this);
+}
+
+CHIP_ERROR DeviceScanner::Stop()
+{
+#if CONFIG_NETWORK_LAYER_BLE
+ ReturnErrorOnFailure(DeviceLayer::PlatformMgrImpl().StopBleScan());
+#endif // CONFIG_NETWORK_LAYER_BLE
+
+ return ChipDnssdStopBrowse(this);
+}
+
+void DeviceScanner::OnNodeDiscovered(const DiscoveredNodeData & nodeData)
+{
+ VerifyOrReturn(nodeData.Is<CommissionNodeData>());
+ auto & commissionData = nodeData.Get<CommissionNodeData>();
+
+ auto discriminator = commissionData.longDiscriminator;
+ auto vendorId = static_cast<VendorId>(commissionData.vendorId);
+ auto productId = commissionData.productId;
+
+ ChipLogProgress(NotSpecified, "OnNodeDiscovered (MDNS): discriminator: %u, vendorId: %u, productId: %u", discriminator,
+ vendorId, productId);
+
+ const CommonResolutionData & resolutionData = commissionData;
+
+ auto & instanceData = mDiscoveredResults[commissionData.instanceName];
+ auto & interfaceData = instanceData[resolutionData.interfaceId.GetPlatformInterface()];
+
+ for (size_t i = 0; i < resolutionData.numIPs; i++)
+ {
+ auto params = Controller::SetUpCodePairerParameters(resolutionData, i);
+ DeviceScannerResult result = { params, vendorId, productId, discriminator, chip::MakeOptional(resolutionData) };
+ interfaceData.push_back(result);
+ }
+
+ commissionData.LogDetail();
+}
+
+void DeviceScanner::OnBrowseAdd(chip::Dnssd::DnssdService service)
+{
+ ChipLogProgress(NotSpecified, "OnBrowseAdd: %s", service.mName);
+ LogErrorOnFailure(ChipDnssdResolve(&service, service.mInterface, this));
+
+ auto & instanceData = mDiscoveredResults[service.mName];
+ auto & interfaceData = instanceData[service.mInterface.GetPlatformInterface()];
+ (void) interfaceData;
+}
+
+void DeviceScanner::OnBrowseRemove(chip::Dnssd::DnssdService service)
+{
+ ChipLogProgress(NotSpecified, "OnBrowseRemove: %s", service.mName);
+ auto & instanceData = mDiscoveredResults[service.mName];
+ auto & interfaceData = instanceData[service.mInterface.GetPlatformInterface()];
+
+ // Check if the interface data has been resolved already, otherwise, just inform the
+ // back end that we may not need it anymore.
+ if (interfaceData.size() == 0)
+ {
+ ChipDnssdResolveNoLongerNeeded(service.mName);
+ }
+
+ // Delete the interface placeholder.
+ instanceData.erase(service.mInterface.GetPlatformInterface());
+
+ // If there is nothing else to resolve for the given instance name, just remove it
+ // too.
+ if (instanceData.size() == 0)
+ {
+ mDiscoveredResults.erase(service.mName);
+ }
+}
+
+void DeviceScanner::OnBrowseStop(CHIP_ERROR error)
+{
+ ChipLogProgress(NotSpecified, "OnBrowseStop: %" CHIP_ERROR_FORMAT, error.Format());
+
+ for (auto & instance : mDiscoveredResults)
+ {
+ for (auto & interface : instance.second)
+ {
+ if (interface.second.size() == 0)
+ {
+ ChipDnssdResolveNoLongerNeeded(instance.first.c_str());
+ }
+ }
+ }
+}
+
+#if CONFIG_NETWORK_LAYER_BLE
+void DeviceScanner::OnBleScanAdd(BLE_CONNECTION_OBJECT connObj, const ChipBLEDeviceIdentificationInfo & info)
+{
+ auto discriminator = info.GetDeviceDiscriminator();
+ auto vendorId = static_cast<VendorId>(info.GetVendorId());
+ auto productId = info.GetProductId();
+
+ ChipLogProgress(NotSpecified, "OnBleScanAdd (BLE): %p, discriminator: %u, vendorId: %u, productId: %u", connObj, discriminator,
+ vendorId, productId);
+
+ auto params = Controller::SetUpCodePairerParameters(connObj, false /* connected */);
+ DeviceScannerResult result = { params, vendorId, productId, discriminator };
+
+ auto & instanceData = mDiscoveredResults[kBleKey];
+ auto & interfaceData = instanceData[chip::Inet::InterfaceId::Null().GetPlatformInterface()];
+ interfaceData.push_back(result);
+}
+
+void DeviceScanner::OnBleScanRemove(BLE_CONNECTION_OBJECT connObj)
+{
+ ChipLogProgress(NotSpecified, "OnBleScanRemove: %p", connObj);
+
+ auto & instanceData = mDiscoveredResults[kBleKey];
+ auto & interfaceData = instanceData[chip::Inet::InterfaceId::Null().GetPlatformInterface()];
+
+ interfaceData.erase(std::remove_if(interfaceData.begin(), interfaceData.end(),
+ [connObj](const DeviceScannerResult & result) {
+ return result.mParams.HasDiscoveredObject() &&
+ result.mParams.GetDiscoveredObject() == connObj;
+ }),
+ interfaceData.end());
+
+ if (interfaceData.size() == 0)
+ {
+ instanceData.clear();
+ mDiscoveredResults.erase(kBleKey);
+ }
+}
+#endif // CONFIG_NETWORK_LAYER_BLE
+
+CHIP_ERROR DeviceScanner::Get(uint16_t index, RendezvousParameters & params)
+{
+ uint16_t currentIndex = 0;
+ for (auto & instance : mDiscoveredResults)
+ {
+ for (auto & interface : instance.second)
+ {
+ for (auto & result : interface.second)
+ {
+ if (currentIndex == index)
+ {
+ params = result.mParams;
+ return CHIP_NO_ERROR;
+ }
+ currentIndex++;
+ }
+ }
+ }
+
+ return CHIP_ERROR_NOT_FOUND;
+}
+
+CHIP_ERROR DeviceScanner::Get(uint16_t index, Dnssd::CommonResolutionData & resolutionData)
+{
+ uint16_t currentIndex = 0;
+ for (auto & instance : mDiscoveredResults)
+ {
+ for (auto & interface : instance.second)
+ {
+ for (auto & result : interface.second)
+ {
+ if (currentIndex == index && result.mResolutionData.HasValue())
+ {
+ resolutionData = result.mResolutionData.Value();
+ return CHIP_NO_ERROR;
+ }
+ currentIndex++;
+ }
+ }
+ }
+
+ return CHIP_ERROR_NOT_FOUND;
+}
+
+void DeviceScanner::Log() const
+{
+ auto resultsCount = mDiscoveredResults.size();
+ VerifyOrReturn(resultsCount > 0, ChipLogProgress(NotSpecified, "No device discovered."));
+
+ [[maybe_unused]] uint16_t index = 0;
+ for (auto & instance : mDiscoveredResults)
+ {
+ ChipLogProgress(NotSpecified, "Instance Name: %s ", instance.first.c_str());
+ for (auto & interface : instance.second)
+ {
+ for (auto & result : interface.second)
+ {
+ char addr[Transport::PeerAddress::kMaxToStringSize];
+ result.mParams.GetPeerAddress().ToString(addr);
+
+ ChipLogProgress(NotSpecified, "\t %u - Discriminator: %u - Vendor: %u - Product: %u - %s", index,
+ result.mDiscriminator, result.mVendorId, result.mProductId, addr);
+ index++;
+ }
+ }
+ }
+}
+
+DeviceScanner & GetDeviceScanner()
+{
+ static DeviceScanner scanner;
+ return scanner;
+}
diff --git a/examples/fabric-admin/commands/common/DeviceScanner.h b/examples/fabric-admin/commands/common/DeviceScanner.h
new file mode 100644
index 0000000..c3e81a5
--- /dev/null
+++ b/examples/fabric-admin/commands/common/DeviceScanner.h
@@ -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.
+ *
+ */
+
+#pragma once
+
+#include <platform/CHIPDeviceConfig.h>
+
+#if CHIP_DEVICE_LAYER_TARGET_DARWIN
+
+#include <controller/CHIPDeviceController.h>
+#include <lib/dnssd/platform/Dnssd.h>
+
+#include <string>
+#include <unordered_map>
+#include <vector>
+
+#if CONFIG_NETWORK_LAYER_BLE
+#include <platform/Darwin/BleScannerDelegate.h>
+#endif // CONFIG_NETWORK_LAYER_BLE
+
+struct DeviceScannerResult
+{
+ chip::Controller::SetUpCodePairerParameters mParams;
+ chip::VendorId mVendorId;
+ uint16_t mProductId;
+ uint16_t mDiscriminator;
+ chip::Optional<chip::Dnssd::CommonResolutionData> mResolutionData;
+};
+
+class DeviceScanner : public chip::Dnssd::DiscoverNodeDelegate,
+ public chip::Dnssd::DnssdBrowseDelegate
+#if CONFIG_NETWORK_LAYER_BLE
+ ,
+ public chip::DeviceLayer::BleScannerDelegate
+#endif // CONFIG_NETWORK_LAYER_BLE
+{
+public:
+ CHIP_ERROR Start();
+ CHIP_ERROR Stop();
+ CHIP_ERROR Get(uint16_t index, chip::RendezvousParameters & params);
+ CHIP_ERROR Get(uint16_t index, chip::Dnssd::CommonResolutionData & resolutionData);
+ void Log() const;
+
+ /////////// DiscoverNodeDelegate Interface /////////
+ void OnNodeDiscovered(const chip::Dnssd::DiscoveredNodeData & nodeData) override;
+
+ /////////// DnssdBrowseDelegate Interface /////////
+ void OnBrowseAdd(chip::Dnssd::DnssdService service) override;
+ void OnBrowseRemove(chip::Dnssd::DnssdService service) override;
+ void OnBrowseStop(CHIP_ERROR error) override;
+
+#if CONFIG_NETWORK_LAYER_BLE
+ /////////// BleScannerDelegate Interface /////////
+ void OnBleScanAdd(BLE_CONNECTION_OBJECT connObj, const chip::Ble::ChipBLEDeviceIdentificationInfo & info) override;
+ void OnBleScanRemove(BLE_CONNECTION_OBJECT connObj) override;
+#endif // CONFIG_NETWORK_LAYER_BLE
+
+private:
+ std::unordered_map<std::string, std::map<chip::Inet::InterfaceId::PlatformType, std::vector<DeviceScannerResult>>>
+ mDiscoveredResults;
+};
+
+DeviceScanner & GetDeviceScanner();
+
+#endif // CHIP_DEVICE_LAYER_TARGET_DARWIN
diff --git a/examples/fabric-admin/commands/common/HexConversion.h b/examples/fabric-admin/commands/common/HexConversion.h
new file mode 100644
index 0000000..99b7f65
--- /dev/null
+++ b/examples/fabric-admin/commands/common/HexConversion.h
@@ -0,0 +1,65 @@
+/*
+ * 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 <lib/core/CHIPError.h>
+#include <lib/support/BytesToHex.h>
+#include <lib/support/Span.h>
+#include <lib/support/logging/CHIPLogging.h>
+
+/**
+ * Utility for converting a hex string to bytes, with the right error checking
+ * and allocation size computation.
+ *
+ * Takes a functor to allocate the buffer to use for the hex bytes. The functor
+ * is expected to return uint8_t *. The caller is responsible for cleaning up
+ * this buffer as needed.
+ *
+ * On success, *octetCount is filled with the number of octets placed in the
+ * buffer. On failure, the value of *octetCount is undefined.
+ */
+template <typename F>
+CHIP_ERROR HexToBytes(chip::CharSpan hex, F bufferAllocator, size_t * octetCount)
+{
+ *octetCount = 0;
+
+ if (hex.size() % 2 != 0)
+ {
+ ChipLogError(NotSpecified, "Error while encoding '%.*s' as an octet string: Odd number of characters.",
+ static_cast<int>(hex.size()), hex.data());
+ return CHIP_ERROR_INVALID_STRING_LENGTH;
+ }
+
+ const size_t bufferSize = hex.size() / 2;
+ uint8_t * buffer = bufferAllocator(bufferSize);
+ if (buffer == nullptr && bufferSize != 0)
+ {
+ ChipLogError(NotSpecified, "Failed to allocate buffer of size: %llu", static_cast<unsigned long long>(bufferSize));
+ return CHIP_ERROR_NO_MEMORY;
+ }
+
+ size_t byteCount = chip::Encoding::HexToBytes(hex.data(), hex.size(), buffer, bufferSize);
+ if (byteCount == 0 && hex.size() != 0)
+ {
+ ChipLogError(NotSpecified, "Error while encoding '%.*s' as an octet string.", static_cast<int>(hex.size()), hex.data());
+ return CHIP_ERROR_INTERNAL;
+ }
+
+ *octetCount = byteCount;
+ return CHIP_NO_ERROR;
+}
diff --git a/examples/fabric-admin/commands/common/RemoteDataModelLogger.cpp b/examples/fabric-admin/commands/common/RemoteDataModelLogger.cpp
new file mode 100644
index 0000000..c8e9d5a
--- /dev/null
+++ b/examples/fabric-admin/commands/common/RemoteDataModelLogger.cpp
@@ -0,0 +1,279 @@
+/*
+ * 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 "RemoteDataModelLogger.h"
+
+#include <lib/support/SafeInt.h>
+#include <lib/support/jsontlv/TlvJson.h>
+
+constexpr char kEventNumberKey[] = "eventNumber";
+constexpr char kDataVersionKey[] = "dataVersion";
+constexpr char kClusterIdKey[] = "clusterId";
+constexpr char kEndpointIdKey[] = "endpointId";
+constexpr char kAttributeIdKey[] = "attributeId";
+constexpr char kEventIdKey[] = "eventId";
+constexpr char kCommandIdKey[] = "commandId";
+constexpr char kErrorIdKey[] = "error";
+constexpr char kClusterErrorIdKey[] = "clusterError";
+constexpr char kValueKey[] = "value";
+constexpr char kNodeIdKey[] = "nodeId";
+constexpr char kNOCKey[] = "NOC";
+constexpr char kICACKey[] = "ICAC";
+constexpr char kRCACKey[] = "RCAC";
+constexpr char kIPKKey[] = "IPK";
+
+namespace {
+RemoteDataModelLoggerDelegate * gDelegate;
+
+CHIP_ERROR LogError(Json::Value & value, const chip::app::StatusIB & status)
+{
+ if (status.mClusterStatus.HasValue())
+ {
+ auto statusValue = status.mClusterStatus.Value();
+ value[kClusterErrorIdKey] = statusValue;
+ }
+
+#if CHIP_CONFIG_IM_STATUS_CODE_VERBOSE_FORMAT
+ auto statusName = chip::Protocols::InteractionModel::StatusName(status.mStatus);
+ value[kErrorIdKey] = statusName;
+#else
+ auto statusName = status.mStatus;
+ value[kErrorIdKey] = chip::to_underlying(statusName);
+#endif // CHIP_CONFIG_IM_STATUS_CODE_VERBOSE_FORMAT
+
+ auto valueStr = chip::JsonToString(value);
+ return gDelegate->LogJSON(valueStr.c_str());
+}
+
+} // namespace
+
+namespace RemoteDataModelLogger {
+CHIP_ERROR LogAttributeAsJSON(const chip::app::ConcreteDataAttributePath & path, chip::TLV::TLVReader * data)
+{
+ VerifyOrReturnError(gDelegate != nullptr, CHIP_NO_ERROR);
+
+ Json::Value value;
+ value[kClusterIdKey] = path.mClusterId;
+ value[kEndpointIdKey] = path.mEndpointId;
+ value[kAttributeIdKey] = path.mAttributeId;
+ if (path.mDataVersion.HasValue())
+ {
+ value[kDataVersionKey] = path.mDataVersion.Value();
+ }
+
+ chip::TLV::TLVReader reader;
+ reader.Init(*data);
+ ReturnErrorOnFailure(chip::TlvToJson(reader, value));
+
+ auto valueStr = chip::JsonToString(value);
+ return gDelegate->LogJSON(valueStr.c_str());
+}
+
+CHIP_ERROR LogErrorAsJSON(const chip::app::ConcreteDataAttributePath & path, const chip::app::StatusIB & status)
+{
+ VerifyOrReturnError(gDelegate != nullptr, CHIP_NO_ERROR);
+
+ Json::Value value;
+ value[kClusterIdKey] = path.mClusterId;
+ value[kEndpointIdKey] = path.mEndpointId;
+ value[kAttributeIdKey] = path.mAttributeId;
+
+ return LogError(value, status);
+}
+
+CHIP_ERROR LogCommandAsJSON(const chip::app::ConcreteCommandPath & path, chip::TLV::TLVReader * data)
+{
+ VerifyOrReturnError(gDelegate != nullptr, CHIP_NO_ERROR);
+
+ Json::Value value;
+ value[kClusterIdKey] = path.mClusterId;
+ value[kEndpointIdKey] = path.mEndpointId;
+ value[kCommandIdKey] = path.mCommandId;
+
+ chip::TLV::TLVReader reader;
+ reader.Init(*data);
+ ReturnErrorOnFailure(chip::TlvToJson(reader, value));
+
+ auto valueStr = chip::JsonToString(value);
+ return gDelegate->LogJSON(valueStr.c_str());
+}
+
+CHIP_ERROR LogErrorAsJSON(const chip::app::ConcreteCommandPath & path, const chip::app::StatusIB & status)
+{
+ VerifyOrReturnError(gDelegate != nullptr, CHIP_NO_ERROR);
+
+ Json::Value value;
+ value[kClusterIdKey] = path.mClusterId;
+ value[kEndpointIdKey] = path.mEndpointId;
+ value[kCommandIdKey] = path.mCommandId;
+
+ return LogError(value, status);
+}
+
+CHIP_ERROR LogEventAsJSON(const chip::app::EventHeader & header, chip::TLV::TLVReader * data)
+{
+ VerifyOrReturnError(gDelegate != nullptr, CHIP_NO_ERROR);
+
+ Json::Value value;
+ value[kClusterIdKey] = header.mPath.mClusterId;
+ value[kEndpointIdKey] = header.mPath.mEndpointId;
+ value[kEventIdKey] = header.mPath.mEventId;
+ value[kEventNumberKey] = header.mEventNumber;
+
+ chip::TLV::TLVReader reader;
+ reader.Init(*data);
+ ReturnErrorOnFailure(chip::TlvToJson(reader, value));
+
+ auto valueStr = chip::JsonToString(value);
+ return gDelegate->LogJSON(valueStr.c_str());
+}
+
+CHIP_ERROR LogErrorAsJSON(const chip::app::EventHeader & header, const chip::app::StatusIB & status)
+{
+ VerifyOrReturnError(gDelegate != nullptr, CHIP_NO_ERROR);
+
+ Json::Value value;
+ value[kClusterIdKey] = header.mPath.mClusterId;
+ value[kEndpointIdKey] = header.mPath.mEndpointId;
+ value[kEventIdKey] = header.mPath.mEventId;
+
+ return LogError(value, status);
+}
+
+CHIP_ERROR LogErrorAsJSON(const CHIP_ERROR & error)
+{
+ VerifyOrReturnError(gDelegate != nullptr, CHIP_NO_ERROR);
+
+ Json::Value value;
+ chip::app::StatusIB status;
+ status.InitFromChipError(error);
+ return LogError(value, status);
+}
+
+CHIP_ERROR LogGetCommissionerNodeId(chip::NodeId value)
+{
+ VerifyOrReturnError(gDelegate != nullptr, CHIP_NO_ERROR);
+
+ Json::Value rootValue;
+ rootValue[kValueKey] = Json::Value();
+ rootValue[kValueKey][kNodeIdKey] = value;
+
+ auto valueStr = chip::JsonToString(rootValue);
+ return gDelegate->LogJSON(valueStr.c_str());
+}
+
+CHIP_ERROR LogGetCommissionerRootCertificate(const char * value)
+{
+ VerifyOrReturnError(gDelegate != nullptr, CHIP_NO_ERROR);
+
+ Json::Value rootValue;
+ rootValue[kValueKey] = Json::Value();
+ rootValue[kValueKey][kRCACKey] = value;
+
+ auto valueStr = chip::JsonToString(rootValue);
+ return gDelegate->LogJSON(valueStr.c_str());
+}
+
+CHIP_ERROR LogIssueNOCChain(const char * noc, const char * icac, const char * rcac, const char * ipk)
+{
+ VerifyOrReturnError(gDelegate != nullptr, CHIP_NO_ERROR);
+
+ Json::Value rootValue;
+ rootValue[kValueKey] = Json::Value();
+ rootValue[kValueKey][kNOCKey] = noc;
+ rootValue[kValueKey][kICACKey] = icac;
+ rootValue[kValueKey][kRCACKey] = rcac;
+ rootValue[kValueKey][kIPKKey] = ipk;
+
+ auto valueStr = chip::JsonToString(rootValue);
+ return gDelegate->LogJSON(valueStr.c_str());
+}
+
+CHIP_ERROR LogDiscoveredNodeData(const chip::Dnssd::CommissionNodeData & nodeData)
+{
+ VerifyOrReturnError(gDelegate != nullptr, CHIP_NO_ERROR);
+
+ auto & commissionData = nodeData;
+ auto & resolutionData = commissionData;
+
+ if (!chip::CanCastTo<uint8_t>(resolutionData.numIPs))
+ {
+ ChipLogError(NotSpecified, "Too many ips.");
+ return CHIP_ERROR_INVALID_ARGUMENT;
+ }
+
+ if (!chip::CanCastTo<uint64_t>(commissionData.rotatingIdLen))
+ {
+ ChipLogError(NotSpecified, "Can not convert rotatingId to json format.");
+ return CHIP_ERROR_INVALID_ARGUMENT;
+ }
+
+ char rotatingId[chip::Dnssd::kMaxRotatingIdLen * 2 + 1] = "";
+ ReturnErrorOnFailure(chip::Encoding::BytesToUppercaseHexString(commissionData.rotatingId, commissionData.rotatingIdLen,
+ rotatingId, sizeof(rotatingId)));
+
+ Json::Value value;
+ value["hostName"] = resolutionData.hostName;
+ value["instanceName"] = commissionData.instanceName;
+ value["longDiscriminator"] = commissionData.longDiscriminator;
+ value["shortDiscriminator"] = ((commissionData.longDiscriminator >> 8) & 0x0F);
+ value["vendorId"] = commissionData.vendorId;
+ value["productId"] = commissionData.productId;
+ value["commissioningMode"] = commissionData.commissioningMode;
+ value["deviceType"] = commissionData.deviceType;
+ value["deviceName"] = commissionData.deviceName;
+ value["rotatingId"] = rotatingId;
+ value["rotatingIdLen"] = static_cast<uint64_t>(commissionData.rotatingIdLen);
+ value["pairingHint"] = commissionData.pairingHint;
+ value["pairingInstruction"] = commissionData.pairingInstruction;
+ value["supportsTcp"] = resolutionData.supportsTcp;
+ value["port"] = resolutionData.port;
+ value["numIPs"] = static_cast<uint8_t>(resolutionData.numIPs);
+
+ if (resolutionData.mrpRetryIntervalIdle.has_value())
+ {
+ value["mrpRetryIntervalIdle"] = resolutionData.mrpRetryIntervalIdle->count();
+ }
+
+ if (resolutionData.mrpRetryIntervalActive.has_value())
+ {
+ value["mrpRetryIntervalActive"] = resolutionData.mrpRetryIntervalActive->count();
+ }
+
+ if (resolutionData.mrpRetryActiveThreshold.has_value())
+ {
+ value["mrpRetryActiveThreshold"] = resolutionData.mrpRetryActiveThreshold->count();
+ }
+
+ if (resolutionData.isICDOperatingAsLIT.has_value())
+ {
+ value["isICDOperatingAsLIT"] = *(resolutionData.isICDOperatingAsLIT);
+ }
+
+ Json::Value rootValue;
+ rootValue[kValueKey] = value;
+
+ auto valueStr = chip::JsonToString(rootValue);
+ return gDelegate->LogJSON(valueStr.c_str());
+}
+
+void SetDelegate(RemoteDataModelLoggerDelegate * delegate)
+{
+ gDelegate = delegate;
+}
+}; // namespace RemoteDataModelLogger
diff --git a/examples/fabric-admin/commands/common/RemoteDataModelLogger.h b/examples/fabric-admin/commands/common/RemoteDataModelLogger.h
new file mode 100644
index 0000000..c31636e
--- /dev/null
+++ b/examples/fabric-admin/commands/common/RemoteDataModelLogger.h
@@ -0,0 +1,48 @@
+/*
+ * 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/ConcreteAttributePath.h>
+#include <app/ConcreteCommandPath.h>
+#include <app/EventHeader.h>
+#include <app/MessageDef/StatusIB.h>
+#include <crypto/CHIPCryptoPAL.h>
+#include <lib/dnssd/Resolver.h>
+
+class RemoteDataModelLoggerDelegate
+{
+public:
+ CHIP_ERROR virtual LogJSON(const char *) = 0;
+ virtual ~RemoteDataModelLoggerDelegate(){};
+};
+
+namespace RemoteDataModelLogger {
+CHIP_ERROR LogAttributeAsJSON(const chip::app::ConcreteDataAttributePath & path, chip::TLV::TLVReader * data);
+CHIP_ERROR LogErrorAsJSON(const chip::app::ConcreteDataAttributePath & path, const chip::app::StatusIB & status);
+CHIP_ERROR LogCommandAsJSON(const chip::app::ConcreteCommandPath & path, chip::TLV::TLVReader * data);
+CHIP_ERROR LogErrorAsJSON(const chip::app::ConcreteCommandPath & path, const chip::app::StatusIB & status);
+CHIP_ERROR LogEventAsJSON(const chip::app::EventHeader & header, chip::TLV::TLVReader * data);
+CHIP_ERROR LogErrorAsJSON(const chip::app::EventHeader & header, const chip::app::StatusIB & status);
+CHIP_ERROR LogErrorAsJSON(const CHIP_ERROR & error);
+CHIP_ERROR LogGetCommissionerNodeId(chip::NodeId value);
+CHIP_ERROR LogGetCommissionerRootCertificate(const char * value);
+CHIP_ERROR LogIssueNOCChain(const char * noc, const char * icac, const char * rcac, const char * ipk);
+CHIP_ERROR LogDiscoveredNodeData(const chip::Dnssd::CommissionNodeData & nodeData);
+void SetDelegate(RemoteDataModelLoggerDelegate * delegate);
+}; // namespace RemoteDataModelLogger
diff --git a/examples/fabric-admin/commands/example/ExampleCredentialIssuerCommands.h b/examples/fabric-admin/commands/example/ExampleCredentialIssuerCommands.h
new file mode 100644
index 0000000..a739ac7
--- /dev/null
+++ b/examples/fabric-admin/commands/example/ExampleCredentialIssuerCommands.h
@@ -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.
+ *
+ */
+
+#pragma once
+
+#include <commands/common/CredentialIssuerCommands.h>
+#include <controller/CHIPDeviceControllerFactory.h>
+#include <controller/ExampleOperationalCredentialsIssuer.h>
+#include <credentials/DeviceAttestationCredsProvider.h>
+#include <credentials/attestation_verifier/DefaultDeviceAttestationVerifier.h>
+#include <credentials/attestation_verifier/DeviceAttestationVerifier.h>
+#include <credentials/examples/DeviceAttestationCredsExample.h>
+
+class ExampleCredentialIssuerCommands : public CredentialIssuerCommands
+{
+public:
+ CHIP_ERROR InitializeCredentialsIssuer(chip::PersistentStorageDelegate & storage) override
+ {
+ return mOpCredsIssuer.Initialize(storage);
+ }
+ CHIP_ERROR SetupDeviceAttestation(chip::Controller::SetupParams & setupParams,
+ const chip::Credentials::AttestationTrustStore * trustStore) override
+ {
+ chip::Credentials::SetDeviceAttestationCredentialsProvider(chip::Credentials::Examples::GetExampleDACProvider());
+
+ mDacVerifier = chip::Credentials::GetDefaultDACVerifier(trustStore);
+ setupParams.deviceAttestationVerifier = mDacVerifier;
+ mDacVerifier->EnableCdTestKeySupport(mAllowTestCdSigningKey);
+
+ return CHIP_NO_ERROR;
+ }
+ chip::Controller::OperationalCredentialsDelegate * GetCredentialIssuer() override { return &mOpCredsIssuer; }
+ void SetCredentialIssuerCATValues(chip::CATValues cats) override { mOpCredsIssuer.SetCATValuesForNextNOCRequest(cats); }
+ CHIP_ERROR GenerateControllerNOCChain(chip::NodeId nodeId, chip::FabricId fabricId, const chip::CATValues & cats,
+ chip::Crypto::P256Keypair & keypair, chip::MutableByteSpan & rcac,
+ chip::MutableByteSpan & icac, chip::MutableByteSpan & noc) override
+ {
+ return mOpCredsIssuer.GenerateNOCChainAfterValidation(nodeId, fabricId, cats, keypair.Pubkey(), rcac, icac, noc);
+ }
+
+ CHIP_ERROR AddAdditionalCDVerifyingCerts(const std::vector<std::vector<uint8_t>> & additionalCdCerts) override
+ {
+ VerifyOrReturnError(mDacVerifier != nullptr, CHIP_ERROR_INCORRECT_STATE);
+
+ for (const auto & cert : additionalCdCerts)
+ {
+ auto cdTrustStore = mDacVerifier->GetCertificationDeclarationTrustStore();
+ VerifyOrReturnError(cdTrustStore != nullptr, CHIP_ERROR_INCORRECT_STATE);
+ ReturnErrorOnFailure(cdTrustStore->AddTrustedKey(chip::ByteSpan(cert.data(), cert.size())));
+ }
+
+ return CHIP_NO_ERROR;
+ }
+
+ void SetCredentialIssuerOption(CredentialIssuerOptions option, bool isEnabled) override
+ {
+ switch (option)
+ {
+ case CredentialIssuerOptions::kMaximizeCertificateSizes:
+ mUsesMaxSizedCerts = isEnabled;
+ mOpCredsIssuer.SetMaximallyLargeCertsUsed(mUsesMaxSizedCerts);
+ break;
+ case CredentialIssuerOptions::kAllowTestCdSigningKey:
+ mAllowTestCdSigningKey = isEnabled;
+ if (mDacVerifier != nullptr)
+ {
+ mDacVerifier->EnableCdTestKeySupport(isEnabled);
+ }
+ break;
+ default:
+ break;
+ }
+ }
+
+ bool GetCredentialIssuerOption(CredentialIssuerOptions option) override
+ {
+ switch (option)
+ {
+ case CredentialIssuerOptions::kMaximizeCertificateSizes:
+ return mUsesMaxSizedCerts;
+ case CredentialIssuerOptions::kAllowTestCdSigningKey:
+ return mAllowTestCdSigningKey;
+ default:
+ return false;
+ }
+ }
+
+protected:
+ bool mUsesMaxSizedCerts = false;
+ // Starts true for legacy purposes
+ bool mAllowTestCdSigningKey = true;
+
+private:
+ chip::Controller::ExampleOperationalCredentialsIssuer mOpCredsIssuer;
+ chip::Credentials::DeviceAttestationVerifier * mDacVerifier;
+};
diff --git a/examples/fabric-admin/commands/interactive/Commands.h b/examples/fabric-admin/commands/interactive/Commands.h
new file mode 100644
index 0000000..e324dda
--- /dev/null
+++ b/examples/fabric-admin/commands/interactive/Commands.h
@@ -0,0 +1,34 @@
+/*
+ * 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 "commands/common/CHIPCommand.h"
+#include "commands/common/Commands.h"
+#include "commands/interactive/InteractiveCommands.h"
+
+void registerCommandsInteractive(Commands & commands, CredentialIssuerCommands * credsIssuerConfig)
+{
+ const char * clusterName = "interactive";
+
+ commands_list clusterCommands = {
+ make_unique<InteractiveStartCommand>(&commands, credsIssuerConfig),
+ };
+
+ commands.RegisterCommandSet(clusterName, clusterCommands, "Commands for starting long-lived interactive modes.");
+}
diff --git a/examples/fabric-admin/commands/interactive/InteractiveCommands.cpp b/examples/fabric-admin/commands/interactive/InteractiveCommands.cpp
new file mode 100644
index 0000000..9ef07e7
--- /dev/null
+++ b/examples/fabric-admin/commands/interactive/InteractiveCommands.cpp
@@ -0,0 +1,139 @@
+/*
+ * 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 "InteractiveCommands.h"
+
+#include <platform/logging/LogV.h>
+
+#include <editline.h>
+
+#include <string>
+#include <vector>
+
+constexpr char kInteractiveModePrompt[] = ">>> ";
+constexpr char kInteractiveModeHistoryFileName[] = "chip_tool_history";
+constexpr char kInteractiveModeStopCommand[] = "quit()";
+
+namespace {
+
+void ClearLine()
+{
+ printf("\r\x1B[0J"); // Move cursor to the beginning of the line and clear from cursor to end of the screen
+}
+
+void ENFORCE_FORMAT(3, 0) LoggingCallback(const char * module, uint8_t category, const char * msg, va_list args)
+{
+ ClearLine();
+ chip::Logging::Platform::LogV(module, category, msg, args);
+ ClearLine();
+}
+
+} // namespace
+
+char * InteractiveStartCommand::GetCommand(char * command)
+{
+ if (command != nullptr)
+ {
+ free(command);
+ command = nullptr;
+ }
+
+ command = readline(kInteractiveModePrompt);
+
+ // Do not save empty lines
+ if (command != nullptr && *command)
+ {
+ add_history(command);
+ write_history(GetHistoryFilePath().c_str());
+ }
+
+ return command;
+}
+
+std::string InteractiveStartCommand::GetHistoryFilePath() const
+{
+ std::string storageDir;
+ if (GetStorageDirectory().HasValue())
+ {
+ storageDir = GetStorageDirectory().Value();
+ }
+ else
+ {
+ // Match what GetFilename in ExamplePersistentStorage.cpp does.
+ const char * dir = getenv("TMPDIR");
+ if (dir == nullptr)
+ {
+ dir = "/tmp";
+ }
+ storageDir = dir;
+ }
+
+ return storageDir + "/" + kInteractiveModeHistoryFileName;
+}
+
+CHIP_ERROR InteractiveStartCommand::RunCommand()
+{
+ read_history(GetHistoryFilePath().c_str());
+
+ // Logs needs to be redirected in order to refresh the screen appropriately when something
+ // is dumped to stdout while the user is typing a command.
+ chip::Logging::SetLogRedirectCallback(LoggingCallback);
+
+ char * command = nullptr;
+ int status;
+ while (true)
+ {
+ command = GetCommand(command);
+ if (command != nullptr && !ParseCommand(command, &status))
+ {
+ break;
+ }
+ }
+
+ if (command != nullptr)
+ {
+ free(command);
+ command = nullptr;
+ }
+
+ SetCommandExitStatus(CHIP_NO_ERROR);
+ return CHIP_NO_ERROR;
+}
+
+bool InteractiveCommand::ParseCommand(char * command, int * status)
+{
+ if (strcmp(command, kInteractiveModeStopCommand) == 0)
+ {
+ // If scheduling the cleanup fails, there is not much we can do.
+ // But if something went wrong while the application is leaving it could be because things have
+ // not been cleaned up properly, so it is still useful to log the failure.
+ LogErrorOnFailure(chip::DeviceLayer::PlatformMgr().ScheduleWork(ExecuteDeferredCleanups, 0));
+ return false;
+ }
+
+ ClearLine();
+
+ *status = mHandler->RunInteractive(command, GetStorageDirectory(), NeedsOperationalAdvertising());
+
+ return true;
+}
+
+bool InteractiveCommand::NeedsOperationalAdvertising()
+{
+ return mAdvertiseOperational.ValueOr(true);
+}
diff --git a/examples/fabric-admin/commands/interactive/InteractiveCommands.h b/examples/fabric-admin/commands/interactive/InteractiveCommands.h
new file mode 100644
index 0000000..21c14a7
--- /dev/null
+++ b/examples/fabric-admin/commands/interactive/InteractiveCommands.h
@@ -0,0 +1,68 @@
+/*
+ * 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 "../clusters/DataModelLogger.h"
+#include "../common/CHIPCommand.h"
+#include "../common/Commands.h"
+
+#include <websocket-server/WebSocketServer.h>
+
+#include <string>
+
+class Commands;
+
+class InteractiveCommand : public CHIPCommand
+{
+public:
+ InteractiveCommand(const char * name, Commands * commandsHandler, const char * helpText,
+ CredentialIssuerCommands * credsIssuerConfig) :
+ CHIPCommand(name, credsIssuerConfig, helpText),
+ mHandler(commandsHandler)
+ {
+ AddArgument("advertise-operational", 0, 1, &mAdvertiseOperational,
+ "Advertise operational node over DNS-SD and accept incoming CASE sessions.");
+ }
+
+ /////////// CHIPCommand Interface /////////
+ chip::System::Clock::Timeout GetWaitDuration() const override { return chip::System::Clock::Seconds16(0); }
+ bool NeedsOperationalAdvertising() override;
+
+ bool ParseCommand(char * command, int * status);
+
+private:
+ Commands * mHandler = nullptr;
+ chip::Optional<bool> mAdvertiseOperational;
+};
+
+class InteractiveStartCommand : public InteractiveCommand
+{
+public:
+ InteractiveStartCommand(Commands * commandsHandler, CredentialIssuerCommands * credsIssuerConfig) :
+ InteractiveCommand("start", commandsHandler, "Start an interactive shell that can then run other commands.",
+ credsIssuerConfig)
+ {}
+
+ /////////// CHIPCommand Interface /////////
+ CHIP_ERROR RunCommand() override;
+
+private:
+ char * GetCommand(char * command);
+ std::string GetHistoryFilePath() const;
+};
diff --git a/examples/fabric-admin/commands/pairing/Commands.h b/examples/fabric-admin/commands/pairing/Commands.h
new file mode 100644
index 0000000..6fdface
--- /dev/null
+++ b/examples/fabric-admin/commands/pairing/Commands.h
@@ -0,0 +1,259 @@
+/*
+ * 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 "commands/common/Commands.h"
+#include "commands/pairing/GetCommissionerNodeIdCommand.h"
+#include "commands/pairing/GetCommissionerRootCertificateCommand.h"
+#include "commands/pairing/IssueNOCChainCommand.h"
+#include "commands/pairing/OpenCommissioningWindowCommand.h"
+#include "commands/pairing/PairingCommand.h"
+
+#include <app/server/Dnssd.h>
+#include <commands/common/CredentialIssuerCommands.h>
+#include <lib/dnssd/Resolver.h>
+
+class Unpair : public PairingCommand
+{
+public:
+ Unpair(CredentialIssuerCommands * credsIssuerConfig) :
+ PairingCommand("unpair", PairingMode::None, PairingNetworkType::None, credsIssuerConfig)
+ {}
+};
+
+class PairCode : public PairingCommand
+{
+public:
+ PairCode(CredentialIssuerCommands * credsIssuerConfig) :
+ PairingCommand("code", PairingMode::Code, PairingNetworkType::None, credsIssuerConfig)
+ {}
+};
+
+class PairCodePase : public PairingCommand
+{
+public:
+ PairCodePase(CredentialIssuerCommands * credsIssuerConfig) :
+ PairingCommand("code-paseonly", PairingMode::CodePaseOnly, PairingNetworkType::None, credsIssuerConfig)
+ {}
+};
+
+class PairCodeWifi : public PairingCommand
+{
+public:
+ PairCodeWifi(CredentialIssuerCommands * credsIssuerConfig) :
+ PairingCommand("code-wifi", PairingMode::Code, PairingNetworkType::WiFi, credsIssuerConfig)
+ {}
+};
+
+class PairCodeThread : public PairingCommand
+{
+public:
+ PairCodeThread(CredentialIssuerCommands * credsIssuerConfig) :
+ PairingCommand("code-thread", PairingMode::Code, PairingNetworkType::Thread, credsIssuerConfig)
+ {}
+};
+
+class PairOnNetwork : public PairingCommand
+{
+public:
+ PairOnNetwork(CredentialIssuerCommands * credsIssuerConfig) :
+ PairingCommand("onnetwork", PairingMode::OnNetwork, PairingNetworkType::None, credsIssuerConfig)
+ {}
+};
+
+class PairOnNetworkShort : public PairingCommand
+{
+public:
+ PairOnNetworkShort(CredentialIssuerCommands * credsIssuerConfig) :
+ PairingCommand("onnetwork-short", PairingMode::OnNetwork, PairingNetworkType::None, credsIssuerConfig,
+ chip::Dnssd::DiscoveryFilterType::kShortDiscriminator)
+ {}
+};
+
+class PairOnNetworkLong : public PairingCommand
+{
+public:
+ PairOnNetworkLong(CredentialIssuerCommands * credsIssuerConfig) :
+ PairingCommand("onnetwork-long", PairingMode::OnNetwork, PairingNetworkType::None, credsIssuerConfig,
+ chip::Dnssd::DiscoveryFilterType::kLongDiscriminator)
+ {}
+};
+
+class PairOnNetworkVendor : public PairingCommand
+{
+public:
+ PairOnNetworkVendor(CredentialIssuerCommands * credsIssuerConfig) :
+ PairingCommand("onnetwork-vendor", PairingMode::OnNetwork, PairingNetworkType::None, credsIssuerConfig,
+ chip::Dnssd::DiscoveryFilterType::kVendorId)
+ {}
+};
+
+class PairOnNetworkFabric : public PairingCommand
+{
+public:
+ PairOnNetworkFabric(CredentialIssuerCommands * credsIssuerConfig) :
+ PairingCommand("onnetwork-fabric", PairingMode::OnNetwork, PairingNetworkType::None, credsIssuerConfig,
+ chip::Dnssd::DiscoveryFilterType::kCompressedFabricId)
+ {}
+};
+
+class PairOnNetworkCommissioningMode : public PairingCommand
+{
+public:
+ PairOnNetworkCommissioningMode(CredentialIssuerCommands * credsIssuerConfig) :
+ PairingCommand("onnetwork-commissioning-mode", PairingMode::OnNetwork, PairingNetworkType::None, credsIssuerConfig,
+ chip::Dnssd::DiscoveryFilterType::kCommissioningMode)
+ {}
+};
+
+class PairOnNetworkCommissioner : public PairingCommand
+{
+public:
+ PairOnNetworkCommissioner(CredentialIssuerCommands * credsIssuerConfig) :
+ PairingCommand("onnetwork-commissioner", PairingMode::OnNetwork, PairingNetworkType::None, credsIssuerConfig,
+ chip::Dnssd::DiscoveryFilterType::kCommissioner)
+ {}
+};
+
+class PairOnNetworkDeviceType : public PairingCommand
+{
+public:
+ PairOnNetworkDeviceType(CredentialIssuerCommands * credsIssuerConfig) :
+ PairingCommand("onnetwork-device-type", PairingMode::OnNetwork, PairingNetworkType::None, credsIssuerConfig,
+ chip::Dnssd::DiscoveryFilterType::kDeviceType)
+ {}
+};
+
+class PairOnNetworkInstanceName : public PairingCommand
+{
+public:
+ PairOnNetworkInstanceName(CredentialIssuerCommands * credsIssuerConfig) :
+ PairingCommand("onnetwork-instance-name", PairingMode::OnNetwork, PairingNetworkType::None, credsIssuerConfig,
+ chip::Dnssd::DiscoveryFilterType::kInstanceName)
+ {}
+};
+
+class PairBleWiFi : public PairingCommand
+{
+public:
+ PairBleWiFi(CredentialIssuerCommands * credsIssuerConfig) :
+ PairingCommand("ble-wifi", PairingMode::Ble, PairingNetworkType::WiFi, credsIssuerConfig)
+ {}
+};
+
+class PairBleThread : public PairingCommand
+{
+public:
+ PairBleThread(CredentialIssuerCommands * credsIssuerConfig) :
+ PairingCommand("ble-thread", PairingMode::Ble, PairingNetworkType::Thread, credsIssuerConfig)
+ {}
+};
+
+class PairSoftAP : public PairingCommand
+{
+public:
+ PairSoftAP(CredentialIssuerCommands * credsIssuerConfig) :
+ PairingCommand("softap", PairingMode::SoftAP, PairingNetworkType::WiFi, credsIssuerConfig)
+ {}
+};
+
+class PairAlreadyDiscovered : public PairingCommand
+{
+public:
+ PairAlreadyDiscovered(CredentialIssuerCommands * credsIssuerConfig) :
+ PairingCommand("already-discovered", PairingMode::AlreadyDiscovered, PairingNetworkType::None, credsIssuerConfig)
+ {}
+};
+
+class PairAlreadyDiscoveredByIndex : public PairingCommand
+{
+public:
+ PairAlreadyDiscoveredByIndex(CredentialIssuerCommands * credsIssuerConfig) :
+ PairingCommand("already-discovered-by-index", PairingMode::AlreadyDiscoveredByIndex, PairingNetworkType::None,
+ credsIssuerConfig)
+ {}
+};
+
+class PairAlreadyDiscoveredByIndexWithWiFi : public PairingCommand
+{
+public:
+ PairAlreadyDiscoveredByIndexWithWiFi(CredentialIssuerCommands * credsIssuerConfig) :
+ PairingCommand("already-discovered-by-index-with-wifi", PairingMode::AlreadyDiscoveredByIndex, PairingNetworkType::WiFi,
+ credsIssuerConfig)
+ {}
+};
+
+class PairAlreadyDiscoveredByIndexWithCode : public PairingCommand
+{
+public:
+ PairAlreadyDiscoveredByIndexWithCode(CredentialIssuerCommands * credsIssuerConfig) :
+ PairingCommand("already-discovered-by-index-with-code", PairingMode::AlreadyDiscoveredByIndexWithCode,
+ PairingNetworkType::None, credsIssuerConfig)
+ {}
+};
+
+class StartUdcServerCommand : public CHIPCommand
+{
+public:
+ StartUdcServerCommand(CredentialIssuerCommands * credsIssuerConfig) : CHIPCommand("start-udc-server", credsIssuerConfig) {}
+ chip::System::Clock::Timeout GetWaitDuration() const override { return chip::System::Clock::Seconds16(300); }
+
+ CHIP_ERROR RunCommand() override
+ {
+ chip::app::DnssdServer::Instance().StartServer(chip::Dnssd::CommissioningMode::kDisabled);
+ return CHIP_NO_ERROR;
+ }
+};
+
+void registerCommandsPairing(Commands & commands, CredentialIssuerCommands * credsIssuerConfig)
+{
+ const char * clusterName = "Pairing";
+
+ commands_list clusterCommands = {
+ make_unique<Unpair>(credsIssuerConfig),
+ make_unique<PairCode>(credsIssuerConfig),
+ make_unique<PairCodePase>(credsIssuerConfig),
+ make_unique<PairCodeWifi>(credsIssuerConfig),
+ make_unique<PairCodeThread>(credsIssuerConfig),
+ make_unique<PairBleWiFi>(credsIssuerConfig),
+ make_unique<PairBleThread>(credsIssuerConfig),
+ make_unique<PairSoftAP>(credsIssuerConfig),
+ make_unique<PairAlreadyDiscovered>(credsIssuerConfig),
+ make_unique<PairAlreadyDiscoveredByIndex>(credsIssuerConfig),
+ make_unique<PairAlreadyDiscoveredByIndexWithWiFi>(credsIssuerConfig),
+ make_unique<PairAlreadyDiscoveredByIndexWithCode>(credsIssuerConfig),
+ make_unique<PairOnNetwork>(credsIssuerConfig),
+ make_unique<PairOnNetworkShort>(credsIssuerConfig),
+ make_unique<PairOnNetworkLong>(credsIssuerConfig),
+ make_unique<PairOnNetworkVendor>(credsIssuerConfig),
+ make_unique<PairOnNetworkCommissioningMode>(credsIssuerConfig),
+ make_unique<PairOnNetworkCommissioner>(credsIssuerConfig),
+ make_unique<PairOnNetworkDeviceType>(credsIssuerConfig),
+ make_unique<PairOnNetworkInstanceName>(credsIssuerConfig),
+ // TODO(#13973) - enable CommissionedListCommand once DNS Cache is implemented
+ // make_unique<CommissionedListCommand>(),
+ make_unique<StartUdcServerCommand>(credsIssuerConfig),
+ make_unique<OpenCommissioningWindowCommand>(credsIssuerConfig),
+ make_unique<GetCommissionerNodeIdCommand>(credsIssuerConfig),
+ make_unique<GetCommissionerRootCertificateCommand>(credsIssuerConfig),
+ make_unique<IssueNOCChainCommand>(credsIssuerConfig),
+ };
+
+ commands.RegisterCommandSet(clusterName, clusterCommands, "Commands for commissioning devices.");
+}
diff --git a/examples/fabric-admin/commands/pairing/GetCommissionerNodeIdCommand.h b/examples/fabric-admin/commands/pairing/GetCommissionerNodeIdCommand.h
new file mode 100644
index 0000000..3234cfe
--- /dev/null
+++ b/examples/fabric-admin/commands/pairing/GetCommissionerNodeIdCommand.h
@@ -0,0 +1,44 @@
+/*
+ * 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 "../common/CHIPCommand.h"
+#include "../common/RemoteDataModelLogger.h"
+
+class GetCommissionerNodeIdCommand : public CHIPCommand
+{
+public:
+ GetCommissionerNodeIdCommand(CredentialIssuerCommands * credIssuerCommands) :
+ CHIPCommand("get-commissioner-node-id", credIssuerCommands)
+ {}
+
+ /////////// CHIPCommand Interface /////////
+ CHIP_ERROR RunCommand() override
+ {
+ chip::NodeId id;
+ ReturnErrorOnFailure(GetIdentityNodeId(GetIdentity(), &id));
+ ChipLogProgress(NotSpecified, "Commissioner Node Id 0x:" ChipLogFormatX64, ChipLogValueX64(id));
+
+ ReturnErrorOnFailure(RemoteDataModelLogger::LogGetCommissionerNodeId(id));
+ SetCommandExitStatus(CHIP_NO_ERROR);
+ return CHIP_NO_ERROR;
+ }
+
+ chip::System::Clock::Timeout GetWaitDuration() const override { return chip::System::Clock::Seconds16(10); }
+};
diff --git a/examples/fabric-admin/commands/pairing/GetCommissionerRootCertificateCommand.h b/examples/fabric-admin/commands/pairing/GetCommissionerRootCertificateCommand.h
new file mode 100644
index 0000000..1d25efc
--- /dev/null
+++ b/examples/fabric-admin/commands/pairing/GetCommissionerRootCertificateCommand.h
@@ -0,0 +1,53 @@
+/*
+ * 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 "../common/CHIPCommand.h"
+#include "../common/RemoteDataModelLogger.h"
+
+#include "ToTLVCert.h"
+
+#include <string>
+
+class GetCommissionerRootCertificateCommand : public CHIPCommand
+{
+public:
+ GetCommissionerRootCertificateCommand(CredentialIssuerCommands * credIssuerCommands) :
+ CHIPCommand("get-commissioner-root-certificate", credIssuerCommands,
+ "Returns a base64-encoded RCAC prefixed with: 'base64:'")
+ {}
+
+ /////////// CHIPCommand Interface /////////
+ CHIP_ERROR RunCommand() override
+ {
+ chip::ByteSpan span;
+ ReturnErrorOnFailure(GetIdentityRootCertificate(GetIdentity(), span));
+
+ std::string rcac;
+ ReturnErrorOnFailure(ToTLVCert(span, rcac));
+ ChipLogProgress(NotSpecified, "RCAC: %s", rcac.c_str());
+
+ ReturnErrorOnFailure(RemoteDataModelLogger::LogGetCommissionerRootCertificate(rcac.c_str()));
+
+ SetCommandExitStatus(CHIP_NO_ERROR);
+ return CHIP_NO_ERROR;
+ }
+
+ chip::System::Clock::Timeout GetWaitDuration() const override { return chip::System::Clock::Seconds16(10); }
+};
diff --git a/examples/fabric-admin/commands/pairing/IssueNOCChainCommand.h b/examples/fabric-admin/commands/pairing/IssueNOCChainCommand.h
new file mode 100644
index 0000000..0103b26
--- /dev/null
+++ b/examples/fabric-admin/commands/pairing/IssueNOCChainCommand.h
@@ -0,0 +1,91 @@
+/*
+ * 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 "../common/CHIPCommand.h"
+#include "../common/RemoteDataModelLogger.h"
+
+#include "ToTLVCert.h"
+
+#include <string>
+
+class IssueNOCChainCommand : public CHIPCommand
+{
+public:
+ IssueNOCChainCommand(CredentialIssuerCommands * credIssuerCommands) :
+ CHIPCommand("issue-noc-chain", credIssuerCommands,
+ "Returns a base64-encoded NOC, ICAC, RCAC, and IPK prefixed with: 'base64:'"),
+ mDeviceNOCChainCallback(OnDeviceNOCChainGeneration, this)
+ {
+ AddArgument("elements", &mNOCSRElements, "NOCSRElements encoded in hexadecimal");
+ AddArgument("node-id", 0, UINT64_MAX, &mNodeId, "The target node id");
+ }
+
+ /////////// CHIPCommand Interface /////////
+ CHIP_ERROR RunCommand() override
+ {
+ auto & commissioner = CurrentCommissioner();
+ ReturnErrorOnFailure(commissioner.IssueNOCChain(mNOCSRElements, mNodeId, &mDeviceNOCChainCallback));
+ return CHIP_NO_ERROR;
+ }
+
+ chip::System::Clock::Timeout GetWaitDuration() const override { return chip::System::Clock::Seconds16(10); }
+
+ static void OnDeviceNOCChainGeneration(void * context, CHIP_ERROR status, const chip::ByteSpan & noc,
+ const chip::ByteSpan & icac, const chip::ByteSpan & rcac,
+ chip::Optional<chip::Crypto::IdentityProtectionKeySpan> ipk,
+ chip::Optional<chip::NodeId> adminSubject)
+ {
+ auto command = static_cast<IssueNOCChainCommand *>(context);
+
+ auto err = status;
+ VerifyOrReturn(CHIP_NO_ERROR == err, command->SetCommandExitStatus(err));
+
+ std::string nocStr;
+ err = ToTLVCert(noc, nocStr);
+ VerifyOrReturn(CHIP_NO_ERROR == err, command->SetCommandExitStatus(err));
+ ChipLogProgress(NotSpecified, "NOC: %s", nocStr.c_str());
+
+ std::string icacStr;
+ err = ToTLVCert(icac, icacStr);
+ VerifyOrReturn(CHIP_NO_ERROR == err, command->SetCommandExitStatus(err));
+ ChipLogProgress(NotSpecified, "ICAC: %s", icacStr.c_str());
+
+ std::string rcacStr;
+ err = ToTLVCert(rcac, rcacStr);
+ VerifyOrReturn(CHIP_NO_ERROR == err, command->SetCommandExitStatus(err));
+ ChipLogProgress(NotSpecified, "RCAC: %s", rcacStr.c_str());
+
+ std::string ipkStr;
+ if (ipk.HasValue())
+ {
+ err = ToBase64(ipk.Value(), ipkStr);
+ VerifyOrReturn(CHIP_NO_ERROR == err, command->SetCommandExitStatus(err));
+ }
+ ChipLogProgress(NotSpecified, "IPK: %s", ipkStr.c_str());
+
+ err = RemoteDataModelLogger::LogIssueNOCChain(nocStr.c_str(), icacStr.c_str(), rcacStr.c_str(), ipkStr.c_str());
+ command->SetCommandExitStatus(err);
+ }
+
+private:
+ chip::Callback::Callback<chip::Controller::OnNOCChainGeneration> mDeviceNOCChainCallback;
+ chip::ByteSpan mNOCSRElements;
+ chip::NodeId mNodeId;
+};
diff --git a/examples/fabric-admin/commands/pairing/OpenCommissioningWindowCommand.cpp b/examples/fabric-admin/commands/pairing/OpenCommissioningWindowCommand.cpp
new file mode 100644
index 0000000..bc4af6c
--- /dev/null
+++ b/examples/fabric-admin/commands/pairing/OpenCommissioningWindowCommand.cpp
@@ -0,0 +1,62 @@
+/*
+ * 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 "OpenCommissioningWindowCommand.h"
+
+#include <system/SystemClock.h>
+
+using namespace ::chip;
+
+CHIP_ERROR OpenCommissioningWindowCommand::RunCommand()
+{
+ mWindowOpener = Platform::MakeUnique<Controller::CommissioningWindowOpener>(&CurrentCommissioner());
+ if (mCommissioningWindowOption == Controller::CommissioningWindowOpener::CommissioningWindowOption::kOriginalSetupCode)
+ {
+ return mWindowOpener->OpenBasicCommissioningWindow(mNodeId, System::Clock::Seconds16(mCommissioningWindowTimeout),
+ &mOnOpenBasicCommissioningWindowCallback);
+ }
+
+ if (mCommissioningWindowOption == Controller::CommissioningWindowOpener::CommissioningWindowOption::kTokenWithRandomPIN)
+ {
+ SetupPayload ignored;
+ return mWindowOpener->OpenCommissioningWindow(mNodeId, System::Clock::Seconds16(mCommissioningWindowTimeout), mIteration,
+ mDiscriminator, NullOptional, NullOptional,
+ &mOnOpenCommissioningWindowCallback, ignored,
+ /* readVIDPIDAttributes */ true);
+ }
+
+ ChipLogError(NotSpecified, "Unknown commissioning window option: %d", to_underlying(mCommissioningWindowOption));
+ return CHIP_ERROR_INVALID_ARGUMENT;
+}
+
+void OpenCommissioningWindowCommand::OnOpenCommissioningWindowResponse(void * context, NodeId remoteId, CHIP_ERROR err,
+ chip::SetupPayload payload)
+{
+ LogErrorOnFailure(err);
+
+ OnOpenBasicCommissioningWindowResponse(context, remoteId, err);
+}
+
+void OpenCommissioningWindowCommand::OnOpenBasicCommissioningWindowResponse(void * context, NodeId remoteId, CHIP_ERROR err)
+{
+ LogErrorOnFailure(err);
+
+ OpenCommissioningWindowCommand * command = reinterpret_cast<OpenCommissioningWindowCommand *>(context);
+ VerifyOrReturn(command != nullptr, ChipLogError(NotSpecified, "OnOpenCommissioningWindowCommand: context is null"));
+ command->SetCommandExitStatus(err);
+}
diff --git a/examples/fabric-admin/commands/pairing/OpenCommissioningWindowCommand.h b/examples/fabric-admin/commands/pairing/OpenCommissioningWindowCommand.h
new file mode 100644
index 0000000..99b179d
--- /dev/null
+++ b/examples/fabric-admin/commands/pairing/OpenCommissioningWindowCommand.h
@@ -0,0 +1,67 @@
+/*
+ * 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 "../common/CHIPCommand.h"
+
+#include <controller/CommissioningWindowOpener.h>
+#include <lib/support/CHIPMem.h>
+
+class OpenCommissioningWindowCommand : public CHIPCommand
+{
+public:
+ OpenCommissioningWindowCommand(CredentialIssuerCommands * credIssuerCommands) :
+ CHIPCommand("open-commissioning-window", credIssuerCommands),
+ mOnOpenCommissioningWindowCallback(OnOpenCommissioningWindowResponse, this),
+ mOnOpenBasicCommissioningWindowCallback(OnOpenBasicCommissioningWindowResponse, this)
+ {
+ AddArgument("node-id", 0, UINT64_MAX, &mNodeId, "Node to send command to.");
+ AddArgument("option", 0, 2, &mCommissioningWindowOption,
+ "1 to use Enhanced Commissioning Method.\n 0 to use Basic Commissioning Method.");
+ AddArgument("window-timeout", 0, UINT16_MAX, &mCommissioningWindowTimeout,
+ "Time, in seconds, before the commissioning window closes.");
+ AddArgument("iteration", chip::Crypto::kSpake2p_Min_PBKDF_Iterations, chip::Crypto::kSpake2p_Max_PBKDF_Iterations,
+ &mIteration, "Number of PBKDF iterations to use to derive the verifier. Ignored if 'option' is 0.");
+ AddArgument("discriminator", 0, 4096, &mDiscriminator, "Discriminator to use for advertising. Ignored if 'option' is 0.");
+ AddArgument("timeout", 0, UINT16_MAX, &mTimeout, "Time, in seconds, before this command is considered to have timed out.");
+ }
+
+ /////////// CHIPCommand Interface /////////
+ CHIP_ERROR RunCommand() override;
+ // We issue multiple data model operations for this command, and the default
+ // timeout for those is 10 seconds, so default to 20 seconds.
+ chip::System::Clock::Timeout GetWaitDuration() const override { return chip::System::Clock::Seconds16(mTimeout.ValueOr(20)); }
+
+private:
+ NodeId mNodeId;
+ chip::Controller::CommissioningWindowOpener::CommissioningWindowOption mCommissioningWindowOption;
+ uint16_t mCommissioningWindowTimeout;
+ uint32_t mIteration;
+ uint16_t mDiscriminator;
+
+ chip::Optional<uint16_t> mTimeout;
+
+ chip::Platform::UniquePtr<chip::Controller::CommissioningWindowOpener> mWindowOpener;
+
+ static void OnOpenCommissioningWindowResponse(void * context, NodeId deviceId, CHIP_ERROR status, chip::SetupPayload payload);
+ static void OnOpenBasicCommissioningWindowResponse(void * context, NodeId deviceId, CHIP_ERROR status);
+
+ chip::Callback::Callback<chip::Controller::OnOpenCommissioningWindow> mOnOpenCommissioningWindowCallback;
+ chip::Callback::Callback<chip::Controller::OnOpenBasicCommissioningWindow> mOnOpenBasicCommissioningWindowCallback;
+};
diff --git a/examples/fabric-admin/commands/pairing/PairingCommand.cpp b/examples/fabric-admin/commands/pairing/PairingCommand.cpp
new file mode 100644
index 0000000..80775f0
--- /dev/null
+++ b/examples/fabric-admin/commands/pairing/PairingCommand.cpp
@@ -0,0 +1,551 @@
+/*
+ * 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 "PairingCommand.h"
+#include "platform/PlatformManager.h"
+#include <commands/common/DeviceScanner.h>
+#include <controller/ExampleOperationalCredentialsIssuer.h>
+#include <crypto/CHIPCryptoPAL.h>
+#include <lib/core/CHIPSafeCasts.h>
+#include <lib/support/logging/CHIPLogging.h>
+#include <protocols/secure_channel/PASESession.h>
+
+#include <setup_payload/ManualSetupPayloadParser.h>
+#include <setup_payload/QRCodeSetupPayloadParser.h>
+
+#include <string>
+
+using namespace ::chip;
+using namespace ::chip::Controller;
+
+CHIP_ERROR PairingCommand::RunCommand()
+{
+ CurrentCommissioner().RegisterPairingDelegate(this);
+ // Clear the CATs in OperationalCredentialsIssuer
+ mCredIssuerCmds->SetCredentialIssuerCATValues(kUndefinedCATs);
+
+ mDeviceIsICD = false;
+
+ if (mCASEAuthTags.HasValue() && mCASEAuthTags.Value().size() <= kMaxSubjectCATAttributeCount)
+ {
+ CATValues cats = kUndefinedCATs;
+ for (size_t index = 0; index < mCASEAuthTags.Value().size(); ++index)
+ {
+ cats.values[index] = mCASEAuthTags.Value()[index];
+ }
+ if (cats.AreValid())
+ {
+ mCredIssuerCmds->SetCredentialIssuerCATValues(cats);
+ }
+ }
+ return RunInternal(mNodeId);
+}
+
+CHIP_ERROR PairingCommand::RunInternal(NodeId remoteId)
+{
+ CHIP_ERROR err = CHIP_NO_ERROR;
+
+ switch (mPairingMode)
+ {
+ case PairingMode::None:
+ err = Unpair(remoteId);
+ break;
+ case PairingMode::Code:
+ err = PairWithCode(remoteId);
+ break;
+ case PairingMode::CodePaseOnly:
+ err = PaseWithCode(remoteId);
+ break;
+ case PairingMode::Ble:
+ err = Pair(remoteId, PeerAddress::BLE());
+ break;
+ case PairingMode::OnNetwork:
+ err = PairWithMdns(remoteId);
+ break;
+ case PairingMode::SoftAP:
+ err = Pair(remoteId, PeerAddress::UDP(mRemoteAddr.address, mRemotePort, mRemoteAddr.interfaceId));
+ break;
+ case PairingMode::AlreadyDiscovered:
+ err = Pair(remoteId, PeerAddress::UDP(mRemoteAddr.address, mRemotePort, mRemoteAddr.interfaceId));
+ break;
+ case PairingMode::AlreadyDiscoveredByIndex:
+ err = PairWithMdnsOrBleByIndex(remoteId, mIndex);
+ break;
+ case PairingMode::AlreadyDiscoveredByIndexWithCode:
+ err = PairWithMdnsOrBleByIndexWithCode(remoteId, mIndex);
+ break;
+ }
+
+ return err;
+}
+
+CommissioningParameters PairingCommand::GetCommissioningParameters()
+{
+ auto params = CommissioningParameters();
+ params.SetSkipCommissioningComplete(mSkipCommissioningComplete.ValueOr(false));
+ if (mBypassAttestationVerifier.ValueOr(false))
+ {
+ params.SetDeviceAttestationDelegate(this);
+ }
+
+ switch (mNetworkType)
+ {
+ case PairingNetworkType::WiFi:
+ params.SetWiFiCredentials(Controller::WiFiCredentials(mSSID, mPassword));
+ break;
+ case PairingNetworkType::Thread:
+ params.SetThreadOperationalDataset(mOperationalDataset);
+ break;
+ case PairingNetworkType::None:
+ break;
+ }
+
+ if (mCountryCode.HasValue())
+ {
+ params.SetCountryCode(CharSpan::fromCharString(mCountryCode.Value()));
+ }
+
+ // mTimeZoneList is an optional argument managed by TypedComplexArgument mComplex_TimeZones.
+ // Since optional Complex arguments are not currently supported via the <chip::Optional> class,
+ // we will use mTimeZoneList.data() value to determine if the argument was provided.
+ if (mTimeZoneList.data())
+ {
+ params.SetTimeZone(mTimeZoneList);
+ }
+
+ // miDSTOffsetList is an optional argument managed by TypedComplexArgument mComplex_DSTOffsets.
+ // Since optional Complex arguments are not currently supported via the <chip::Optional> class,
+ // we will use mTimeZoneList.data() value to determine if the argument was provided.
+ if (mDSTOffsetList.data())
+ {
+ params.SetDSTOffsets(mDSTOffsetList);
+ }
+
+ if (mICDRegistration.ValueOr(false))
+ {
+ params.SetICDRegistrationStrategy(ICDRegistrationStrategy::kBeforeComplete);
+
+ if (!mICDSymmetricKey.HasValue())
+ {
+ chip::Crypto::DRBG_get_bytes(mRandomGeneratedICDSymmetricKey, sizeof(mRandomGeneratedICDSymmetricKey));
+ mICDSymmetricKey.SetValue(ByteSpan(mRandomGeneratedICDSymmetricKey));
+ }
+ if (!mICDCheckInNodeId.HasValue())
+ {
+ mICDCheckInNodeId.SetValue(CurrentCommissioner().GetNodeId());
+ }
+ if (!mICDMonitoredSubject.HasValue())
+ {
+ mICDMonitoredSubject.SetValue(mICDCheckInNodeId.Value());
+ }
+ // These Optionals must have values now.
+ // The commissioner will verify these values.
+ params.SetICDSymmetricKey(mICDSymmetricKey.Value());
+ if (mICDStayActiveDurationMsec.HasValue())
+ {
+ params.SetICDStayActiveDurationMsec(mICDStayActiveDurationMsec.Value());
+ }
+ params.SetICDCheckInNodeId(mICDCheckInNodeId.Value());
+ params.SetICDMonitoredSubject(mICDMonitoredSubject.Value());
+ }
+
+ return params;
+}
+
+CHIP_ERROR PairingCommand::PaseWithCode(NodeId remoteId)
+{
+ auto discoveryType = DiscoveryType::kAll;
+ if (mUseOnlyOnNetworkDiscovery.ValueOr(false))
+ {
+ discoveryType = DiscoveryType::kDiscoveryNetworkOnly;
+ }
+
+ if (mDiscoverOnce.ValueOr(false))
+ {
+ discoveryType = DiscoveryType::kDiscoveryNetworkOnlyWithoutPASEAutoRetry;
+ }
+
+ return CurrentCommissioner().EstablishPASEConnection(remoteId, mOnboardingPayload, discoveryType);
+}
+
+CHIP_ERROR PairingCommand::PairWithCode(NodeId remoteId)
+{
+ CommissioningParameters commissioningParams = GetCommissioningParameters();
+
+ // If no network discovery behavior and no network credentials are provided, assume that the pairing command is trying to pair
+ // with an on-network device.
+ if (!mUseOnlyOnNetworkDiscovery.HasValue())
+ {
+ auto threadCredentials = commissioningParams.GetThreadOperationalDataset();
+ auto wiFiCredentials = commissioningParams.GetWiFiCredentials();
+ mUseOnlyOnNetworkDiscovery.SetValue(!threadCredentials.HasValue() && !wiFiCredentials.HasValue());
+ }
+
+ auto discoveryType = DiscoveryType::kAll;
+ if (mUseOnlyOnNetworkDiscovery.ValueOr(false))
+ {
+ discoveryType = DiscoveryType::kDiscoveryNetworkOnly;
+ }
+
+ if (mDiscoverOnce.ValueOr(false))
+ {
+ discoveryType = DiscoveryType::kDiscoveryNetworkOnlyWithoutPASEAutoRetry;
+ }
+
+ return CurrentCommissioner().PairDevice(remoteId, mOnboardingPayload, commissioningParams, discoveryType);
+}
+
+CHIP_ERROR PairingCommand::Pair(NodeId remoteId, PeerAddress address)
+{
+ auto params = RendezvousParameters().SetSetupPINCode(mSetupPINCode).SetDiscriminator(mDiscriminator).SetPeerAddress(address);
+
+ CHIP_ERROR err = CHIP_NO_ERROR;
+ if (mPaseOnly.ValueOr(false))
+ {
+ err = CurrentCommissioner().EstablishPASEConnection(remoteId, params);
+ }
+ else
+ {
+ auto commissioningParams = GetCommissioningParameters();
+ err = CurrentCommissioner().PairDevice(remoteId, params, commissioningParams);
+ }
+ return err;
+}
+
+CHIP_ERROR PairingCommand::PairWithMdnsOrBleByIndex(NodeId remoteId, uint16_t index)
+{
+#if CHIP_DEVICE_LAYER_TARGET_DARWIN
+ VerifyOrReturnError(IsInteractive(), CHIP_ERROR_INCORRECT_STATE);
+
+ RendezvousParameters params;
+ ReturnErrorOnFailure(GetDeviceScanner().Get(index, params));
+ params.SetSetupPINCode(mSetupPINCode);
+
+ CHIP_ERROR err = CHIP_NO_ERROR;
+ if (mPaseOnly.ValueOr(false))
+ {
+ err = CurrentCommissioner().EstablishPASEConnection(remoteId, params);
+ }
+ else
+ {
+ auto commissioningParams = GetCommissioningParameters();
+ err = CurrentCommissioner().PairDevice(remoteId, params, commissioningParams);
+ }
+ return err;
+#else
+ return CHIP_ERROR_NOT_IMPLEMENTED;
+#endif // CHIP_DEVICE_LAYER_TARGET_DARWIN
+}
+
+CHIP_ERROR PairingCommand::PairWithMdnsOrBleByIndexWithCode(NodeId remoteId, uint16_t index)
+{
+#if CHIP_DEVICE_LAYER_TARGET_DARWIN
+ VerifyOrReturnError(IsInteractive(), CHIP_ERROR_INCORRECT_STATE);
+
+ Dnssd::CommonResolutionData resolutionData;
+ auto err = GetDeviceScanner().Get(index, resolutionData);
+ if (CHIP_ERROR_NOT_FOUND == err)
+ {
+ // There is no device with this index that has some resolution data. This could simply
+ // be because the device is a ble device. In this case let's fall back to looking for
+ // a device with this index and some RendezvousParameters.
+ chip::SetupPayload payload;
+ bool isQRCode = strncmp(mOnboardingPayload, kQRCodePrefix, strlen(kQRCodePrefix)) == 0;
+ if (isQRCode)
+ {
+ ReturnErrorOnFailure(QRCodeSetupPayloadParser(mOnboardingPayload).populatePayload(payload));
+ VerifyOrReturnError(payload.isValidQRCodePayload(), CHIP_ERROR_INVALID_ARGUMENT);
+ }
+ else
+ {
+ ReturnErrorOnFailure(ManualSetupPayloadParser(mOnboardingPayload).populatePayload(payload));
+ VerifyOrReturnError(payload.isValidManualCode(), CHIP_ERROR_INVALID_ARGUMENT);
+ }
+
+ mSetupPINCode = payload.setUpPINCode;
+ return PairWithMdnsOrBleByIndex(remoteId, index);
+ }
+
+ err = CHIP_NO_ERROR;
+ if (mPaseOnly.ValueOr(false))
+ {
+ err = CurrentCommissioner().EstablishPASEConnection(remoteId, mOnboardingPayload, DiscoveryType::kDiscoveryNetworkOnly,
+ MakeOptional(resolutionData));
+ }
+ else
+ {
+ auto commissioningParams = GetCommissioningParameters();
+ err = CurrentCommissioner().PairDevice(remoteId, mOnboardingPayload, commissioningParams,
+ DiscoveryType::kDiscoveryNetworkOnly, MakeOptional(resolutionData));
+ }
+ return err;
+#else
+ return CHIP_ERROR_NOT_IMPLEMENTED;
+#endif // CHIP_DEVICE_LAYER_TARGET_DARWIN
+}
+
+CHIP_ERROR PairingCommand::PairWithMdns(NodeId remoteId)
+{
+ Dnssd::DiscoveryFilter filter(mFilterType);
+ switch (mFilterType)
+ {
+ case chip::Dnssd::DiscoveryFilterType::kNone:
+ break;
+ case chip::Dnssd::DiscoveryFilterType::kShortDiscriminator:
+ case chip::Dnssd::DiscoveryFilterType::kLongDiscriminator:
+ case chip::Dnssd::DiscoveryFilterType::kCompressedFabricId:
+ case chip::Dnssd::DiscoveryFilterType::kVendorId:
+ case chip::Dnssd::DiscoveryFilterType::kDeviceType:
+ filter.code = mDiscoveryFilterCode;
+ break;
+ case chip::Dnssd::DiscoveryFilterType::kCommissioningMode:
+ break;
+ case chip::Dnssd::DiscoveryFilterType::kCommissioner:
+ filter.code = 1;
+ break;
+ case chip::Dnssd::DiscoveryFilterType::kInstanceName:
+ filter.code = 0;
+ filter.instanceName = mDiscoveryFilterInstanceName;
+ break;
+ }
+
+ CurrentCommissioner().RegisterDeviceDiscoveryDelegate(this);
+ return CurrentCommissioner().DiscoverCommissionableNodes(filter);
+}
+
+CHIP_ERROR PairingCommand::Unpair(NodeId remoteId)
+{
+ mCurrentFabricRemover = Platform::MakeUnique<Controller::CurrentFabricRemover>(&CurrentCommissioner());
+ return mCurrentFabricRemover->RemoveCurrentFabric(remoteId, &mCurrentFabricRemoveCallback);
+}
+
+void PairingCommand::OnStatusUpdate(DevicePairingDelegate::Status status)
+{
+ switch (status)
+ {
+ case DevicePairingDelegate::Status::SecurePairingSuccess:
+ ChipLogProgress(NotSpecified, "Secure Pairing Success");
+ ChipLogProgress(NotSpecified, "CASE establishment successful");
+ break;
+ case DevicePairingDelegate::Status::SecurePairingFailed:
+ ChipLogError(NotSpecified, "Secure Pairing Failed");
+ SetCommandExitStatus(CHIP_ERROR_INCORRECT_STATE);
+ break;
+ }
+}
+
+void PairingCommand::OnPairingComplete(CHIP_ERROR err)
+{
+ if (err == CHIP_NO_ERROR)
+ {
+ ChipLogProgress(NotSpecified, "Pairing Success");
+ ChipLogProgress(NotSpecified, "PASE establishment successful");
+ if (mPairingMode == PairingMode::CodePaseOnly || mPaseOnly.ValueOr(false))
+ {
+ SetCommandExitStatus(err);
+ }
+ }
+ else
+ {
+ ChipLogProgress(NotSpecified, "Pairing Failure: %s", ErrorStr(err));
+ }
+
+ if (err != CHIP_NO_ERROR)
+ {
+ SetCommandExitStatus(err);
+ }
+}
+
+void PairingCommand::OnPairingDeleted(CHIP_ERROR err)
+{
+ if (err == CHIP_NO_ERROR)
+ {
+ ChipLogProgress(NotSpecified, "Pairing Deleted Success");
+ }
+ else
+ {
+ ChipLogProgress(NotSpecified, "Pairing Deleted Failure: %s", ErrorStr(err));
+ }
+
+ SetCommandExitStatus(err);
+}
+
+void PairingCommand::OnCommissioningComplete(NodeId nodeId, CHIP_ERROR err)
+{
+ if (err == CHIP_NO_ERROR)
+ {
+ ChipLogProgress(NotSpecified, "Device commissioning completed with success");
+ }
+ else
+ {
+ // When ICD device commissioning fails, the ICDClientInfo stored in OnICDRegistrationComplete needs to be removed.
+ if (mDeviceIsICD)
+ {
+ CHIP_ERROR deleteEntryError =
+ CHIPCommand::sICDClientStorage.DeleteEntry(ScopedNodeId(mNodeId, CurrentCommissioner().GetFabricIndex()));
+ if (deleteEntryError != CHIP_NO_ERROR)
+ {
+ ChipLogError(NotSpecified, "Failed to delete ICD entry: %s", ErrorStr(err));
+ }
+ }
+ ChipLogProgress(NotSpecified, "Device commissioning Failure: %s", ErrorStr(err));
+ }
+
+ SetCommandExitStatus(err);
+}
+
+void PairingCommand::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 numm-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 PairingCommand::OnICDRegistrationComplete(NodeId nodeId, uint32_t icdCounter)
+{
+ char icdSymmetricKeyHex[chip::Crypto::kAES_CCM128_Key_Length * 2 + 1];
+
+ chip::Encoding::BytesToHex(mICDSymmetricKey.Value().data(), mICDSymmetricKey.Value().size(), icdSymmetricKeyHex,
+ sizeof(icdSymmetricKeyHex), chip::Encoding::HexFlags::kNullTerminate);
+
+ app::ICDClientInfo clientInfo;
+ clientInfo.peer_node = ScopedNodeId(nodeId, CurrentCommissioner().GetFabricIndex());
+ clientInfo.monitored_subject = mICDMonitoredSubject.Value();
+ clientInfo.start_icd_counter = icdCounter;
+
+ CHIP_ERROR err = CHIPCommand::sICDClientStorage.SetKey(clientInfo, mICDSymmetricKey.Value());
+ if (err == CHIP_NO_ERROR)
+ {
+ err = CHIPCommand::sICDClientStorage.StoreEntry(clientInfo);
+ }
+
+ if (err != CHIP_NO_ERROR)
+ {
+ CHIPCommand::sICDClientStorage.RemoveKey(clientInfo);
+ ChipLogError(NotSpecified, "Failed to persist symmetric key for " ChipLogFormatX64 ": %s", ChipLogValueX64(nodeId),
+ err.AsString());
+ SetCommandExitStatus(err);
+ return;
+ }
+
+ mDeviceIsICD = true;
+
+ ChipLogProgress(NotSpecified, "Saved ICD Symmetric key for " ChipLogFormatX64, ChipLogValueX64(nodeId));
+ ChipLogProgress(NotSpecified,
+ "ICD Registration Complete for device " ChipLogFormatX64 " / Check-In NodeID: " ChipLogFormatX64
+ " / Monitored Subject: " ChipLogFormatX64 " / Symmetric Key: %s / ICDCounter %u",
+ ChipLogValueX64(nodeId), ChipLogValueX64(mICDCheckInNodeId.Value()),
+ ChipLogValueX64(mICDMonitoredSubject.Value()), icdSymmetricKeyHex, icdCounter);
+}
+
+void PairingCommand::OnICDStayActiveComplete(NodeId deviceId, uint32_t promisedActiveDuration)
+{
+ ChipLogProgress(NotSpecified, "ICD Stay Active Complete for device " ChipLogFormatX64 " / promisedActiveDuration: %u",
+ ChipLogValueX64(deviceId), promisedActiveDuration);
+}
+
+void PairingCommand::OnDiscoveredDevice(const chip::Dnssd::CommissionNodeData & nodeData)
+{
+ // Ignore nodes with closed commissioning window
+ VerifyOrReturn(nodeData.commissioningMode != 0);
+
+ auto & resolutionData = nodeData;
+
+ const uint16_t port = resolutionData.port;
+ char buf[chip::Inet::IPAddress::kMaxStringLength];
+ resolutionData.ipAddress[0].ToString(buf);
+ ChipLogProgress(NotSpecified, "Discovered Device: %s:%u", buf, port);
+
+ // Stop Mdns discovery.
+ auto err = CurrentCommissioner().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)
+ {
+ SetCommandExitStatus(err);
+ return;
+ }
+
+ CurrentCommissioner().RegisterDeviceDiscoveryDelegate(nullptr);
+
+ auto interfaceId = resolutionData.ipAddress[0].IsIPv6LinkLocal() ? resolutionData.interfaceId : Inet::InterfaceId::Null();
+ auto peerAddress = PeerAddress::UDP(resolutionData.ipAddress[0], port, interfaceId);
+ err = Pair(mNodeId, peerAddress);
+ if (CHIP_NO_ERROR != err)
+ {
+ SetCommandExitStatus(err);
+ }
+}
+
+void PairingCommand::OnCurrentFabricRemove(void * context, NodeId nodeId, CHIP_ERROR err)
+{
+ PairingCommand * command = reinterpret_cast<PairingCommand *>(context);
+ VerifyOrReturn(command != nullptr, ChipLogError(NotSpecified, "OnCurrentFabricRemove: context is null"));
+
+ if (err == CHIP_NO_ERROR)
+ {
+ ChipLogProgress(NotSpecified, "Device unpair completed with success: " ChipLogFormatX64, ChipLogValueX64(nodeId));
+ }
+ else
+ {
+ ChipLogProgress(NotSpecified, "Device unpair Failure: " ChipLogFormatX64 " %s", ChipLogValueX64(nodeId), ErrorStr(err));
+ }
+
+ command->SetCommandExitStatus(err);
+}
+
+chip::Optional<uint16_t> PairingCommand::FailSafeExpiryTimeoutSecs() const
+{
+ // We don't need to set additional failsafe timeout as we don't ask the final user if he wants to continue
+ return chip::Optional<uint16_t>();
+}
+
+void PairingCommand::OnDeviceAttestationCompleted(chip::Controller::DeviceCommissioner * deviceCommissioner,
+ chip::DeviceProxy * device,
+ const chip::Credentials::DeviceAttestationVerifier::AttestationDeviceInfo & info,
+ chip::Credentials::AttestationVerificationResult attestationResult)
+{
+ // Bypass attestation verification, continue with success
+ auto err = deviceCommissioner->ContinueCommissioningAfterDeviceAttestation(
+ device, chip::Credentials::AttestationVerificationResult::kSuccess);
+ if (CHIP_NO_ERROR != err)
+ {
+ SetCommandExitStatus(err);
+ }
+}
diff --git a/examples/fabric-admin/commands/pairing/PairingCommand.h b/examples/fabric-admin/commands/pairing/PairingCommand.h
new file mode 100644
index 0000000..4ff3903
--- /dev/null
+++ b/examples/fabric-admin/commands/pairing/PairingCommand.h
@@ -0,0 +1,268 @@
+/*
+ * 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 "../common/CHIPCommand.h"
+#include <controller/CommissioningDelegate.h>
+#include <controller/CurrentFabricRemover.h>
+
+#include <commands/common/CredentialIssuerCommands.h>
+#include <lib/support/Span.h>
+#include <lib/support/ThreadOperationalDataset.h>
+
+enum class PairingMode
+{
+ None,
+ Code,
+ CodePaseOnly,
+ Ble,
+ SoftAP,
+ AlreadyDiscovered,
+ AlreadyDiscoveredByIndex,
+ AlreadyDiscoveredByIndexWithCode,
+ OnNetwork,
+};
+
+enum class PairingNetworkType
+{
+ None,
+ WiFi,
+ Thread,
+};
+
+class PairingCommand : public CHIPCommand,
+ public chip::Controller::DevicePairingDelegate,
+ public chip::Controller::DeviceDiscoveryDelegate,
+ public chip::Credentials::DeviceAttestationDelegate
+{
+public:
+ PairingCommand(const char * commandName, PairingMode mode, PairingNetworkType networkType,
+ CredentialIssuerCommands * credIssuerCmds,
+ chip::Dnssd::DiscoveryFilterType filterType = chip::Dnssd::DiscoveryFilterType::kNone) :
+ CHIPCommand(commandName, credIssuerCmds),
+ mPairingMode(mode), mNetworkType(networkType), mFilterType(filterType),
+ mRemoteAddr{ IPAddress::Any, chip::Inet::InterfaceId::Null() }, mComplex_TimeZones(&mTimeZoneList),
+ mComplex_DSTOffsets(&mDSTOffsetList), mCurrentFabricRemoveCallback(OnCurrentFabricRemove, this)
+ {
+ AddArgument("node-id", 0, UINT64_MAX, &mNodeId);
+ AddArgument("bypass-attestation-verifier", 0, 1, &mBypassAttestationVerifier,
+ "Bypass the attestation verifier. If not provided or false, the attestation verifier is not bypassed."
+ " If true, the commissioning will continue in case of attestation verification failure.");
+ AddArgument("case-auth-tags", 1, UINT32_MAX, &mCASEAuthTags, "The CATs to be encoded in the NOC sent to the commissionee");
+ AddArgument("icd-registration", 0, 1, &mICDRegistration,
+ "Whether to register for check-ins from ICDs during commissioning. Default: false");
+ AddArgument("icd-check-in-nodeid", 0, UINT64_MAX, &mICDCheckInNodeId,
+ "The check-in node id for the ICD, default: node id of the commissioner.");
+ AddArgument("icd-monitored-subject", 0, UINT64_MAX, &mICDMonitoredSubject,
+ "The monitored subject of the ICD, default: The node id used for icd-check-in-nodeid.");
+ AddArgument("icd-symmetric-key", &mICDSymmetricKey, "The 16 bytes ICD symmetric key, default: randomly generated.");
+ AddArgument("icd-stay-active-duration", 0, UINT32_MAX, &mICDStayActiveDurationMsec,
+ "If set, a LIT ICD that is commissioned will be requested to stay active for this many milliseconds");
+ switch (networkType)
+ {
+ case PairingNetworkType::None:
+ break;
+ case PairingNetworkType::WiFi:
+ AddArgument("ssid", &mSSID);
+ AddArgument("password", &mPassword);
+ break;
+ case PairingNetworkType::Thread:
+ AddArgument("operationalDataset", &mOperationalDataset);
+ break;
+ }
+
+ switch (mode)
+ {
+ case PairingMode::None:
+ break;
+ case PairingMode::Code:
+ AddArgument("skip-commissioning-complete", 0, 1, &mSkipCommissioningComplete);
+ FALLTHROUGH;
+ case PairingMode::CodePaseOnly:
+ AddArgument("payload", &mOnboardingPayload);
+ AddArgument("discover-once", 0, 1, &mDiscoverOnce);
+ AddArgument("use-only-onnetwork-discovery", 0, 1, &mUseOnlyOnNetworkDiscovery);
+ break;
+ case PairingMode::Ble:
+ AddArgument("skip-commissioning-complete", 0, 1, &mSkipCommissioningComplete);
+ AddArgument("setup-pin-code", 0, 134217727, &mSetupPINCode);
+ AddArgument("discriminator", 0, 4096, &mDiscriminator);
+ break;
+ case PairingMode::OnNetwork:
+ AddArgument("skip-commissioning-complete", 0, 1, &mSkipCommissioningComplete);
+ AddArgument("setup-pin-code", 0, 134217727, &mSetupPINCode);
+ AddArgument("pase-only", 0, 1, &mPaseOnly);
+ break;
+ case PairingMode::SoftAP:
+ AddArgument("skip-commissioning-complete", 0, 1, &mSkipCommissioningComplete);
+ AddArgument("setup-pin-code", 0, 134217727, &mSetupPINCode);
+ AddArgument("discriminator", 0, 4096, &mDiscriminator);
+ AddArgument("device-remote-ip", &mRemoteAddr);
+ AddArgument("device-remote-port", 0, UINT16_MAX, &mRemotePort);
+ AddArgument("pase-only", 0, 1, &mPaseOnly);
+ break;
+ case PairingMode::AlreadyDiscovered:
+ AddArgument("skip-commissioning-complete", 0, 1, &mSkipCommissioningComplete);
+ AddArgument("setup-pin-code", 0, 134217727, &mSetupPINCode);
+ AddArgument("device-remote-ip", &mRemoteAddr);
+ AddArgument("device-remote-port", 0, UINT16_MAX, &mRemotePort);
+ AddArgument("pase-only", 0, 1, &mPaseOnly);
+ break;
+ case PairingMode::AlreadyDiscoveredByIndex:
+ AddArgument("skip-commissioning-complete", 0, 1, &mSkipCommissioningComplete);
+ AddArgument("setup-pin-code", 0, 134217727, &mSetupPINCode);
+ AddArgument("index", 0, UINT16_MAX, &mIndex);
+ AddArgument("pase-only", 0, 1, &mPaseOnly);
+ break;
+ case PairingMode::AlreadyDiscoveredByIndexWithCode:
+ AddArgument("skip-commissioning-complete", 0, 1, &mSkipCommissioningComplete);
+ AddArgument("payload", &mOnboardingPayload);
+ AddArgument("index", 0, UINT16_MAX, &mIndex);
+ AddArgument("pase-only", 0, 1, &mPaseOnly);
+ break;
+ }
+
+ switch (filterType)
+ {
+ case chip::Dnssd::DiscoveryFilterType::kNone:
+ break;
+ case chip::Dnssd::DiscoveryFilterType::kShortDiscriminator:
+ AddArgument("discriminator", 0, 15, &mDiscoveryFilterCode);
+ break;
+ case chip::Dnssd::DiscoveryFilterType::kLongDiscriminator:
+ AddArgument("discriminator", 0, 4096, &mDiscoveryFilterCode);
+ break;
+ case chip::Dnssd::DiscoveryFilterType::kVendorId:
+ AddArgument("vendor-id", 0, UINT16_MAX, &mDiscoveryFilterCode);
+ break;
+ case chip::Dnssd::DiscoveryFilterType::kCompressedFabricId:
+ AddArgument("fabric-id", 0, UINT64_MAX, &mDiscoveryFilterCode);
+ break;
+ case chip::Dnssd::DiscoveryFilterType::kCommissioningMode:
+ case chip::Dnssd::DiscoveryFilterType::kCommissioner:
+ break;
+ case chip::Dnssd::DiscoveryFilterType::kDeviceType:
+ AddArgument("device-type", 0, UINT16_MAX, &mDiscoveryFilterCode);
+ break;
+ case chip::Dnssd::DiscoveryFilterType::kInstanceName:
+ AddArgument("name", &mDiscoveryFilterInstanceName);
+ break;
+ }
+
+ if (mode != PairingMode::None)
+ {
+ AddArgument("country-code", &mCountryCode,
+ "Country code to use to set the Basic Information cluster's Location attribute");
+
+ // mTimeZoneList is an optional argument managed by TypedComplexArgument mComplex_TimeZones.
+ // Since optional Complex arguments are not currently supported via the <chip::Optional> class,
+ // we explicitly set the kOptional flag.
+ AddArgument("time-zone", &mComplex_TimeZones,
+ "TimeZone list to use when setting Time Synchronization cluster's TimeZone attribute", Argument::kOptional);
+
+ // mDSTOffsetList is an optional argument managed by TypedComplexArgument mComplex_DSTOffsets.
+ // Since optional Complex arguments are not currently supported via the <chip::Optional> class,
+ // we explicitly set the kOptional flag.
+ AddArgument("dst-offset", &mComplex_DSTOffsets,
+ "DSTOffset list to use when setting Time Synchronization cluster's DSTOffset attribute",
+ Argument::kOptional);
+ }
+
+ AddArgument("timeout", 0, UINT16_MAX, &mTimeout);
+ }
+
+ /////////// CHIPCommand Interface /////////
+ CHIP_ERROR RunCommand() override;
+ chip::System::Clock::Timeout GetWaitDuration() const override { return chip::System::Clock::Seconds16(mTimeout.ValueOr(120)); }
+
+ /////////// 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(NodeId deviceId, CHIP_ERROR error) override;
+ void OnICDRegistrationComplete(NodeId deviceId, uint32_t icdCounter) override;
+ void OnICDStayActiveComplete(NodeId deviceId, uint32_t promisedActiveDuration) override;
+
+ /////////// DeviceDiscoveryDelegate Interface /////////
+ void OnDiscoveredDevice(const chip::Dnssd::CommissionNodeData & nodeData) override;
+
+ /////////// DeviceAttestationDelegate /////////
+ chip::Optional<uint16_t> FailSafeExpiryTimeoutSecs() const override;
+ void OnDeviceAttestationCompleted(chip::Controller::DeviceCommissioner * deviceCommissioner, chip::DeviceProxy * device,
+ const chip::Credentials::DeviceAttestationVerifier::AttestationDeviceInfo & info,
+ chip::Credentials::AttestationVerificationResult attestationResult) override;
+
+private:
+ CHIP_ERROR RunInternal(NodeId remoteId);
+ CHIP_ERROR Pair(NodeId remoteId, PeerAddress address);
+ CHIP_ERROR PairWithMdns(NodeId remoteId);
+ CHIP_ERROR PairWithCode(NodeId remoteId);
+ CHIP_ERROR PaseWithCode(NodeId remoteId);
+ CHIP_ERROR PairWithMdnsOrBleByIndex(NodeId remoteId, uint16_t index);
+ CHIP_ERROR PairWithMdnsOrBleByIndexWithCode(NodeId remoteId, uint16_t index);
+ CHIP_ERROR Unpair(NodeId remoteId);
+ chip::Controller::CommissioningParameters GetCommissioningParameters();
+
+ const PairingMode mPairingMode;
+ const PairingNetworkType mNetworkType;
+ const chip::Dnssd::DiscoveryFilterType mFilterType;
+ Command::AddressWithInterface mRemoteAddr;
+ NodeId mNodeId;
+ chip::Optional<uint16_t> mTimeout;
+ chip::Optional<bool> mDiscoverOnce;
+ chip::Optional<bool> mUseOnlyOnNetworkDiscovery;
+ chip::Optional<bool> mPaseOnly;
+ chip::Optional<bool> mSkipCommissioningComplete;
+ chip::Optional<bool> mBypassAttestationVerifier;
+ chip::Optional<std::vector<uint32_t>> mCASEAuthTags;
+ chip::Optional<char *> mCountryCode;
+ chip::Optional<bool> mICDRegistration;
+ chip::Optional<NodeId> mICDCheckInNodeId;
+ chip::Optional<chip::ByteSpan> mICDSymmetricKey;
+ chip::Optional<uint64_t> mICDMonitoredSubject;
+ chip::Optional<uint32_t> mICDStayActiveDurationMsec;
+ chip::app::DataModel::List<chip::app::Clusters::TimeSynchronization::Structs::TimeZoneStruct::Type> mTimeZoneList;
+ TypedComplexArgument<chip::app::DataModel::List<chip::app::Clusters::TimeSynchronization::Structs::TimeZoneStruct::Type>>
+ mComplex_TimeZones;
+ chip::app::DataModel::List<chip::app::Clusters::TimeSynchronization::Structs::DSTOffsetStruct::Type> mDSTOffsetList;
+ TypedComplexArgument<chip::app::DataModel::List<chip::app::Clusters::TimeSynchronization::Structs::DSTOffsetStruct::Type>>
+ mComplex_DSTOffsets;
+
+ uint16_t mRemotePort;
+ uint16_t mDiscriminator;
+ uint32_t mSetupPINCode;
+ uint16_t mIndex;
+ chip::ByteSpan mOperationalDataset;
+ chip::ByteSpan mSSID;
+ chip::ByteSpan mPassword;
+ char * mOnboardingPayload;
+ uint64_t mDiscoveryFilterCode;
+ char * mDiscoveryFilterInstanceName;
+
+ bool mDeviceIsICD;
+ uint8_t mRandomGeneratedICDSymmetricKey[chip::Crypto::kAES_CCM128_Key_Length];
+
+ // For unpair
+ chip::Platform::UniquePtr<chip::Controller::CurrentFabricRemover> mCurrentFabricRemover;
+ chip::Callback::Callback<chip::Controller::OnCurrentFabricRemove> mCurrentFabricRemoveCallback;
+
+ static void OnCurrentFabricRemove(void * context, NodeId remoteNodeId, CHIP_ERROR status);
+ void PersistIcdInfo();
+};
diff --git a/examples/fabric-admin/commands/pairing/ToTLVCert.cpp b/examples/fabric-admin/commands/pairing/ToTLVCert.cpp
new file mode 100644
index 0000000..01f9156
--- /dev/null
+++ b/examples/fabric-admin/commands/pairing/ToTLVCert.cpp
@@ -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.
+ *
+ */
+
+#include "ToTLVCert.h"
+
+#include <credentials/CHIPCert.h>
+#include <lib/support/Base64.h>
+
+#include <string>
+
+constexpr char kBase64Header[] = "base64:";
+constexpr size_t kBase64HeaderLen = ArraySize(kBase64Header) - 1;
+
+CHIP_ERROR ToBase64(const chip::ByteSpan & input, std::string & outputAsPrefixedBase64)
+{
+ chip::Platform::ScopedMemoryBuffer<char> base64String;
+ base64String.Alloc(kBase64HeaderLen + BASE64_ENCODED_LEN(input.size()) + 1);
+ VerifyOrReturnError(base64String.Get() != nullptr, CHIP_ERROR_NO_MEMORY);
+
+ auto encodedLen = chip::Base64Encode(input.data(), static_cast<uint16_t>(input.size()), base64String.Get() + kBase64HeaderLen);
+ if (encodedLen)
+ {
+ memcpy(base64String.Get(), kBase64Header, kBase64HeaderLen);
+ encodedLen = static_cast<uint16_t>(encodedLen + kBase64HeaderLen);
+ }
+ base64String.Get()[encodedLen] = '\0';
+ outputAsPrefixedBase64 = std::string(base64String.Get(), encodedLen);
+
+ return CHIP_NO_ERROR;
+}
+
+CHIP_ERROR ToTLVCert(const chip::ByteSpan & derEncodedCertificate, std::string & tlvCertAsPrefixedBase64)
+{
+ uint8_t chipCertBuffer[chip::Credentials::kMaxCHIPCertLength];
+ chip::MutableByteSpan chipCertBytes(chipCertBuffer);
+ ReturnErrorOnFailure(chip::Credentials::ConvertX509CertToChipCert(derEncodedCertificate, chipCertBytes));
+ ReturnErrorOnFailure(ToBase64(chipCertBytes, tlvCertAsPrefixedBase64));
+ return CHIP_NO_ERROR;
+}
diff --git a/examples/fabric-admin/commands/pairing/ToTLVCert.h b/examples/fabric-admin/commands/pairing/ToTLVCert.h
new file mode 100644
index 0000000..2995647
--- /dev/null
+++ b/examples/fabric-admin/commands/pairing/ToTLVCert.h
@@ -0,0 +1,25 @@
+/*
+ * 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 <lib/support/Span.h>
+#include <string>
+
+CHIP_ERROR ToBase64(const chip::ByteSpan & input, std::string & outputAsPrefixedBase64);
+CHIP_ERROR ToTLVCert(const chip::ByteSpan & derEncodedCertificate, std::string & tlvCertAsPrefixedBase64);
diff --git a/examples/fabric-admin/fabric-admin.gni b/examples/fabric-admin/fabric-admin.gni
new file mode 100644
index 0000000..021ab77
--- /dev/null
+++ b/examples/fabric-admin/fabric-admin.gni
@@ -0,0 +1,22 @@
+# 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/build.gni")
+import("//build_overrides/chip.gni")
+
+declare_args() {
+ # Use a separate eventloop for CHIP tasks
+ config_use_separate_eventloop = true
+ config_use_local_storage = true
+}
diff --git a/examples/fabric-admin/include/CHIPProjectAppConfig.h b/examples/fabric-admin/include/CHIPProjectAppConfig.h
new file mode 100644
index 0000000..b3f85d6
--- /dev/null
+++ b/examples/fabric-admin/include/CHIPProjectAppConfig.h
@@ -0,0 +1,67 @@
+/*
+ *
+ * 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.
+ */
+
+/**
+ * @file
+ * Project configuration for Fabric Admin.
+ *
+ */
+#ifndef CHIPPROJECTCONFIG_H
+#define CHIPPROJECTCONFIG_H
+
+#define CHIP_CONFIG_MAX_FABRICS 17
+
+#define CHIP_CONFIG_EVENT_LOGGING_NUM_EXTERNAL_CALLBACKS 2
+
+// Uncomment this for a large Tunnel MTU.
+// #define CHIP_CONFIG_TUNNEL_INTERFACE_MTU (9000)
+
+// Enable support functions for parsing command-line arguments
+#define CHIP_CONFIG_ENABLE_ARG_PARSER 1
+
+// Use a default pairing code if one hasn't been provisioned in flash.
+#define CHIP_DEVICE_CONFIG_USE_TEST_SETUP_PIN_CODE 20202021
+#define CHIP_DEVICE_CONFIG_USE_TEST_SETUP_DISCRIMINATOR 0xF00
+
+// Enable reading DRBG seed data from /dev/(u)random.
+// This is needed for test applications and the CHIP device manager to function
+// properly when CHIP_CONFIG_RNG_IMPLEMENTATION_CHIPDRBG is enabled.
+#define CHIP_CONFIG_DEV_RANDOM_DRBG_SEED 1
+
+// For convenience, Chip Security Test Mode can be enabled and the
+// requirement for authentication in various protocols can be disabled.
+//
+// WARNING: These options make it possible to circumvent basic Chip security functionality,
+// including message encryption. Because of this they MUST NEVER BE ENABLED IN PRODUCTION BUILDS.
+//
+#define CHIP_CONFIG_SECURITY_TEST_MODE 0
+
+#define CHIP_CONFIG_ENABLE_UPDATE 1
+
+#define CHIP_SYSTEM_CONFIG_PACKETBUFFER_POOL_SIZE 0
+
+#define CHIP_CONFIG_DATA_MANAGEMENT_CLIENT_EXPERIMENTAL 1
+
+#define CHIP_DEVICE_CONFIG_ENABLE_COMMISSIONER_DISCOVERY 1
+
+// Enable some test-only interaction model APIs.
+#define CONFIG_BUILD_FOR_HOST_UNIT_TEST 1
+
+// Allow us, for test purposes, to encode invalid enum values.
+#define CHIP_CONFIG_IM_ENABLE_ENCODING_SENTINEL_ENUM_VALUES 1
+
+#endif /* CHIPPROJECTCONFIG_H */
diff --git a/examples/fabric-admin/main.cpp b/examples/fabric-admin/main.cpp
new file mode 100644
index 0000000..e517c67
--- /dev/null
+++ b/examples/fabric-admin/main.cpp
@@ -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.
+ *
+ */
+
+#include "commands/common/Commands.h"
+
+#include "commands/clusters/SubscriptionsCommands.h"
+#include "commands/interactive/Commands.h"
+#include "commands/pairing/Commands.h"
+#include <zap-generated/cluster/Commands.h>
+
+// ================================================================================
+// Main Code
+// ================================================================================
+int main(int argc, char * argv[])
+{
+ ExampleCredentialIssuerCommands credIssuerCommands;
+ Commands commands;
+
+ registerCommandsInteractive(commands, &credIssuerCommands);
+ registerCommandsPairing(commands, &credIssuerCommands);
+ registerClusters(commands, &credIssuerCommands);
+ registerCommandsSubscriptions(commands, &credIssuerCommands);
+
+ return commands.Run(argc, argv);
+}
diff --git a/examples/fabric-admin/third_party/connectedhomeip b/examples/fabric-admin/third_party/connectedhomeip
new file mode 120000
index 0000000..1b20c9f
--- /dev/null
+++ b/examples/fabric-admin/third_party/connectedhomeip
@@ -0,0 +1 @@
+../../../
\ No newline at end of file