mirror of
https://github.com/espressif/esp-matter.git
synced 2026-04-27 19:13:13 +00:00
Merge branch 'client/add_send_custom_command' into 'main'
client: Add custom commands send APIs and switch the command sending of controller to the new APIs See merge request app-frameworks/esp-matter!521
This commit is contained in:
@@ -1,3 +1,7 @@
|
||||
# 9-November-2023
|
||||
|
||||
- esp_matter_controller: Change the format of the command data field payload for cluster-invoke commands and the attribute value payload for attribute-write commands. Used unified JSON object format for these payloads. Please refer the document of ``Matter controller`` to learn how to construct them.
|
||||
|
||||
# 2-November-2023
|
||||
|
||||
All of the non-volatile attribute values now are stored in the namespace `esp_matter_kvs` with the attribute key base64-encoded of bytes (`endpoint-id`+`cluster-id`+`attribute-id`). For the devices that store the non-volatile attribute values in the previous namespace with previous attribute-key, the values will be moved and the previous keys will be erased.
|
||||
|
||||
@@ -3,6 +3,7 @@ file(GLOB CLUSTER_DIR_LIST true ${MATTER_SDK_PATH}/src/app/clusters/*)
|
||||
|
||||
set(SRC_DIRS_LIST "."
|
||||
"private"
|
||||
"utils"
|
||||
"${MATTER_SDK_PATH}/zzz_generated/app-common/app-common/zap-generated/attributes"
|
||||
"${MATTER_SDK_PATH}/src/app/server"
|
||||
"${MATTER_SDK_PATH}/src/app/util"
|
||||
@@ -17,6 +18,7 @@ foreach(CLUSTER_DIR ${CLUSTER_DIR_LIST})
|
||||
endforeach()
|
||||
|
||||
set(INCLUDE_DIRS_LIST "."
|
||||
"utils"
|
||||
"${MATTER_SDK_PATH}/zzz_generated/app-common"
|
||||
"${MATTER_SDK_PATH}/third_party/nlfaultinjection/include"
|
||||
"${MATTER_SDK_PATH}/src")
|
||||
@@ -26,7 +28,7 @@ if (CONFIG_ESP_MATTER_ENABLE_DATA_MODEL)
|
||||
list(APPEND INCLUDE_DIRS_LIST "zap_common")
|
||||
endif()
|
||||
|
||||
set(REQUIRES_LIST chip bt esp_matter_console nvs_flash app_update esp_secure_cert_mgr mbedtls esp_system openthread)
|
||||
set(REQUIRES_LIST chip bt esp_matter_console nvs_flash app_update esp_secure_cert_mgr mbedtls esp_system openthread json)
|
||||
|
||||
idf_component_register( SRC_DIRS ${SRC_DIRS_LIST}
|
||||
INCLUDE_DIRS ${INCLUDE_DIRS_LIST}
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
#include <esp_matter_core.h>
|
||||
|
||||
#include <app/clusters/bindings/BindingManager.h>
|
||||
#include <json_to_tlv.h>
|
||||
#include <zap-generated/CHIPClusters.h>
|
||||
|
||||
using namespace chip::app::Clusters;
|
||||
@@ -195,6 +196,113 @@ static void send_command_failure_callback(void *context, CHIP_ERROR error)
|
||||
{
|
||||
ESP_LOGI(TAG, "Send command failure: err: %" CHIP_ERROR_FORMAT, error.Format());
|
||||
}
|
||||
|
||||
namespace custom {
|
||||
namespace command {
|
||||
|
||||
using command_data_tag = chip::app::CommandDataIB::Tag;
|
||||
using chip::TLV::ContextTag;
|
||||
using chip::TLV::TLVWriter;
|
||||
|
||||
esp_err_t send_command(void *ctx, peer_device_t *remote_device, const CommandPathParams &command_path,
|
||||
const char *command_data_json_str, custom_command_callback::on_success_callback_t on_success,
|
||||
custom_command_callback::on_error_callback_t on_error,
|
||||
const Optional<uint16_t> &timed_invoke_timeout_ms, const Optional<Timeout> &response_timeout)
|
||||
{
|
||||
if (!remote_device->GetSecureSession().HasValue() || remote_device->GetSecureSession().Value()->IsGroupSession()) {
|
||||
ESP_LOGE(TAG, "Invalid Session Type");
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
if (command_path.mFlags.Has(chip::app::CommandPathFlags::kGroupIdValid)) {
|
||||
ESP_LOGE(TAG, "Invalid CommandPathFlags");
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
auto decoder = chip::Platform::MakeUnique<custom_command_callback>(ctx, on_success, on_error);
|
||||
if (decoder == nullptr) {
|
||||
ESP_LOGE(TAG, "No memory for command callback");
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
auto on_done = [raw_decoder_ptr = decoder.get()](void *context, CommandSender *command_sender) {
|
||||
chip::Platform::Delete(command_sender);
|
||||
chip::Platform::Delete(raw_decoder_ptr);
|
||||
};
|
||||
decoder->set_on_done_callback(on_done);
|
||||
|
||||
auto command_sender = chip::Platform::MakeUnique<CommandSender>(decoder.get(), remote_device->GetExchangeManager(),
|
||||
timed_invoke_timeout_ms.HasValue());
|
||||
if (command_sender == nullptr) {
|
||||
ESP_LOGE(TAG, "No memory for command sender");
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
if (command_sender->PrepareCommand(command_path, /* aStartDataStruct */ false) != CHIP_NO_ERROR) {
|
||||
ESP_LOGE(TAG, "Failed to prepare command");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
TLVWriter *writer = command_sender->GetCommandDataIBTLVWriter();
|
||||
if (writer == nullptr) {
|
||||
ESP_LOGE(TAG, "No TLV writer in command sender");
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
esp_err_t err = json_to_tlv(command_data_json_str, *writer, ContextTag(command_data_tag::kFields));
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to convert json string to TLV");
|
||||
return err;
|
||||
}
|
||||
if (command_sender->FinishCommand(timed_invoke_timeout_ms) != CHIP_NO_ERROR) {
|
||||
ESP_LOGE(TAG, "Failed to finish command");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
if (command_sender->SendCommandRequest(remote_device->GetSecureSession().Value(), response_timeout) !=
|
||||
CHIP_NO_ERROR) {
|
||||
ESP_LOGE(TAG, "Failed to send command request");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
(void)decoder.release();
|
||||
(void)command_sender.release();
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t send_group_command(const uint8_t fabric_index, const CommandPathParams &command_path,
|
||||
const char *command_data_json_str)
|
||||
{
|
||||
if (!command_path.mFlags.Has(chip::app::CommandPathFlags::kGroupIdValid)) {
|
||||
ESP_LOGE(TAG, "Invalid CommandPathFlags");
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
chip::Transport::OutgoingGroupSession session(command_path.mGroupId, fabric_index);
|
||||
chip::Messaging::ExchangeManager &exchange_mgr = chip::Server::GetInstance().GetExchangeManager();
|
||||
auto command_sender = chip::Platform::MakeUnique<chip::app::CommandSender>(nullptr, &exchange_mgr);
|
||||
if (command_sender == nullptr) {
|
||||
ESP_LOGE(TAG, "No memory for command sender");
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
if (command_sender->PrepareCommand(command_path, /* aStartDataStruct */ false) != CHIP_NO_ERROR) {
|
||||
ESP_LOGE(TAG, "Failed to prepare command");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
TLVWriter *writer = command_sender->GetCommandDataIBTLVWriter();
|
||||
if (writer == nullptr) {
|
||||
ESP_LOGE(TAG, "No TLV writer in command sender");
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
esp_err_t err = json_to_tlv(command_data_json_str, *writer, ContextTag(command_data_tag::kFields));
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to convert json string to TLV");
|
||||
return err;
|
||||
}
|
||||
if (command_sender->FinishCommand(chip::NullOptional) != CHIP_NO_ERROR) {
|
||||
ESP_LOGE(TAG, "Failed to finish command");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
if (command_sender->SendGroupCommandRequest(SessionHandle(session)) != CHIP_NO_ERROR) {
|
||||
ESP_LOGE(TAG, "Failed to send command request");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
} // namespace command
|
||||
} // namespace custom
|
||||
|
||||
namespace on_off {
|
||||
namespace command {
|
||||
|
||||
|
||||
@@ -22,6 +22,92 @@ namespace esp_matter {
|
||||
namespace cluster {
|
||||
using client::peer_device_t;
|
||||
|
||||
/** Custom command send APIs
|
||||
*
|
||||
* They can be used for all the commands of all the clusters, including the custom clusters.
|
||||
*/
|
||||
namespace custom {
|
||||
namespace command {
|
||||
|
||||
using chip::Optional;
|
||||
using chip::app::CommandPathParams;
|
||||
using chip::app::CommandSender;
|
||||
using chip::app::ConcreteCommandPath;
|
||||
using chip::app::StatusIB;
|
||||
using chip::Messaging::ExchangeManager;
|
||||
using chip::System::Clock::Timeout;
|
||||
using chip::TLV::TLVReader;
|
||||
|
||||
class custom_command_callback final : public chip::app::CommandSender::Callback {
|
||||
public:
|
||||
using on_success_callback_t =
|
||||
std::function<void(void *, const ConcreteCommandPath &, const StatusIB &, TLVReader *)>;
|
||||
using on_error_callback_t = std::function<void(void *, CHIP_ERROR error)>;
|
||||
using on_done_callback_t = std::function<void(void *, CommandSender *command_sender)>;
|
||||
|
||||
custom_command_callback(void *ctx, on_success_callback_t on_success, on_error_callback_t on_error,
|
||||
on_done_callback_t on_done = {})
|
||||
: on_success_cb(on_success)
|
||||
, on_error_cb(on_error)
|
||||
, on_done_cb(on_done)
|
||||
, context(ctx)
|
||||
{
|
||||
}
|
||||
void set_on_done_callback(on_done_callback_t on_done) { on_done_cb = on_done; }
|
||||
|
||||
private:
|
||||
void OnResponse(CommandSender *command_sender, const ConcreteCommandPath &command_path, const StatusIB &status,
|
||||
TLVReader *response_data) override
|
||||
{
|
||||
if (called_callback) {
|
||||
return;
|
||||
}
|
||||
called_callback = true;
|
||||
if (on_success_cb) {
|
||||
on_success_cb(context, command_path, status, response_data);
|
||||
}
|
||||
}
|
||||
|
||||
void OnError(const CommandSender *command_sender, CHIP_ERROR error) override
|
||||
{
|
||||
if (called_callback) {
|
||||
return;
|
||||
}
|
||||
called_callback = true;
|
||||
if (on_error_cb) {
|
||||
on_error_cb(context, error);
|
||||
}
|
||||
}
|
||||
|
||||
void OnDone(CommandSender *command_sender) override
|
||||
{
|
||||
if (!called_callback) {
|
||||
OnError(command_sender, CHIP_END_OF_TLV);
|
||||
}
|
||||
if (on_done_cb) {
|
||||
on_done_cb(context, command_sender);
|
||||
}
|
||||
}
|
||||
|
||||
on_success_callback_t on_success_cb;
|
||||
on_error_callback_t on_error_cb;
|
||||
on_done_callback_t on_done_cb;
|
||||
bool called_callback = false;
|
||||
void *context;
|
||||
};
|
||||
|
||||
esp_err_t send_command(void *ctx, peer_device_t *remote_device, const CommandPathParams &command_path,
|
||||
const char *command_data_json_str, custom_command_callback::on_success_callback_t on_success,
|
||||
custom_command_callback::on_error_callback_t on_error,
|
||||
const Optional<uint16_t> &timed_invoke_timeout_ms,
|
||||
const Optional<Timeout> &response_timeout = chip::NullOptional);
|
||||
|
||||
esp_err_t send_group_command(const uint8_t fabric_index, const CommandPathParams &command_path,
|
||||
const char *command_data_json_str);
|
||||
|
||||
} // namespace command
|
||||
} // namespace custom
|
||||
|
||||
/** Specific command send APIs
|
||||
*
|
||||
* If some standard command is not present here, it can be added.
|
||||
|
||||
@@ -0,0 +1,449 @@
|
||||
// Copyright 2023 Espressif Systems (Shanghai) PTE LTD
|
||||
//
|
||||
// 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 <algorithm>
|
||||
#include <cJSON.h>
|
||||
#include <esp_check.h>
|
||||
#include <esp_matter_mem.h>
|
||||
#include <json_to_tlv.h>
|
||||
#include <lib/support/Base64.h>
|
||||
#include <lib/support/SafeInt.h>
|
||||
|
||||
#include <stdlib.h>
|
||||
|
||||
using namespace chip;
|
||||
using chip::TLV::TLVElementType;
|
||||
|
||||
constexpr char TAG[] = "JsonToTlv";
|
||||
constexpr uint32_t k_temporary_implicit_profile_id = 0xFF01;
|
||||
|
||||
namespace esp_matter {
|
||||
|
||||
constexpr size_t k_max_json_name_len = 64;
|
||||
|
||||
struct element_context {
|
||||
element_context() {}
|
||||
~element_context() {}
|
||||
char json_name[k_max_json_name_len];
|
||||
TLV::Tag tag = chip::TLV::AnonymousTag();
|
||||
TLV::TLVElementType type;
|
||||
TLV::TLVElementType sub_type;
|
||||
};
|
||||
|
||||
static int compare_by_tag(const void *a, const void *b)
|
||||
{
|
||||
const element_context *element_a = (const element_context *)a;
|
||||
const element_context *element_b = (const element_context *)b;
|
||||
if (TLV::IsContextTag(element_a->tag) == TLV::IsContextTag(element_b->tag)) {
|
||||
return (int)TLV::TagNumFromTag(element_a->tag) - (int)TLV::TagNumFromTag(element_b->tag);
|
||||
}
|
||||
return TLV::IsContextTag(element_a->tag);
|
||||
}
|
||||
|
||||
static size_t get_char_count(const char *str, char ch)
|
||||
{
|
||||
size_t ret = 0;
|
||||
if (!str) {
|
||||
return ret;
|
||||
}
|
||||
for (size_t i = 0; i < strlen(str); ++i) {
|
||||
if (ch == str[i]) {
|
||||
ret++;
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
static bool is_unsigned_integer(const char *str, size_t len)
|
||||
{
|
||||
if (len == 0) {
|
||||
return false;
|
||||
}
|
||||
for (size_t i = 0; i < len; ++i) {
|
||||
if (str[i] > '9' || str[i] < '0') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static esp_err_t type_str_to_tlv_element_type(const char *type_str, size_t len, TLVElementType &type)
|
||||
{
|
||||
if (len == strlen(element_type::k_int8) && strncmp(type_str, element_type::k_int8, len) == 0) {
|
||||
type = TLVElementType::Int8;
|
||||
return ESP_OK;
|
||||
} else if (len == strlen(element_type::k_int16) && strncmp(type_str, element_type::k_int16, len) == 0) {
|
||||
type = TLVElementType::Int16;
|
||||
return ESP_OK;
|
||||
} else if (len == strlen(element_type::k_int32) && strncmp(type_str, element_type::k_int32, len) == 0) {
|
||||
type = TLVElementType::Int32;
|
||||
return ESP_OK;
|
||||
} else if (len == strlen(element_type::k_int64) && strncmp(type_str, element_type::k_int64, len) == 0) {
|
||||
type = TLVElementType::Int64;
|
||||
return ESP_OK;
|
||||
} else if (len == strlen(element_type::k_uint8) && strncmp(type_str, element_type::k_uint8, len) == 0) {
|
||||
type = TLVElementType::UInt8;
|
||||
return ESP_OK;
|
||||
} else if (len == strlen(element_type::k_uint16) && strncmp(type_str, element_type::k_uint16, len) == 0) {
|
||||
type = TLVElementType::UInt16;
|
||||
return ESP_OK;
|
||||
} else if (len == strlen(element_type::k_uint32) && strncmp(type_str, element_type::k_uint32, len) == 0) {
|
||||
type = TLVElementType::UInt32;
|
||||
return ESP_OK;
|
||||
} else if (len == strlen(element_type::k_uint64) && strncmp(type_str, element_type::k_uint64, len) == 0) {
|
||||
type = TLVElementType::UInt64;
|
||||
return ESP_OK;
|
||||
} else if (len == strlen(element_type::k_float) && strncmp(type_str, element_type::k_float, len) == 0) {
|
||||
type = TLVElementType::FloatingPointNumber32;
|
||||
return ESP_OK;
|
||||
} else if (len == strlen(element_type::k_double) && strncmp(type_str, element_type::k_double, len) == 0) {
|
||||
type = TLVElementType::FloatingPointNumber64;
|
||||
return ESP_OK;
|
||||
} else if (len == strlen(element_type::k_bool) && strncmp(type_str, element_type::k_bool, len) == 0) {
|
||||
type = TLVElementType::BooleanFalse;
|
||||
return ESP_OK;
|
||||
} else if (len == strlen(element_type::k_null) && strncmp(type_str, element_type::k_null, len) == 0) {
|
||||
type = TLVElementType::Null;
|
||||
return ESP_OK;
|
||||
} else if (len == strlen(element_type::k_bytes) && strncmp(type_str, element_type::k_bytes, len) == 0) {
|
||||
type = TLVElementType::ByteString_1ByteLength;
|
||||
return ESP_OK;
|
||||
} else if (len == strlen(element_type::k_string) && strncmp(type_str, element_type::k_string, len) == 0) {
|
||||
type = TLVElementType::UTF8String_1ByteLength;
|
||||
return ESP_OK;
|
||||
} else if (len == strlen(element_type::k_array) && strncmp(type_str, element_type::k_array, len) == 0) {
|
||||
type = TLVElementType::Array;
|
||||
return ESP_OK;
|
||||
} else if (len == strlen(element_type::k_struct) && strncmp(type_str, element_type::k_struct, len) == 0) {
|
||||
type = TLVElementType::Structure;
|
||||
return ESP_OK;
|
||||
} else if (len == strlen(element_type::k_empty) && strncmp(type_str, element_type::k_empty, len) == 0) {
|
||||
type = TLVElementType::NotSpecified;
|
||||
return ESP_OK;
|
||||
}
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
static esp_err_t split_json_name(const char *json_name, uint64_t &tag_number, TLVElementType &type,
|
||||
TLVElementType &subtype)
|
||||
{
|
||||
size_t split_char_count = get_char_count(json_name, ':');
|
||||
ESP_RETURN_ON_FALSE(split_char_count == 1 || split_char_count == 2, ESP_ERR_INVALID_ARG, TAG,
|
||||
"Invalid json name format");
|
||||
ESP_RETURN_ON_FALSE(strchr(json_name, ':'), ESP_ERR_INVALID_ARG, TAG, "Invalid json name format");
|
||||
const char *tag_start = split_char_count == 1 ? json_name : strchr(json_name, ':') + 1;
|
||||
ESP_RETURN_ON_FALSE(strchr(tag_start, ':'), ESP_ERR_INVALID_ARG, TAG, "Invalid json name format");
|
||||
const char *type_start = strchr(tag_start, ':') + 1;
|
||||
size_t tag_len = type_start - tag_start - 1;
|
||||
const char *subtype_prev = strchr(type_start, '-');
|
||||
const char *subtype_start = subtype_prev ? subtype_prev + 1 : 0;
|
||||
size_t type_len = subtype_start ? subtype_start - type_start - 1 : strlen(type_start);
|
||||
size_t subtype_len = subtype_start ? strlen(subtype_start) : 0;
|
||||
|
||||
ESP_RETURN_ON_FALSE(is_unsigned_integer(tag_start, tag_len), ESP_ERR_INVALID_ARG, TAG, "Not an unsigned integer");
|
||||
tag_number = strtoull(tag_start, NULL, 10);
|
||||
ESP_RETURN_ON_ERROR(type_str_to_tlv_element_type(type_start, type_len, type), TAG,
|
||||
"Failed to convert json_type_str to tlv element type");
|
||||
ESP_RETURN_ON_FALSE(type != TLVElementType::NotSpecified, ESP_ERR_INVALID_ARG, TAG,
|
||||
"The tlv element type cannot be no-specified");
|
||||
if (type == TLVElementType::Array) {
|
||||
ESP_RETURN_ON_FALSE(subtype_start && subtype_len != 0, ESP_ERR_INVALID_ARG, TAG, "No subtype for array");
|
||||
ESP_RETURN_ON_ERROR(type_str_to_tlv_element_type(subtype_start, subtype_len, subtype), TAG,
|
||||
"Failed to convert json_type_str to tlv element type");
|
||||
} else {
|
||||
subtype = TLVElementType::NotSpecified;
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static esp_err_t internal_convert_tlv_tag(const uint64_t tag_number, TLV::Tag &tag,
|
||||
const uint32_t profile_id = k_temporary_implicit_profile_id)
|
||||
{
|
||||
if (tag_number <= UINT8_MAX) {
|
||||
tag = TLV::ContextTag(static_cast<uint8_t>(tag_number));
|
||||
} else if (tag_number < UINT32_MAX) {
|
||||
tag = TLV::ProfileTag(profile_id, static_cast<uint32_t>(tag_number));
|
||||
} else {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static esp_err_t parse_json_name(const char *name, element_context &element_ctx, uint32_t implicit_profile_id)
|
||||
{
|
||||
uint64_t tag_number = 0;
|
||||
ESP_RETURN_ON_FALSE(name, ESP_ERR_INVALID_ARG, TAG, "json name cannot be NULL");
|
||||
ESP_RETURN_ON_ERROR(split_json_name(name, tag_number, element_ctx.type, element_ctx.sub_type), TAG,
|
||||
"Failed to parse json name");
|
||||
ESP_RETURN_ON_ERROR(internal_convert_tlv_tag(tag_number, element_ctx.tag, implicit_profile_id), TAG,
|
||||
"Failed to convert TLV tag");
|
||||
strncpy(element_ctx.json_name, name, strlen(name));
|
||||
element_ctx.json_name[strlen(name)] = 0;
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static bool is_valid_base64_str(const char *str)
|
||||
{
|
||||
const char *base64_chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||
if (!str) {
|
||||
return false;
|
||||
}
|
||||
size_t len = strlen(str);
|
||||
if (len % 4 != 0) {
|
||||
return false;
|
||||
}
|
||||
size_t padding_len = 0;
|
||||
if (str[len - 1] == '=') {
|
||||
padding_len++;
|
||||
if (str[len - 2] == '=') {
|
||||
padding_len++;
|
||||
}
|
||||
}
|
||||
for (size_t i = 0; i < len - padding_len; ++i) {
|
||||
if (strchr(base64_chars, str[i]) == NULL) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static size_t get_object_element_count(const cJSON *json)
|
||||
{
|
||||
ESP_RETURN_ON_FALSE(json->type == cJSON_Object, 0, TAG, "Invalid type");
|
||||
size_t ret = 0;
|
||||
cJSON *current_element = json->child;
|
||||
while (current_element) {
|
||||
current_element = current_element->next;
|
||||
ret++;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
static esp_err_t encode_tlv_element(const cJSON *val, TLV::TLVWriter &writer, const element_context &element_ctx)
|
||||
{
|
||||
TLV::Tag tag = element_ctx.tag;
|
||||
|
||||
switch (element_ctx.type) {
|
||||
case TLVElementType::Int8: {
|
||||
ESP_RETURN_ON_FALSE(val->type == cJSON_Number, ESP_ERR_INVALID_ARG, TAG, "Invalid type");
|
||||
ESP_RETURN_ON_FALSE(val->valueint <= INT8_MAX && val->valueint >= INT8_MIN, ESP_ERR_INVALID_ARG, TAG,
|
||||
"Invalid range");
|
||||
int8_t int8_val = val->valueint;
|
||||
ESP_RETURN_ON_FALSE(writer.Put(tag, int8_val) == CHIP_NO_ERROR, ESP_FAIL, TAG, "Failed to encode");
|
||||
break;
|
||||
}
|
||||
case TLVElementType::Int16: {
|
||||
ESP_RETURN_ON_FALSE(val->type == cJSON_Number, ESP_ERR_INVALID_ARG, TAG, "Invalid type");
|
||||
ESP_RETURN_ON_FALSE(val->valueint <= INT16_MAX && val->valueint >= INT16_MIN, ESP_ERR_INVALID_ARG, TAG,
|
||||
"Invalid range");
|
||||
int16_t int16_val = val->valueint;
|
||||
ESP_RETURN_ON_FALSE(writer.Put(tag, int16_val) == CHIP_NO_ERROR, ESP_FAIL, TAG, "Failed to encode");
|
||||
break;
|
||||
}
|
||||
case TLVElementType::Int32: {
|
||||
ESP_RETURN_ON_FALSE(val->type == cJSON_Number, ESP_ERR_INVALID_ARG, TAG, "Invalid type");
|
||||
int32_t int32_val = val->valueint;
|
||||
ESP_RETURN_ON_FALSE(writer.Put(tag, int32_val) == CHIP_NO_ERROR, ESP_FAIL, TAG, "Failed to encode");
|
||||
break;
|
||||
}
|
||||
case TLVElementType::Int64: {
|
||||
ESP_RETURN_ON_FALSE(val->type == cJSON_Number, ESP_ERR_INVALID_ARG, TAG, "Invalid type");
|
||||
int64_t int64_val =
|
||||
(val->valueint < INT32_MAX && val->valueint > INT32_MIN) ? val->valueint : (int64_t)val->valuedouble;
|
||||
ESP_RETURN_ON_FALSE(writer.Put(tag, int64_val) == CHIP_NO_ERROR, ESP_FAIL, TAG, "Failed to encode");
|
||||
break;
|
||||
}
|
||||
case TLVElementType::UInt8: {
|
||||
ESP_RETURN_ON_FALSE(val->type == cJSON_Number, ESP_ERR_INVALID_ARG, TAG, "Invalid type");
|
||||
ESP_RETURN_ON_FALSE(val->valueint <= UINT8_MAX && val->valueint >= 0, ESP_ERR_INVALID_ARG, TAG,
|
||||
"Invalid range");
|
||||
uint8_t uint8_val = val->valueint;
|
||||
ESP_RETURN_ON_FALSE(writer.Put(tag, uint8_val) == CHIP_NO_ERROR, ESP_FAIL, TAG, "Failed to encode");
|
||||
break;
|
||||
}
|
||||
case TLVElementType::UInt16: {
|
||||
ESP_RETURN_ON_FALSE(val->type == cJSON_Number, ESP_ERR_INVALID_ARG, TAG, "Invalid type");
|
||||
ESP_RETURN_ON_FALSE(val->valueint <= UINT16_MAX && val->valueint >= 0, ESP_ERR_INVALID_ARG, TAG,
|
||||
"Invalid range");
|
||||
uint16_t uint16_val = val->valueint;
|
||||
ESP_RETURN_ON_FALSE(writer.Put(tag, uint16_val) == CHIP_NO_ERROR, ESP_FAIL, TAG, "Failed to encode");
|
||||
break;
|
||||
}
|
||||
case TLVElementType::UInt32: {
|
||||
ESP_RETURN_ON_FALSE(val->type == cJSON_Number, ESP_ERR_INVALID_ARG, TAG, "Invalid type");
|
||||
ESP_RETURN_ON_FALSE(val->valueint >= 0, ESP_ERR_INVALID_ARG, TAG, "Invalid range");
|
||||
uint32_t uint32_val = val->valueint < INT32_MAX ? val->valueint : (uint32_t)val->valuedouble;
|
||||
ESP_RETURN_ON_FALSE(writer.Put(tag, uint32_val) == CHIP_NO_ERROR, ESP_FAIL, TAG, "Failed to encode");
|
||||
break;
|
||||
}
|
||||
case TLVElementType::UInt64: {
|
||||
ESP_RETURN_ON_FALSE(val->type == cJSON_Number, ESP_ERR_INVALID_ARG, TAG, "Invalid type");
|
||||
ESP_RETURN_ON_FALSE(val->valueint >= 0, ESP_ERR_INVALID_ARG, TAG, "Invalid range");
|
||||
uint64_t uint64_val = val->valueint < INT32_MAX ? val->valueint : (uint64_t)val->valuedouble;
|
||||
ESP_RETURN_ON_FALSE(writer.Put(tag, uint64_val) == CHIP_NO_ERROR, ESP_FAIL, TAG, "Failed to encode");
|
||||
break;
|
||||
}
|
||||
case TLVElementType::FloatingPointNumber32: {
|
||||
if (val->type == cJSON_Number) {
|
||||
float float_val = val->valuedouble;
|
||||
ESP_RETURN_ON_FALSE(writer.Put(tag, float_val) == CHIP_NO_ERROR, ESP_FAIL, TAG, "Failed to encode");
|
||||
} else if (val->type == cJSON_String) {
|
||||
if (strcmp(val->valuestring, element_type::k_floating_point_positive_infinity) == 0) {
|
||||
ESP_RETURN_ON_FALSE(writer.Put(tag, std::numeric_limits<float>::infinity()) == CHIP_NO_ERROR, ESP_FAIL,
|
||||
TAG, "Failed to encode");
|
||||
} else if (strcmp(val->valuestring, element_type::k_floating_point_negative_infinity) == 0) {
|
||||
ESP_RETURN_ON_FALSE(writer.Put(tag, -std::numeric_limits<float>::infinity()) == CHIP_NO_ERROR, ESP_FAIL,
|
||||
TAG, "Failed to encode");
|
||||
} else {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Invalid type");
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case TLVElementType::FloatingPointNumber64: {
|
||||
if (val->type == cJSON_Number) {
|
||||
double double_val = val->valuedouble;
|
||||
ESP_RETURN_ON_FALSE(writer.Put(tag, double_val) == CHIP_NO_ERROR, ESP_FAIL, TAG, "Failed to encode");
|
||||
} else if (val->type == cJSON_String) {
|
||||
if (strcmp(val->valuestring, element_type::k_floating_point_positive_infinity) == 0) {
|
||||
ESP_RETURN_ON_FALSE(writer.Put(tag, std::numeric_limits<double>::infinity()) == CHIP_NO_ERROR, ESP_FAIL,
|
||||
TAG, "Failed to encode");
|
||||
} else if (strcmp(val->valuestring, element_type::k_floating_point_negative_infinity) == 0) {
|
||||
ESP_RETURN_ON_FALSE(writer.Put(tag, -std::numeric_limits<double>::infinity()) == CHIP_NO_ERROR,
|
||||
ESP_FAIL, TAG, "Failed to encode");
|
||||
} else {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Invalid type");
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case TLVElementType::BooleanTrue:
|
||||
case TLVElementType::BooleanFalse: {
|
||||
ESP_RETURN_ON_FALSE(val->type == cJSON_False || val->type == cJSON_True, ESP_ERR_INVALID_ARG, TAG,
|
||||
"Invalid type");
|
||||
bool bool_val = (val->type == cJSON_True);
|
||||
ESP_RETURN_ON_FALSE(writer.Put(tag, bool_val) == CHIP_NO_ERROR, ESP_FAIL, TAG, "Failed to encode");
|
||||
break;
|
||||
}
|
||||
case TLVElementType::ByteString_1ByteLength: {
|
||||
ESP_RETURN_ON_FALSE(val->type == cJSON_String && val->valuestring, ESP_ERR_INVALID_ARG, TAG, "Invalid type");
|
||||
size_t encoded_len = strlen(val->valuestring);
|
||||
ESP_RETURN_ON_FALSE(chip::CanCastTo<uint16_t>(encoded_len), ESP_ERR_INVALID_ARG, TAG, "Invalid type");
|
||||
ESP_RETURN_ON_FALSE(is_valid_base64_str(val->valuestring), ESP_ERR_INVALID_ARG, TAG, "Invalid type");
|
||||
Platform::ScopedMemoryBuffer<uint8_t> byte_str;
|
||||
byte_str.Alloc(BASE64_MAX_DECODED_LEN(static_cast<uint16_t>(encoded_len)));
|
||||
ESP_RETURN_ON_FALSE(byte_str.Get(), ESP_ERR_NO_MEM, TAG, "No memory");
|
||||
auto decoded_len = Base64Decode(val->valuestring, static_cast<uint16_t>(encoded_len), byte_str.Get());
|
||||
ESP_RETURN_ON_FALSE(writer.PutBytes(tag, byte_str.Get(), decoded_len) == CHIP_NO_ERROR, ESP_FAIL, TAG,
|
||||
"Failed to encode");
|
||||
break;
|
||||
}
|
||||
case TLVElementType::UTF8String_1ByteLength: {
|
||||
ESP_RETURN_ON_FALSE(val->type == cJSON_String, ESP_ERR_INVALID_ARG, TAG, "Invalid type");
|
||||
ESP_RETURN_ON_FALSE(writer.PutString(tag, val->valuestring) == CHIP_NO_ERROR, ESP_FAIL, TAG,
|
||||
"Failed to encode");
|
||||
break;
|
||||
}
|
||||
case TLVElementType::Null: {
|
||||
ESP_RETURN_ON_FALSE(val->type == cJSON_NULL, ESP_ERR_INVALID_ARG, TAG, "Invalid type");
|
||||
ESP_RETURN_ON_FALSE(writer.PutNull(tag) == CHIP_NO_ERROR, ESP_FAIL, TAG, "Failed to encode");
|
||||
break;
|
||||
}
|
||||
case TLVElementType::Array: {
|
||||
TLV::TLVType container_type;
|
||||
esp_err_t err = ESP_OK;
|
||||
size_t array_size = cJSON_GetArraySize(val);
|
||||
ESP_RETURN_ON_FALSE(val->type == cJSON_Array, ESP_ERR_INVALID_ARG, TAG, "Invalid type");
|
||||
if (element_ctx.sub_type == TLV::TLVElementType::NotSpecified) {
|
||||
ESP_RETURN_ON_FALSE(array_size == 0, ESP_ERR_INVALID_ARG, TAG, "Invalid array size");
|
||||
}
|
||||
ESP_RETURN_ON_FALSE(writer.StartContainer(tag, TLV::kTLVType_Array, container_type) == CHIP_NO_ERROR, ESP_FAIL,
|
||||
TAG, "Failed to start container");
|
||||
element_context nested_element_ctx;
|
||||
nested_element_ctx.tag = TLV::AnonymousTag();
|
||||
nested_element_ctx.type = element_ctx.sub_type;
|
||||
for (size_t i = 0; i < array_size; ++i) {
|
||||
if ((err = encode_tlv_element(cJSON_GetArrayItem(val, i), writer, nested_element_ctx)) != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to encode");
|
||||
writer.EndContainer(container_type);
|
||||
return err;
|
||||
}
|
||||
}
|
||||
ESP_RETURN_ON_FALSE(writer.EndContainer(container_type) == CHIP_NO_ERROR, ESP_FAIL, TAG, "Failed to end container");
|
||||
break;
|
||||
}
|
||||
case TLVElementType::Structure: {
|
||||
TLV::TLVType container_type;
|
||||
esp_err_t err = ESP_OK;
|
||||
ESP_RETURN_ON_FALSE(val->type == cJSON_Object, ESP_ERR_INVALID_ARG, TAG, "Invalid type");
|
||||
size_t element_count = get_object_element_count(val);
|
||||
auto element_array = std::make_unique<element_context[]>(element_count);
|
||||
ESP_RETURN_ON_FALSE(element_array.get(), ESP_ERR_NO_MEM, TAG, "No memory for element_array");
|
||||
cJSON *element = val->child;
|
||||
size_t element_idx = 0;
|
||||
while (element && element_idx < element_count) {
|
||||
ESP_RETURN_ON_ERROR(parse_json_name(element->string, element_array[element_idx], writer.ImplicitProfileId),
|
||||
TAG, "Failed to parse json name");
|
||||
element = element->next;
|
||||
element_idx++;
|
||||
}
|
||||
qsort(element_array.get(), element_count, sizeof(element_context), compare_by_tag);
|
||||
ESP_RETURN_ON_FALSE(writer.StartContainer(tag, TLV::kTLVType_Structure, container_type) == CHIP_NO_ERROR,
|
||||
ESP_FAIL, TAG, "Failed to start container");
|
||||
for (element_idx = 0; element_idx < element_count; ++element_idx) {
|
||||
if ((err = encode_tlv_element(cJSON_GetObjectItem(val, element_array[element_idx].json_name), writer,
|
||||
element_array[element_idx])) != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to encode");
|
||||
writer.EndContainer(container_type);
|
||||
return err;
|
||||
}
|
||||
}
|
||||
ESP_RETURN_ON_FALSE(writer.EndContainer(container_type) == CHIP_NO_ERROR, ESP_FAIL, TAG, "Failed to end container");
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t json_to_tlv(const char *json_str, chip::TLV::TLVWriter &writer, chip::TLV::Tag tag)
|
||||
{
|
||||
cJSON *json = cJSON_Parse(json_str);
|
||||
if (!json) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
if (json->type != cJSON_Object) {
|
||||
cJSON_Delete(json);
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
element_context element_ctx;
|
||||
element_ctx.type = TLVElementType::Structure;
|
||||
element_ctx.sub_type = TLVElementType::NotSpecified;
|
||||
element_ctx.tag = tag;
|
||||
esp_err_t err = encode_tlv_element(json, writer, element_ctx);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to encode tlv element");
|
||||
}
|
||||
cJSON_Delete(json);
|
||||
return err;
|
||||
}
|
||||
|
||||
} // namespace esp_matter
|
||||
@@ -0,0 +1,58 @@
|
||||
// Copyright 2023 Espressif Systems (Shanghai) PTE LTD
|
||||
//
|
||||
// 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 <esp_err.h>
|
||||
#include <lib/core/TLV.h>
|
||||
#include <string>
|
||||
|
||||
namespace esp_matter {
|
||||
|
||||
namespace element_type {
|
||||
// Supported Data Type
|
||||
const char k_int8[] = "INT8";
|
||||
const char k_int16[] = "INT16";
|
||||
const char k_int32[] = "INT32";
|
||||
const char k_int64[] = "INT64";
|
||||
const char k_uint8[] = "UINT8";
|
||||
const char k_uint16[] = "UINT16";
|
||||
const char k_uint32[] = "UINT32";
|
||||
const char k_uint64[] = "UINT64";
|
||||
const char k_bool[] = "BOOL";
|
||||
const char k_float[] = "FLOAT";
|
||||
const char k_double[] = "DOUBLE";
|
||||
const char k_bytes[] = "BYTES";
|
||||
const char k_string[] = "STRING";
|
||||
const char k_null[] = "NULL";
|
||||
const char k_struct[] = "STRUCT";
|
||||
const char k_array[] = "ARRAY";
|
||||
const char k_empty[] = "?";
|
||||
|
||||
const char k_floating_point_positive_infinity[] = "Infinity";
|
||||
const char k_floating_point_negative_infinity[] = "-Infinity";
|
||||
} // namespace element_type
|
||||
|
||||
/** Convert a JSON object to the given TLVWriter
|
||||
*
|
||||
* @param[in] json_str The JSON string that represents a TLV structure
|
||||
* @param[out] writer The TLV output from the JSON object
|
||||
* @param[in] tag The TLV tag of the TLV structure
|
||||
*
|
||||
* @return ESP_OK on success
|
||||
* @return error in case of failure
|
||||
*/
|
||||
esp_err_t json_to_tlv(const char *json_str, chip::TLV::TLVWriter &writer, chip::TLV::Tag tag);
|
||||
|
||||
} // namespace esp_matter
|
||||
@@ -20,6 +20,14 @@ menu "ESP Matter Controller"
|
||||
help
|
||||
Maximum number of active device the commissioner supports.
|
||||
|
||||
config ESP_MATTER_CONTROLLER_JSON_STRING_BUFFER_LEN
|
||||
int "Max JSON string buffer length"
|
||||
depends on ESP_MATTER_CONTROLLER_ENABLE
|
||||
default 1024
|
||||
help
|
||||
Max JSON string buffer length. This buffer will be used to store the command data field
|
||||
for cluster-invoked command or attribute value for write-attribute command.
|
||||
|
||||
config ESP_MATTER_CONTROLLER_CUSTOM_CLUSTER_ENABLE
|
||||
bool "Enable controller custom cluster"
|
||||
depends on ESP_MATTER_CONTROLLER_ENABLE && !ESP_MATTER_COMMISSIONER_ENABLE
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -16,6 +16,7 @@
|
||||
|
||||
#include <controller/CommissioneeDeviceProxy.h>
|
||||
#include <esp_matter.h>
|
||||
#include <esp_matter_client.h>
|
||||
#include <esp_matter_mem.h>
|
||||
|
||||
namespace esp_matter {
|
||||
@@ -23,74 +24,73 @@ namespace controller {
|
||||
|
||||
using chip::ScopedNodeId;
|
||||
using chip::SessionHandle;
|
||||
using chip::app::ConcreteCommandPath;
|
||||
using chip::app::StatusIB;
|
||||
using chip::Messaging::ExchangeManager;
|
||||
using chip::TLV::TLVReader;
|
||||
using esp_matter::client::peer_device_t;
|
||||
using esp_matter::cluster::custom::command::custom_command_callback;
|
||||
|
||||
constexpr size_t k_max_command_data_str_len = 256;
|
||||
constexpr size_t k_max_command_data_size = 8;
|
||||
|
||||
typedef struct command_data {
|
||||
uint32_t cluster_id;
|
||||
uint32_t command_id;
|
||||
int command_data_count;
|
||||
char command_data_str[k_max_command_data_size][k_max_command_data_str_len];
|
||||
} command_data_t;
|
||||
|
||||
typedef esp_err_t (*cluster_command_handler_t)(command_data_t *command_data, chip::OperationalDeviceProxy *device_proxy,
|
||||
uint16_t endpoint_id);
|
||||
|
||||
typedef esp_err_t (*cluster_group_command_handler_t)(command_data_t *command_data, uint16_t group_id);
|
||||
constexpr char k_empty_command_data_field[] = "{}";
|
||||
constexpr size_t k_command_data_field_buffer_size = CONFIG_ESP_MATTER_CONTROLLER_JSON_STRING_BUFFER_LEN;
|
||||
|
||||
class cluster_command {
|
||||
public:
|
||||
cluster_command(uint64_t destination_id, uint16_t endpoint_id, command_data_t *command_data)
|
||||
cluster_command(uint64_t destination_id, uint16_t endpoint_id, uint32_t cluster_id, uint32_t command_id,
|
||||
const char *command_data_field,
|
||||
custom_command_callback::on_success_callback_t on_success = default_success_fcn,
|
||||
custom_command_callback::on_error_callback_t on_error = default_error_fcn)
|
||||
: m_destination_id(destination_id)
|
||||
, m_endpoint_id(endpoint_id)
|
||||
, m_command_data(command_data)
|
||||
, m_cluster_id(cluster_id)
|
||||
, m_command_id(command_id)
|
||||
, on_device_connected_cb(on_device_connected_fcn, this)
|
||||
, on_device_connection_failure_cb(on_device_connection_failure_fcn, this)
|
||||
, on_success_cb(on_success)
|
||||
, on_error_cb(on_error)
|
||||
{
|
||||
}
|
||||
|
||||
~cluster_command()
|
||||
{
|
||||
if (m_command_data) {
|
||||
esp_matter_mem_free(m_command_data);
|
||||
if (command_data_field) {
|
||||
strncpy(m_command_data_field, command_data_field, k_command_data_field_buffer_size - 1);
|
||||
m_command_data_field[strnlen(command_data_field, k_command_data_field_buffer_size - 1)] = 0;
|
||||
} else {
|
||||
strcpy(m_command_data_field, k_empty_command_data_field);
|
||||
m_command_data_field[strlen(k_empty_command_data_field)] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
~cluster_command() {}
|
||||
|
||||
esp_err_t send_command();
|
||||
|
||||
bool is_group_command() { return chip::IsGroupId(m_destination_id); }
|
||||
|
||||
static void set_unsupported_cluster_command_handler(cluster_command_handler_t handler)
|
||||
{
|
||||
unsupported_cluster_command_handler = handler;
|
||||
}
|
||||
|
||||
static void set_unsupported_cluster_group_command_handler(cluster_group_command_handler_t handler)
|
||||
{
|
||||
unsupported_cluster_group_command_handler = handler;
|
||||
}
|
||||
|
||||
private:
|
||||
uint64_t m_destination_id;
|
||||
uint16_t m_endpoint_id;
|
||||
command_data_t *m_command_data;
|
||||
static cluster_command_handler_t unsupported_cluster_command_handler;
|
||||
static cluster_group_command_handler_t unsupported_cluster_group_command_handler;
|
||||
uint32_t m_cluster_id;
|
||||
uint32_t m_command_id;
|
||||
char m_command_data_field[k_command_data_field_buffer_size];
|
||||
|
||||
static void on_device_connected_fcn(void *context, ExchangeManager &exchangeMgr,
|
||||
const SessionHandle &sessionHandle);
|
||||
static void on_device_connection_failure_fcn(void *context, const ScopedNodeId &peerId, CHIP_ERROR error);
|
||||
|
||||
static void default_success_fcn(void *ctx, const ConcreteCommandPath &command_path, const StatusIB &status,
|
||||
TLVReader *response_data);
|
||||
|
||||
static void default_error_fcn(void *ctx, CHIP_ERROR error);
|
||||
|
||||
static esp_err_t dispatch_group_command(void *context);
|
||||
|
||||
chip::Callback::Callback<chip::OnDeviceConnected> on_device_connected_cb;
|
||||
chip::Callback::Callback<chip::OnDeviceConnectionFailure> on_device_connection_failure_cb;
|
||||
|
||||
custom_command_callback::on_success_callback_t on_success_cb;
|
||||
custom_command_callback::on_error_callback_t on_error_cb;
|
||||
};
|
||||
|
||||
esp_err_t send_invoke_cluster_command(uint64_t node_id, uint16_t endpoint_id, int cmd_data_argc, char **cmd_data_argv);
|
||||
esp_err_t send_invoke_cluster_command(uint64_t node_id, uint16_t endpoint_id, uint32_t cluster_id, uint32_t command_id,
|
||||
const char *command_data_field);
|
||||
|
||||
} // namespace controller
|
||||
} // namespace esp_matter
|
||||
|
||||
@@ -39,7 +39,6 @@ using chip::NodeId;
|
||||
using chip::Platform::ScopedMemoryBufferWithSize;
|
||||
using chip::Inet::IPAddress;
|
||||
using chip::Transport::PeerAddress;
|
||||
using esp_matter::controller::command_data_t;
|
||||
|
||||
const static char *TAG = "controller_console";
|
||||
|
||||
@@ -189,6 +188,7 @@ static esp_err_t controller_pairing_handler(int argc, char **argv)
|
||||
if (argc != 6) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
uint64_t nodeId = string_to_uint64(argv[1]);
|
||||
uint32_t pincode = string_to_uint32(argv[4]);
|
||||
uint16_t disc = string_to_uint16(argv[5]);
|
||||
@@ -300,8 +300,11 @@ static esp_err_t controller_invoke_command_handler(int argc, char **argv)
|
||||
|
||||
uint64_t node_id = string_to_uint64(argv[0]);
|
||||
uint16_t endpoint_id = string_to_uint16(argv[1]);
|
||||
uint32_t cluster_id = string_to_uint32(argv[2]);
|
||||
uint32_t command_id = string_to_uint32(argv[3]);
|
||||
|
||||
return controller::send_invoke_cluster_command(node_id, endpoint_id, argc - 2, argv + 2);
|
||||
return controller::send_invoke_cluster_command(node_id, endpoint_id, cluster_id, command_id,
|
||||
argc > 4 ? argv[4] : NULL);
|
||||
}
|
||||
|
||||
static esp_err_t controller_read_attr_handler(int argc, char **argv)
|
||||
@@ -441,7 +444,10 @@ esp_err_t controller_register_commands()
|
||||
"Send command to the nodes.\n"
|
||||
"\tUsage: controller invoke-cmd [node-id|group-id] [endpoint-id] [cluster-id] [command-id] [payload]\n"
|
||||
"\tNotes: group-id should start with prefix '0xFFFFFFFFFFFF', endpoint-id will be ignored if the fist "
|
||||
"parameter is group-id.",
|
||||
"parameter is group-id.\n"
|
||||
"\tNotes: The payload should be a JSON object that includes all the command data fields defined in the "
|
||||
"SPEC. You can get the format of the payload from "
|
||||
"https://docs.espressif.com/projects/esp-matter/en/latest/esp32/developing.html#cluster-commands",
|
||||
.handler = controller_invoke_command_handler,
|
||||
},
|
||||
{
|
||||
@@ -452,9 +458,12 @@ esp_err_t controller_register_commands()
|
||||
},
|
||||
{
|
||||
.name = "write-attr",
|
||||
.description =
|
||||
"Write attributes of the nodes.\n"
|
||||
"\tUsage: controller write-attr [node-id|group-id] [endpoint-id] [cluster-id] [attr-id] [attr-value]",
|
||||
.description = "Write attributes of the nodes.\n"
|
||||
"\tUsage: controller write-attr [node-id|group-id] [endpoint-id] [cluster-id] [attr-id] "
|
||||
"[attr-value]\n"
|
||||
"\tNotes: attr-value should be a JSON object that contains the attribute value JSON item."
|
||||
"You can get the format of the attr-value from "
|
||||
"https://docs.espressif.com/projects/esp-matter/en/latest/esp32/developing.html#write-attribute-commands",
|
||||
.handler = controller_write_attr_handler,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
#include <esp_check.h>
|
||||
#include <esp_matter_controller_utils.h>
|
||||
#include <esp_matter_controller_write_command.h>
|
||||
#include <json_parser.h>
|
||||
#include <json_to_tlv.h>
|
||||
#if CONFIG_ESP_MATTER_COMMISSIONER_ENABLE
|
||||
#include <esp_matter_commissioner.h>
|
||||
#else
|
||||
@@ -26,52 +26,96 @@ using namespace chip::app::Clusters;
|
||||
using chip::ByteSpan;
|
||||
using chip::DeviceProxy;
|
||||
using chip::app::DataModel::List;
|
||||
using chip::Platform::MakeUnique;
|
||||
using chip::Platform::New;
|
||||
using chip::TLV::ContextTag;
|
||||
using chip::TLV::TLVReader;
|
||||
using chip::TLV::TLVType;
|
||||
using chip::TLV::TLVWriter;
|
||||
using attribute_data_tag = chip::app::AttributeDataIB::Tag;
|
||||
|
||||
static const char *TAG = "write_command";
|
||||
static constexpr size_t k_encoded_buf_size = chip::kMaxAppMessageLen;
|
||||
|
||||
namespace esp_matter {
|
||||
namespace controller {
|
||||
|
||||
template <class T>
|
||||
void write_command<T>::on_device_connected_fcn(void *context, ExchangeManager &exchangeMgr,
|
||||
const SessionHandle &sessionHandle)
|
||||
esp_err_t write_command::encode_attribute_value(uint8_t *encoded_buf, size_t encoded_buf_size,
|
||||
const char *attr_val_json_str, TLVReader &out_reader)
|
||||
{
|
||||
write_command<T> *cmd = (write_command<T> *)context;
|
||||
CHIP_ERROR err = CHIP_NO_ERROR;
|
||||
WriteClient *write_client =
|
||||
New<WriteClient>(&exchangeMgr, &(cmd->get_chunked_write_callback()), chip::NullOptional, false);
|
||||
if (!write_client) {
|
||||
TLVWriter writer;
|
||||
uint32_t encoded_len = 0;
|
||||
TLVReader reader;
|
||||
|
||||
writer.Init(encoded_buf, encoded_buf_size);
|
||||
ESP_RETURN_ON_ERROR(json_to_tlv(attr_val_json_str, writer, chip::TLV::AnonymousTag()), TAG,
|
||||
"Failed to parse attribute value");
|
||||
ESP_RETURN_ON_FALSE(writer.Finalize() == CHIP_NO_ERROR, ESP_FAIL, TAG, "Failed to finalize tlv writer");
|
||||
encoded_len = writer.GetLengthWritten();
|
||||
reader.Init(encoded_buf, encoded_len);
|
||||
ESP_RETURN_ON_FALSE(reader.Next() == CHIP_NO_ERROR, ESP_FAIL, TAG, "Failed to read next");
|
||||
ESP_RETURN_ON_FALSE(reader.GetType() == TLVType::kTLVType_Structure, ESP_ERR_INVALID_ARG, TAG,
|
||||
"The TLV type must be structure");
|
||||
ESP_RETURN_ON_FALSE(reader.OpenContainer(out_reader) == CHIP_NO_ERROR, ESP_FAIL, TAG, "Failed to open container");
|
||||
ESP_RETURN_ON_FALSE(out_reader.Next() == CHIP_NO_ERROR, ESP_FAIL, TAG, "Failed to read next");
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
void write_command::on_device_connected_fcn(void *context, ExchangeManager &exchangeMgr,
|
||||
const SessionHandle &sessionHandle)
|
||||
{
|
||||
write_command *cmd = (write_command *)context;
|
||||
if (cmd->m_attr_path.HasWildcardEndpointId()) {
|
||||
ESP_LOGE(TAG, "Endpoint Id Invalid");
|
||||
chip::Platform::Delete(cmd);
|
||||
return;
|
||||
}
|
||||
ConcreteDataAttributePath path(cmd->m_attr_path.mEndpointId, cmd->m_attr_path.mClusterId,
|
||||
cmd->m_attr_path.mAttributeId);
|
||||
chip::Platform::ScopedMemoryBuffer<uint8_t> encoded_buf;
|
||||
|
||||
auto write_client = MakeUnique<WriteClient>(&exchangeMgr, &(cmd->m_chunked_callback), chip::NullOptional, false);
|
||||
if (write_client == nullptr) {
|
||||
ESP_LOGE(TAG, "Failed to alloc memory for WriteClient");
|
||||
chip::Platform::Delete(cmd);
|
||||
return;
|
||||
}
|
||||
err = write_client->EncodeAttribute(cmd->get_attribute_path(), cmd->get_attribute_val(), chip::NullOptional);
|
||||
if (err != CHIP_NO_ERROR) {
|
||||
ESP_LOGE(TAG, "Failed to Encode Attribute for WriteClient");
|
||||
encoded_buf.Alloc(k_encoded_buf_size);
|
||||
if (!encoded_buf.Get()) {
|
||||
ESP_LOGE(TAG, "Failed to alloc memory for encoded_buf");
|
||||
chip::Platform::Delete(cmd);
|
||||
return;
|
||||
}
|
||||
TLVReader attr_val_reader;
|
||||
if (encode_attribute_value(encoded_buf.Get(), k_encoded_buf_size, cmd->m_attr_val_str, attr_val_reader) != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to encode attribute value to a TLV reader");
|
||||
chip::Platform::Delete(cmd);
|
||||
chip::Platform::Delete(write_client);
|
||||
return;
|
||||
}
|
||||
|
||||
err = write_client->SendWriteRequest(sessionHandle);
|
||||
if (err != CHIP_NO_ERROR) {
|
||||
if (write_client->PutPreencodedAttribute(path, attr_val_reader) != CHIP_NO_ERROR) {
|
||||
ESP_LOGE(TAG, "Failed to put pre-encoded attribute value to WriteClient");
|
||||
chip::Platform::Delete(cmd);
|
||||
return;
|
||||
}
|
||||
|
||||
if (write_client->SendWriteRequest(sessionHandle) != CHIP_NO_ERROR) {
|
||||
ESP_LOGE(TAG, "Failed to Send Write Request");
|
||||
chip::Platform::Delete(cmd);
|
||||
chip::Platform::Delete(write_client);
|
||||
return;
|
||||
}
|
||||
return;
|
||||
// Release the write_client as it will be managed by the callbacks
|
||||
write_client.release();
|
||||
}
|
||||
|
||||
template <class T>
|
||||
void write_command<T>::on_device_connection_failure_fcn(void *context, const ScopedNodeId &peerId, CHIP_ERROR error)
|
||||
void write_command::on_device_connection_failure_fcn(void *context, const ScopedNodeId &peerId, CHIP_ERROR error)
|
||||
{
|
||||
write_command<T> *cmd = (write_command<T> *)context;
|
||||
delete cmd;
|
||||
write_command *cmd = (write_command *)context;
|
||||
chip::Platform::Delete(cmd);
|
||||
return;
|
||||
}
|
||||
|
||||
template <class T>
|
||||
esp_err_t write_command<T>::send_command()
|
||||
esp_err_t write_command::send_command()
|
||||
{
|
||||
#if CONFIG_ESP_MATTER_COMMISSIONER_ENABLE
|
||||
if (CHIP_NO_ERROR ==
|
||||
@@ -89,767 +133,22 @@ esp_err_t write_command<T>::send_command()
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
namespace clusters {
|
||||
namespace on_off {
|
||||
|
||||
static esp_err_t write_attribute(uint64_t node_id, uint16_t endpoint_id, uint32_t attribute_id, char *attribute_val_str)
|
||||
{
|
||||
esp_err_t err = ESP_OK;
|
||||
switch (attribute_id) {
|
||||
case OnOff::Attributes::OnTime::Id:
|
||||
case OnOff::Attributes::OffWaitTime::Id: {
|
||||
write_command<uint16_t> *cmd = New<write_command<uint16_t>>(node_id, endpoint_id, OnOff::Id, attribute_id,
|
||||
string_to_uint16(attribute_val_str));
|
||||
ESP_RETURN_ON_FALSE(cmd, ESP_ERR_NO_MEM, TAG, "Failed to alloc memory for write_command");
|
||||
return cmd->send_command();
|
||||
break;
|
||||
}
|
||||
case OnOff::Attributes::StartUpOnOff::Id: {
|
||||
write_command<uint8_t> *cmd = New<write_command<uint8_t>>(node_id, endpoint_id, OnOff::Id, attribute_id,
|
||||
string_to_uint8(attribute_val_str));
|
||||
ESP_RETURN_ON_FALSE(cmd, ESP_ERR_NO_MEM, TAG, "Failed to alloc memory for write_command");
|
||||
return cmd->send_command();
|
||||
break;
|
||||
}
|
||||
default:
|
||||
err = ESP_ERR_NOT_SUPPORTED;
|
||||
break;
|
||||
}
|
||||
return err;
|
||||
}
|
||||
|
||||
} // namespace on_off
|
||||
|
||||
namespace level_control {
|
||||
|
||||
static esp_err_t write_attribute(uint64_t node_id, uint16_t endpoint_id, uint32_t attribute_id, char *attribute_val_str)
|
||||
{
|
||||
esp_err_t err = ESP_OK;
|
||||
switch (attribute_id) {
|
||||
case LevelControl::Attributes::OnOffTransitionTime::Id:
|
||||
case LevelControl::Attributes::OnTransitionTime::Id:
|
||||
case LevelControl::Attributes::OffTransitionTime::Id: {
|
||||
write_command<uint16_t> *cmd = New<write_command<uint16_t>>(node_id, endpoint_id, LevelControl::Id,
|
||||
attribute_id, string_to_uint16(attribute_val_str));
|
||||
ESP_RETURN_ON_FALSE(cmd, ESP_ERR_NO_MEM, TAG, "Failed to alloc memory for write_command");
|
||||
return cmd->send_command();
|
||||
break;
|
||||
}
|
||||
case LevelControl::Attributes::OnLevel::Id:
|
||||
case LevelControl::Attributes::DefaultMoveRate::Id:
|
||||
case LevelControl::Attributes::Options::Id:
|
||||
case LevelControl::Attributes::StartUpCurrentLevel::Id: {
|
||||
write_command<uint8_t> *cmd = New<write_command<uint8_t>>(node_id, endpoint_id, LevelControl::Id, attribute_id,
|
||||
string_to_uint8(attribute_val_str));
|
||||
ESP_RETURN_ON_FALSE(cmd, ESP_ERR_NO_MEM, TAG, "Failed to alloc memory for write_command");
|
||||
return cmd->send_command();
|
||||
break;
|
||||
}
|
||||
default:
|
||||
err = ESP_ERR_NOT_SUPPORTED;
|
||||
break;
|
||||
}
|
||||
return err;
|
||||
}
|
||||
|
||||
} // namespace level_control
|
||||
|
||||
namespace color_control {
|
||||
|
||||
static esp_err_t write_attribute(uint64_t node_id, uint16_t endpoint_id, uint32_t attribute_id, char *attribute_val_str)
|
||||
{
|
||||
esp_err_t err = ESP_OK;
|
||||
switch (attribute_id) {
|
||||
case ColorControl::Attributes::StartUpColorTemperatureMireds::Id: {
|
||||
write_command<uint16_t> *cmd = New<write_command<uint16_t>>(node_id, endpoint_id, ColorControl::Id,
|
||||
attribute_id, string_to_uint16(attribute_val_str));
|
||||
ESP_RETURN_ON_FALSE(cmd, ESP_ERR_NO_MEM, TAG, "Failed to alloc memory for write_command");
|
||||
return cmd->send_command();
|
||||
break;
|
||||
}
|
||||
case ColorControl::Attributes::Options::Id: {
|
||||
write_command<uint8_t> *cmd = New<write_command<uint8_t>>(node_id, endpoint_id, ColorControl::Id, attribute_id,
|
||||
string_to_uint8(attribute_val_str));
|
||||
ESP_RETURN_ON_FALSE(cmd, ESP_ERR_NO_MEM, TAG, "Failed to alloc memory for write_command");
|
||||
return cmd->send_command();
|
||||
break;
|
||||
}
|
||||
default:
|
||||
err = ESP_ERR_NOT_SUPPORTED;
|
||||
break;
|
||||
}
|
||||
return err;
|
||||
}
|
||||
} // namespace color_control
|
||||
|
||||
namespace access_control {
|
||||
|
||||
using AccessControl::AccessControlEntryAuthModeEnum;
|
||||
using AccessControl::AccessControlEntryPrivilegeEnum;
|
||||
|
||||
constexpr size_t k_max_acl_entries = CHIP_CONFIG_EXAMPLE_ACCESS_CONTROL_MAX_ENTRIES_PER_FABRIC;
|
||||
constexpr size_t k_max_subjects_per_acl = CHIP_CONFIG_EXAMPLE_ACCESS_CONTROL_MAX_SUBJECTS_PER_ENTRY;
|
||||
constexpr size_t k_max_targets_per_acl = CHIP_CONFIG_EXAMPLE_ACCESS_CONTROL_MAX_TARGETS_PER_ENTRY;
|
||||
|
||||
using acl_obj = AccessControl::Structs::AccessControlEntryStruct::Type;
|
||||
using acl_target_obj = AccessControl::Structs::AccessControlTargetStruct::Type;
|
||||
typedef struct acl_attr {
|
||||
acl_obj acl_array[k_max_acl_entries];
|
||||
uint64_t subjects_array[k_max_acl_entries][k_max_subjects_per_acl];
|
||||
acl_target_obj targets_array[k_max_acl_entries][k_max_targets_per_acl];
|
||||
} acl_attr_t;
|
||||
|
||||
static void acl_attr_free(void *ctx)
|
||||
{
|
||||
acl_attr_t *attr_ptr = reinterpret_cast<acl_attr_t *>(ctx);
|
||||
chip::Platform::Delete(attr_ptr);
|
||||
}
|
||||
|
||||
static esp_err_t parse_acl_json(char *json_str, acl_attr_t *acl, size_t *acl_size)
|
||||
{
|
||||
jparse_ctx_t jctx;
|
||||
ESP_RETURN_ON_FALSE(json_parse_start(&jctx, json_str, strlen(json_str)) == 0, ESP_ERR_INVALID_ARG, TAG,
|
||||
"Failed to parse the ACL json string on json_parse_start");
|
||||
size_t acl_index = 0;
|
||||
while (acl_index < k_max_acl_entries && json_arr_get_object(&jctx, acl_index) == 0) {
|
||||
int int_val;
|
||||
// FabricIndex
|
||||
if (json_obj_get_int(&jctx, "fabricIndex", &int_val) == 0) {
|
||||
acl->acl_array[acl_index].fabricIndex = int_val;
|
||||
}
|
||||
// Privilege
|
||||
ESP_RETURN_ON_FALSE(json_obj_get_int(&jctx, "privilege", &int_val) == 0, ESP_ERR_INVALID_ARG, TAG,
|
||||
"Failed to get privilege from the ACL json string");
|
||||
acl->acl_array[acl_index].privilege = AccessControlEntryPrivilegeEnum(int_val);
|
||||
// AuthMode
|
||||
ESP_RETURN_ON_FALSE(json_obj_get_int(&jctx, "authMode", &int_val) == 0, ESP_ERR_INVALID_ARG, TAG,
|
||||
"Failed to get authMode from the ACL json string");
|
||||
acl->acl_array[acl_index].authMode = AccessControlEntryAuthModeEnum(int_val);
|
||||
// Subjects
|
||||
int subjects_num = 0;
|
||||
if (json_obj_get_array(&jctx, "subjects", &subjects_num) == 0 && subjects_num > 0) {
|
||||
ESP_RETURN_ON_FALSE(subjects_num <= k_max_subjects_per_acl, ESP_ERR_INVALID_ARG, TAG,
|
||||
"Failed to get subjects from the ACL json string: Error on subjects_num");
|
||||
for (size_t subj_index = 0; subj_index < subjects_num; ++subj_index) {
|
||||
int64_t subject_val;
|
||||
ESP_RETURN_ON_FALSE(json_arr_get_int64(&jctx, subj_index, &subject_val) == 0, ESP_ERR_INVALID_ARG, TAG,
|
||||
"Failed to get subjects from the ACL json string: Error on subject-%u value",
|
||||
subj_index);
|
||||
acl->subjects_array[acl_index][subj_index] = subject_val;
|
||||
}
|
||||
acl->acl_array[acl_index].subjects.SetNonNull(acl->subjects_array[acl_index], subjects_num);
|
||||
json_obj_leave_array(&jctx);
|
||||
} else {
|
||||
acl->acl_array[acl_index].subjects.SetNull();
|
||||
}
|
||||
// Targets
|
||||
int targets_num = 0;
|
||||
if (json_obj_get_array(&jctx, "targets", &targets_num) == 0 && targets_num > 0) {
|
||||
ESP_RETURN_ON_FALSE(targets_num <= k_max_targets_per_acl, ESP_ERR_INVALID_ARG, TAG,
|
||||
"Failed to get targets from the ACL json string: Error on targets length");
|
||||
for (size_t targ_index = 0; targ_index < targets_num; ++targ_index) {
|
||||
ESP_RETURN_ON_FALSE(json_arr_get_object(&jctx, targ_index) == 0, ESP_ERR_INVALID_ARG, TAG,
|
||||
"Failed to get targets from the ACL json string: Error on targets-%u value",
|
||||
targ_index);
|
||||
int64_t cluster_val, device_type_val;
|
||||
int endpoint_val;
|
||||
bool exist_cluster, exist_endpoint, exist_device_type;
|
||||
|
||||
exist_cluster = json_obj_get_int64(&jctx, "cluster", &cluster_val) == 0;
|
||||
exist_endpoint = json_obj_get_int(&jctx, "endpoint", &endpoint_val) == 0;
|
||||
exist_device_type = json_obj_get_int64(&jctx, "deviceType", &device_type_val) == 0;
|
||||
if ((!exist_cluster && !exist_endpoint && !exist_device_type) ||
|
||||
(exist_endpoint && exist_device_type)) {
|
||||
ESP_LOGE(TAG, "Target-%u value is invalid, skip it", targ_index);
|
||||
json_arr_leave_object(&jctx);
|
||||
continue;
|
||||
}
|
||||
// Cluster
|
||||
if (exist_cluster) {
|
||||
acl->targets_array[acl_index][targ_index].cluster.SetNonNull(static_cast<uint32_t>(cluster_val));
|
||||
} else {
|
||||
acl->targets_array[acl_index][targ_index].cluster.SetNull();
|
||||
}
|
||||
// Endpoint
|
||||
if (exist_endpoint) {
|
||||
acl->targets_array[acl_index][targ_index].endpoint.SetNonNull(static_cast<uint16_t>(endpoint_val));
|
||||
} else {
|
||||
acl->targets_array[acl_index][targ_index].endpoint.SetNull();
|
||||
}
|
||||
// DeviceType
|
||||
if (exist_device_type) {
|
||||
acl->targets_array[acl_index][targ_index].deviceType.SetNonNull(
|
||||
static_cast<uint32_t>(device_type_val));
|
||||
} else {
|
||||
acl->targets_array[acl_index][targ_index].deviceType.SetNull();
|
||||
}
|
||||
json_arr_leave_object(&jctx);
|
||||
}
|
||||
acl->acl_array[acl_index].targets.SetNonNull(acl->targets_array[acl_index], targets_num);
|
||||
json_obj_leave_array(&jctx);
|
||||
} else {
|
||||
acl->acl_array[acl_index].targets.SetNull();
|
||||
}
|
||||
// Leave Object
|
||||
json_arr_leave_object(&jctx);
|
||||
acl_index++;
|
||||
}
|
||||
*acl_size = acl_index;
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
// The extension data may be used to store arbitrary TLV-encoded data related to a fabric’s ACL Entries.
|
||||
constexpr size_t k_max_extension_entries = CHIP_CONFIG_EXAMPLE_ACCESS_CONTROL_MAX_ENTRIES_PER_FABRIC;
|
||||
constexpr size_t k_max_extension_data_len = 128;
|
||||
|
||||
using extension_obj = AccessControl::Structs::AccessControlExtensionStruct::Type;
|
||||
typedef struct extension_attr {
|
||||
extension_obj extension_array[k_max_extension_entries];
|
||||
uint8_t data_array[k_max_extension_entries][k_max_extension_data_len];
|
||||
} extension_attr_t;
|
||||
|
||||
static void extension_attr_free(void *ctx)
|
||||
{
|
||||
extension_attr_t *attr = reinterpret_cast<extension_attr_t *>(ctx);
|
||||
chip::Platform::Delete(attr);
|
||||
}
|
||||
|
||||
static esp_err_t parse_extension_json(char *json_str, extension_attr_t *extension, size_t *extension_size)
|
||||
{
|
||||
jparse_ctx_t jctx;
|
||||
ESP_RETURN_ON_FALSE(json_parse_start(&jctx, json_str, strlen(json_str)) == 0, ESP_ERR_INVALID_ARG, TAG,
|
||||
"Failed to parse the Extension json string on json_parse_start");
|
||||
size_t index = 0;
|
||||
while (index < k_max_extension_entries && json_arr_get_object(&jctx, index) == 0) {
|
||||
int fabric_index;
|
||||
if (json_obj_get_int(&jctx, "fabricIndex", &fabric_index) == 0) {
|
||||
extension->extension_array[index].fabricIndex = fabric_index;
|
||||
}
|
||||
|
||||
char data_oct_str[k_max_extension_data_len * 2 + 1] = {0};
|
||||
if (json_obj_get_string(&jctx, "data", data_oct_str, k_max_extension_data_len * 2 + 1) != 0) {
|
||||
ESP_LOGE(TAG, "Failed to get data from the Extension json string");
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
} else {
|
||||
size_t data_len = oct_str_to_byte_arr(data_oct_str, extension->data_array[index]);
|
||||
ESP_RETURN_ON_FALSE(data_len > 0, ESP_ERR_INVALID_ARG, TAG,
|
||||
"Failed to convert the data octstring to byte array");
|
||||
extension->extension_array[index].data = ByteSpan(extension->data_array[index], data_len);
|
||||
}
|
||||
json_arr_leave_object(&jctx);
|
||||
index++;
|
||||
}
|
||||
*extension_size = index;
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static esp_err_t write_attribute(uint64_t node_id, uint16_t endpoint_id, uint32_t attribute_id, char *attribute_val_str)
|
||||
{
|
||||
esp_err_t err = ESP_OK;
|
||||
switch (attribute_id) {
|
||||
case AccessControl::Attributes::Acl::Id: {
|
||||
size_t acl_size = 0;
|
||||
acl_attr_t *attr_val = New<acl_attr_t>();
|
||||
ESP_RETURN_ON_FALSE(attr_val, ESP_ERR_NO_MEM, TAG, "Failed to alloc acl_attr_t");
|
||||
ESP_RETURN_ON_ERROR(parse_acl_json(attribute_val_str, attr_val, &acl_size), TAG,
|
||||
"Failed to parse the ACL json string");
|
||||
List<acl_obj> access_control_list(attr_val->acl_array, acl_size);
|
||||
write_command<List<acl_obj>> *cmd = New<write_command<List<acl_obj>>>(node_id, endpoint_id, AccessControl::Id,
|
||||
attribute_id, access_control_list);
|
||||
ESP_RETURN_ON_FALSE(cmd, ESP_ERR_NO_MEM, TAG, "Failed to alloc memory for write_command");
|
||||
cmd->set_attribute_free_handler(acl_attr_free, attr_val);
|
||||
return cmd->send_command();
|
||||
break;
|
||||
}
|
||||
case AccessControl::Attributes::Extension::Id: {
|
||||
size_t extension_size = 0;
|
||||
extension_attr_t *attr_val = New<extension_attr_t>();
|
||||
ESP_RETURN_ON_FALSE(attr_val, ESP_ERR_NO_MEM, TAG, "Failed to alloc extension_attr_t");
|
||||
ESP_RETURN_ON_ERROR(parse_extension_json(attribute_val_str, attr_val, &extension_size), TAG,
|
||||
"Failed to parse the Extension json string");
|
||||
List<extension_obj> extension_list(attr_val->extension_array, extension_size);
|
||||
write_command<List<extension_obj>> *cmd = New<write_command<List<extension_obj>>>(
|
||||
node_id, endpoint_id, AccessControl::Id, attribute_id, extension_list);
|
||||
ESP_RETURN_ON_FALSE(cmd, ESP_ERR_NO_MEM, TAG, "Failed to alloc memory for write_command");
|
||||
cmd->set_attribute_free_handler(extension_attr_free, attr_val);
|
||||
return cmd->send_command();
|
||||
break;
|
||||
}
|
||||
default:
|
||||
err = ESP_ERR_NOT_SUPPORTED;
|
||||
break;
|
||||
}
|
||||
return err;
|
||||
}
|
||||
|
||||
} // namespace access_control
|
||||
|
||||
namespace binding {
|
||||
using binding_obj = Binding::Structs::TargetStruct::Type;
|
||||
typedef struct binding_attr {
|
||||
binding_obj binding_array[CONFIG_MAX_BINDINGS];
|
||||
} binding_attr_t;
|
||||
|
||||
static void binding_attr_free(void *ctx)
|
||||
{
|
||||
binding_attr_t *attr_ptr = reinterpret_cast<binding_attr_t *>(ctx);
|
||||
chip::Platform::Delete(attr_ptr);
|
||||
}
|
||||
|
||||
static esp_err_t parse_binding_json(char *json_str, binding_attr_t *binding, size_t *binding_size)
|
||||
{
|
||||
jparse_ctx_t jctx;
|
||||
ESP_RETURN_ON_FALSE(json_parse_start(&jctx, json_str, strlen(json_str)) == 0, ESP_ERR_INVALID_ARG, TAG,
|
||||
"Failed to parse the Binding json string on json_parse_start");
|
||||
size_t index = 0;
|
||||
while (index < CONFIG_MAX_BINDINGS && json_arr_get_object(&jctx, index) == 0) {
|
||||
int int_val;
|
||||
int64_t int64_val;
|
||||
// Fabric
|
||||
if (json_obj_get_int(&jctx, "fabricIndex", &int_val) == 0) {
|
||||
binding->binding_array[index].fabricIndex = int_val;
|
||||
}
|
||||
|
||||
if (json_obj_get_int64(&jctx, "node", &int64_val) == 0) {
|
||||
// Unicast Binding
|
||||
binding->binding_array[index].node.SetValue(int64_val);
|
||||
binding->binding_array[index].group.ClearValue();
|
||||
|
||||
ESP_RETURN_ON_FALSE(json_obj_get_int(&jctx, "endpoint", &int_val) == 0, ESP_ERR_INVALID_ARG, TAG,
|
||||
"Failed to get endpoint from the Binding json string");
|
||||
binding->binding_array[index].endpoint.SetValue(int_val);
|
||||
|
||||
ESP_RETURN_ON_FALSE(json_obj_get_int(&jctx, "cluster", &int_val) == 0, ESP_ERR_INVALID_ARG, TAG,
|
||||
"Failed to get cluster from the Binding json string");
|
||||
binding->binding_array[index].cluster.SetValue(int_val);
|
||||
} else if (json_obj_get_int64(&jctx, "group", &int64_val) == 0) {
|
||||
// Group binding
|
||||
binding->binding_array[index].group.SetValue(int64_val);
|
||||
binding->binding_array[index].node.ClearValue();
|
||||
binding->binding_array[index].endpoint.ClearValue();
|
||||
binding->binding_array[index].cluster.ClearValue();
|
||||
} else {
|
||||
ESP_LOGE(TAG, "The Binding json string is invalid");
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
json_arr_leave_object(&jctx);
|
||||
index++;
|
||||
}
|
||||
*binding_size = index;
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static esp_err_t write_attribute(uint64_t node_id, uint16_t endpoint_id, uint32_t attribute_id, char *attribute_val_str)
|
||||
{
|
||||
esp_err_t err = ESP_OK;
|
||||
switch (attribute_id) {
|
||||
case Binding::Attributes::Binding::Id: {
|
||||
size_t binding_size = 0;
|
||||
binding_attr_t *attr_val = chip::Platform::New<binding_attr_t>();
|
||||
ESP_RETURN_ON_FALSE(attr_val, ESP_ERR_NO_MEM, TAG, "Failed to alloc binding_attr_t");
|
||||
ESP_RETURN_ON_ERROR(parse_binding_json(attribute_val_str, attr_val, &binding_size), TAG,
|
||||
"Failed to parse the Binding json string");
|
||||
List<binding_obj> binding_list(attr_val->binding_array, binding_size);
|
||||
write_command<List<binding_obj>> *cmd =
|
||||
New<write_command<List<binding_obj>>>(node_id, endpoint_id, Binding::Id, attribute_id, binding_list);
|
||||
ESP_RETURN_ON_FALSE(cmd, ESP_ERR_NO_MEM, TAG, "Failed to alloc memory for write_command");
|
||||
cmd->set_attribute_free_handler(binding_attr_free, attr_val);
|
||||
return cmd->send_command();
|
||||
break;
|
||||
}
|
||||
default:
|
||||
err = ESP_ERR_NOT_SUPPORTED;
|
||||
break;
|
||||
}
|
||||
return err;
|
||||
}
|
||||
|
||||
} // namespace binding
|
||||
|
||||
namespace group_key_management {
|
||||
|
||||
using group_key_map_obj = GroupKeyManagement::Structs::GroupKeyMapStruct::Type;
|
||||
|
||||
constexpr size_t k_max_group_key_map_size = CHIP_CONFIG_MAX_GROUP_KEYS_PER_FABRIC;
|
||||
|
||||
typedef struct group_key_map_attr {
|
||||
group_key_map_obj group_key_map_array[k_max_group_key_map_size];
|
||||
} group_key_map_attr_t;
|
||||
|
||||
static void group_key_map_attr_free(void *ctx)
|
||||
{
|
||||
group_key_map_attr_t *attr_ptr = reinterpret_cast<group_key_map_attr_t *>(ctx);
|
||||
chip::Platform::Delete(attr_ptr);
|
||||
}
|
||||
|
||||
static esp_err_t parse_group_key_map_json(char *json_str, group_key_map_attr_t *group_key_map,
|
||||
size_t *group_key_map_size)
|
||||
{
|
||||
jparse_ctx_t jctx;
|
||||
ESP_RETURN_ON_FALSE(json_parse_start(&jctx, json_str, strlen(json_str)) == 0, ESP_ERR_INVALID_ARG, TAG,
|
||||
"Group key map json string is wrong");
|
||||
size_t index = 0;
|
||||
while (index < k_max_group_key_map_size && json_arr_get_object(&jctx, index) == 0) {
|
||||
int int_val;
|
||||
// Fabric
|
||||
if (json_obj_get_int(&jctx, "fabricIndex", &int_val) == 0) {
|
||||
group_key_map->group_key_map_array[index].fabricIndex = int_val;
|
||||
}
|
||||
ESP_RETURN_ON_FALSE(json_obj_get_int(&jctx, "groupId", &int_val) == 0, ESP_ERR_INVALID_ARG, TAG,
|
||||
"Failed to get groupId");
|
||||
group_key_map->group_key_map_array[index].groupId = int_val;
|
||||
ESP_RETURN_ON_FALSE(json_obj_get_int(&jctx, "groupKeySetID", &int_val) == 0, ESP_ERR_INVALID_ARG, TAG,
|
||||
"Failed to get groupKeySetId");
|
||||
group_key_map->group_key_map_array[index].groupKeySetID = int_val;
|
||||
json_arr_leave_object(&jctx);
|
||||
index++;
|
||||
}
|
||||
*group_key_map_size = index;
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static esp_err_t write_attribute(uint64_t node_id, uint16_t endpoint_id, uint32_t attribute_id, char *attribute_val_str)
|
||||
{
|
||||
esp_err_t err = ESP_OK;
|
||||
switch (attribute_id) {
|
||||
case GroupKeyManagement::Attributes::GroupKeyMap::Id: {
|
||||
size_t group_key_map_size = 0;
|
||||
group_key_map_attr_t *attr_val = chip::Platform::New<group_key_map_attr_t>();
|
||||
ESP_RETURN_ON_FALSE(attr_val, ESP_ERR_NO_MEM, TAG, "Failed to alloc group_key_map_attr_t");
|
||||
ESP_RETURN_ON_ERROR(parse_group_key_map_json(attribute_val_str, attr_val, &group_key_map_size), TAG,
|
||||
"Failed to parse group_key_map json string");
|
||||
List<group_key_map_obj> group_key_map_list(attr_val->group_key_map_array, group_key_map_size);
|
||||
write_command<List<group_key_map_obj>> *cmd = New<write_command<List<group_key_map_obj>>>(
|
||||
node_id, endpoint_id, GroupKeyManagement::Id, attribute_id, group_key_map_list);
|
||||
ESP_RETURN_ON_FALSE(cmd, ESP_ERR_NO_MEM, TAG, "Failed to alloc memory for write_command");
|
||||
cmd->set_attribute_free_handler(group_key_map_attr_free, attr_val);
|
||||
return cmd->send_command();
|
||||
break;
|
||||
}
|
||||
default:
|
||||
err = ESP_ERR_NOT_SUPPORTED;
|
||||
break;
|
||||
}
|
||||
return err;
|
||||
}
|
||||
|
||||
} // namespace group_key_management
|
||||
|
||||
namespace identify {
|
||||
|
||||
static esp_err_t write_attribute(uint64_t node_id, uint16_t endpoint_id, uint32_t attribute_id, char *attribute_val_str)
|
||||
{
|
||||
if (attribute_id == Identify::Attributes::IdentifyTime::Id) {
|
||||
write_command<uint16_t> *cmd = New<write_command<uint16_t>>(node_id, endpoint_id, Identify::Id, attribute_id,
|
||||
string_to_uint16(attribute_val_str));
|
||||
ESP_RETURN_ON_FALSE(cmd, ESP_ERR_NO_MEM, TAG, "Failed to alloc memory for write_command");
|
||||
return cmd->send_command();
|
||||
}
|
||||
return ESP_ERR_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
} // namespace identify
|
||||
|
||||
namespace thermostat {
|
||||
|
||||
static esp_err_t write_attribute(uint64_t node_id, uint16_t endpoint_id, uint32_t attribute_id, char *attribute_val_str)
|
||||
{
|
||||
esp_err_t err = ESP_OK;
|
||||
switch (attribute_id) {
|
||||
// uint8 value type
|
||||
case Thermostat::Attributes::HVACSystemTypeConfiguration::Id:
|
||||
case Thermostat::Attributes::RemoteSensing::Id:
|
||||
case Thermostat::Attributes::ControlSequenceOfOperation::Id:
|
||||
case Thermostat::Attributes::SystemMode::Id:
|
||||
case Thermostat::Attributes::TemperatureSetpointHold::Id:
|
||||
case Thermostat::Attributes::ThermostatProgrammingOperationMode::Id:
|
||||
case Thermostat::Attributes::OccupiedSetback::Id:
|
||||
case Thermostat::Attributes::UnoccupiedSetback::Id:
|
||||
case Thermostat::Attributes::EmergencyHeatDelta::Id:
|
||||
case Thermostat::Attributes::ACType::Id:
|
||||
case Thermostat::Attributes::ACRefrigerantType::Id:
|
||||
case Thermostat::Attributes::ACCompressorType::Id:
|
||||
case Thermostat::Attributes::ACLouverPosition::Id:
|
||||
case Thermostat::Attributes::ACCapacityformat::Id: {
|
||||
write_command<uint8_t> *cmd = New<write_command<uint8_t>>(node_id, endpoint_id, Thermostat::Id, attribute_id,
|
||||
string_to_uint8(attribute_val_str));
|
||||
ESP_RETURN_ON_FALSE(cmd, ESP_ERR_NO_MEM, TAG, "Failed to alloc memory for write_command");
|
||||
err = cmd->send_command();
|
||||
break;
|
||||
}
|
||||
// int8 value type
|
||||
case Thermostat::Attributes::LocalTemperatureCalibration::Id:
|
||||
case Thermostat::Attributes::MinSetpointDeadBand::Id: {
|
||||
write_command<int8_t> *cmd = New<write_command<int8_t>>(node_id, endpoint_id, Thermostat::Id, attribute_id,
|
||||
string_to_int8(attribute_val_str));
|
||||
ESP_RETURN_ON_FALSE(cmd, ESP_ERR_NO_MEM, TAG, "Failed to alloc memory for write_command");
|
||||
err = cmd->send_command();
|
||||
break;
|
||||
}
|
||||
// int16 value type
|
||||
case Thermostat::Attributes::OccupiedCoolingSetpoint::Id:
|
||||
case Thermostat::Attributes::OccupiedHeatingSetpoint::Id:
|
||||
case Thermostat::Attributes::UnoccupiedCoolingSetpoint::Id:
|
||||
case Thermostat::Attributes::UnoccupiedHeatingSetpoint::Id:
|
||||
case Thermostat::Attributes::MinHeatSetpointLimit::Id:
|
||||
case Thermostat::Attributes::MaxHeatSetpointLimit::Id:
|
||||
case Thermostat::Attributes::MinCoolSetpointLimit::Id:
|
||||
case Thermostat::Attributes::MaxCoolSetpointLimit::Id: {
|
||||
write_command<int16_t> *cmd = New<write_command<int16_t>>(node_id, endpoint_id, Thermostat::Id, attribute_id,
|
||||
string_to_int16(attribute_val_str));
|
||||
ESP_RETURN_ON_FALSE(cmd, ESP_ERR_NO_MEM, TAG, "Failed to alloc memory for write_command");
|
||||
err = cmd->send_command();
|
||||
break;
|
||||
}
|
||||
// uint16 value type
|
||||
case Thermostat::Attributes::TemperatureSetpointHoldDuration::Id:
|
||||
case Thermostat::Attributes::ACCapacity::Id: {
|
||||
write_command<uint16_t> *cmd = New<write_command<uint16_t>>(node_id, endpoint_id, Thermostat::Id, attribute_id,
|
||||
string_to_uint16(attribute_val_str));
|
||||
ESP_RETURN_ON_FALSE(cmd, ESP_ERR_NO_MEM, TAG, "Failed to alloc memory for write_command");
|
||||
err = cmd->send_command();
|
||||
break;
|
||||
}
|
||||
// uint32 value type
|
||||
case Thermostat::Attributes::ACErrorCode::Id: {
|
||||
write_command<uint32_t> *cmd = New<write_command<uint32_t>>(node_id, endpoint_id, Thermostat::Id, attribute_id,
|
||||
string_to_uint32(attribute_val_str));
|
||||
ESP_RETURN_ON_FALSE(cmd, ESP_ERR_NO_MEM, TAG, "Failed to alloc memory for write_command");
|
||||
err = cmd->send_command();
|
||||
break;
|
||||
}
|
||||
default:
|
||||
err = ESP_ERR_NOT_SUPPORTED;
|
||||
break;
|
||||
}
|
||||
return err;
|
||||
}
|
||||
|
||||
} // namespace thermostat
|
||||
|
||||
namespace door_lock {
|
||||
|
||||
constexpr size_t k_max_language_str_len = 3;
|
||||
|
||||
static void language_str_free(void *ctx)
|
||||
{
|
||||
chip::Platform::MemoryFree(ctx);
|
||||
}
|
||||
|
||||
static esp_err_t write_attribute(uint64_t node_id, uint16_t endpoint_id, uint32_t attribute_id, char *attribute_val_str)
|
||||
{
|
||||
esp_err_t err = ESP_OK;
|
||||
switch (attribute_id) {
|
||||
// uint32 value type
|
||||
case DoorLock::Attributes::DoorOpenEvents::Id:
|
||||
case DoorLock::Attributes::DoorClosedEvents::Id:
|
||||
case DoorLock::Attributes::AutoRelockTime::Id: {
|
||||
write_command<uint32_t> *cmd = New<write_command<uint32_t>>(node_id, endpoint_id, DoorLock::Id, attribute_id,
|
||||
string_to_uint32(attribute_val_str));
|
||||
ESP_RETURN_ON_FALSE(cmd, ESP_ERR_NO_MEM, TAG, "Failed to alloc memory for write_command");
|
||||
err = cmd->send_command();
|
||||
break;
|
||||
}
|
||||
// uint16 value type
|
||||
case DoorLock::Attributes::OpenPeriod::Id:
|
||||
case DoorLock::Attributes::ExpiringUserTimeout::Id: {
|
||||
write_command<uint16_t> *cmd = New<write_command<uint16_t>>(node_id, endpoint_id, DoorLock::Id, attribute_id,
|
||||
string_to_uint16(attribute_val_str));
|
||||
ESP_RETURN_ON_FALSE(cmd, ESP_ERR_NO_MEM, TAG, "Failed to alloc memory for write_command");
|
||||
err = cmd->send_command();
|
||||
break;
|
||||
}
|
||||
// string value type
|
||||
case DoorLock::Attributes::Language::Id: {
|
||||
char *language_buf = static_cast<char *>(chip::Platform::MemoryAlloc(k_max_language_str_len));
|
||||
if (!language_buf) {
|
||||
ESP_LOGE(TAG, "Failed to alloc memory for language_buf");
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
strncpy(language_buf, attribute_val_str, strnlen(attribute_val_str, k_max_language_str_len - 1));
|
||||
language_buf[k_max_language_str_len - 1] = 0;
|
||||
write_command<chip::CharSpan> *cmd = New<write_command<chip::CharSpan>>(
|
||||
node_id, endpoint_id, DoorLock::Id, attribute_id, chip::CharSpan(language_buf, strlen(language_buf)));
|
||||
ESP_RETURN_ON_FALSE(cmd, ESP_ERR_NO_MEM, TAG, "Failed to alloc memory for write_command");
|
||||
cmd->set_attribute_free_handler(language_str_free, language_buf);
|
||||
err = cmd->send_command();
|
||||
break;
|
||||
}
|
||||
// uint8 value type
|
||||
case DoorLock::Attributes::LEDSettings::Id:
|
||||
case DoorLock::Attributes::SoundVolume::Id:
|
||||
case DoorLock::Attributes::OperatingMode::Id:
|
||||
case DoorLock::Attributes::LocalProgrammingFeatures::Id:
|
||||
case DoorLock::Attributes::WrongCodeEntryLimit::Id:
|
||||
case DoorLock::Attributes::UserCodeTemporaryDisableTime::Id: {
|
||||
write_command<uint8_t> *cmd = New<write_command<uint8_t>>(node_id, endpoint_id, DoorLock::Id, attribute_id,
|
||||
string_to_uint8(attribute_val_str));
|
||||
ESP_RETURN_ON_FALSE(cmd, ESP_ERR_NO_MEM, TAG, "Failed to alloc memory for write_command");
|
||||
err = cmd->send_command();
|
||||
break;
|
||||
}
|
||||
// boolean value type
|
||||
case DoorLock::Attributes::EnableLocalProgramming::Id:
|
||||
case DoorLock::Attributes::EnableOneTouchLocking::Id:
|
||||
case DoorLock::Attributes::EnableInsideStatusLED::Id:
|
||||
case DoorLock::Attributes::EnablePrivacyModeButton::Id:
|
||||
case DoorLock::Attributes::SendPINOverTheAir::Id:
|
||||
case DoorLock::Attributes::RequirePINforRemoteOperation::Id: {
|
||||
write_command<bool> *cmd = New<write_command<bool>>(node_id, endpoint_id, DoorLock::Id, attribute_id,
|
||||
string_to_bool(attribute_val_str));
|
||||
ESP_RETURN_ON_FALSE(cmd, ESP_ERR_NO_MEM, TAG, "Failed to alloc memory for write_command");
|
||||
err = cmd->send_command();
|
||||
break;
|
||||
}
|
||||
default:
|
||||
err = ESP_ERR_NOT_SUPPORTED;
|
||||
break;
|
||||
}
|
||||
return err;
|
||||
}
|
||||
} // namespace door_lock
|
||||
|
||||
namespace occupancy_sensing {
|
||||
|
||||
static esp_err_t write_attribute(uint64_t node_id, uint16_t endpoint_id, uint32_t attribute_id, char *attribute_val_str)
|
||||
{
|
||||
esp_err_t err = ESP_OK;
|
||||
switch (attribute_id) {
|
||||
case OccupancySensing::Attributes::PIROccupiedToUnoccupiedDelay::Id:
|
||||
case OccupancySensing::Attributes::PIRUnoccupiedToOccupiedDelay::Id:
|
||||
case OccupancySensing::Attributes::UltrasonicOccupiedToUnoccupiedDelay::Id:
|
||||
case OccupancySensing::Attributes::UltrasonicUnoccupiedToOccupiedDelay::Id:
|
||||
case OccupancySensing::Attributes::PhysicalContactOccupiedToUnoccupiedDelay::Id:
|
||||
case OccupancySensing::Attributes::PhysicalContactUnoccupiedToOccupiedDelay::Id: {
|
||||
write_command<uint16_t> *cmd = New<write_command<uint16_t>>(node_id, endpoint_id, OccupancySensing::Id,
|
||||
attribute_id, string_to_uint16(attribute_val_str));
|
||||
ESP_RETURN_ON_FALSE(cmd, ESP_ERR_NO_MEM, TAG, "Failed to alloc memory for write_command");
|
||||
return cmd->send_command();
|
||||
break;
|
||||
}
|
||||
case OccupancySensing::Attributes::PIRUnoccupiedToOccupiedThreshold::Id:
|
||||
case OccupancySensing::Attributes::UltrasonicUnoccupiedToOccupiedThreshold::Id:
|
||||
case OccupancySensing::Attributes::PhysicalContactUnoccupiedToOccupiedThreshold::Id: {
|
||||
write_command<uint8_t> *cmd = New<write_command<uint8_t>>(node_id, endpoint_id, OccupancySensing::Id,
|
||||
attribute_id, string_to_uint8(attribute_val_str));
|
||||
ESP_RETURN_ON_FALSE(cmd, ESP_ERR_NO_MEM, TAG, "Failed to alloc memory for write_command");
|
||||
return cmd->send_command();
|
||||
break;
|
||||
}
|
||||
default:
|
||||
err = ESP_ERR_NOT_SUPPORTED;
|
||||
}
|
||||
return err;
|
||||
}
|
||||
|
||||
} // namespace occupancy_sensing
|
||||
|
||||
namespace window_covering {
|
||||
|
||||
static esp_err_t write_attribute(uint64_t node_id, uint16_t endpoint_id, uint32_t attribute_id, char *attribute_val_str)
|
||||
{
|
||||
esp_err_t err = ESP_OK;
|
||||
switch (attribute_id) {
|
||||
case WindowCovering::Attributes::Mode::Id: {
|
||||
write_command<uint8_t> *cmd = New<write_command<uint8_t>>(node_id, endpoint_id, WindowCovering::Id,
|
||||
attribute_id, string_to_uint8(attribute_val_str));
|
||||
ESP_RETURN_ON_FALSE(cmd, ESP_ERR_NO_MEM, TAG, "Failed to alloc memory for write_command");
|
||||
return cmd->send_command();
|
||||
break;
|
||||
}
|
||||
default:
|
||||
err = ESP_ERR_NOT_SUPPORTED;
|
||||
break;
|
||||
}
|
||||
return err;
|
||||
}
|
||||
|
||||
} // namespace window_covering
|
||||
|
||||
namespace thermostat_userinterface_configuration {
|
||||
|
||||
static esp_err_t write_attribute(uint64_t node_id, uint16_t endpoint_id, uint32_t attribute_id, char *attribute_val_str)
|
||||
{
|
||||
esp_err_t err = ESP_OK;
|
||||
switch (attribute_id) {
|
||||
case ThermostatUserInterfaceConfiguration::Attributes::TemperatureDisplayMode::Id:
|
||||
case ThermostatUserInterfaceConfiguration::Attributes::KeypadLockout::Id:
|
||||
case ThermostatUserInterfaceConfiguration::Attributes::ScheduleProgrammingVisibility::Id: {
|
||||
write_command<uint8_t> *cmd =
|
||||
New<write_command<uint8_t>>(node_id, endpoint_id, ThermostatUserInterfaceConfiguration::Id, attribute_id,
|
||||
string_to_uint8(attribute_val_str));
|
||||
ESP_RETURN_ON_FALSE(cmd, ESP_ERR_NO_MEM, TAG, "Failed to alloc memory for write_command");
|
||||
return cmd->send_command();
|
||||
break;
|
||||
}
|
||||
default:
|
||||
err = ESP_ERR_NOT_SUPPORTED;
|
||||
}
|
||||
return err;
|
||||
}
|
||||
|
||||
} // namespace thermostat_userinterface_configuration
|
||||
|
||||
} // namespace clusters
|
||||
|
||||
static attribute_write_handler_t s_unsupported_attribute_write_handler = NULL;
|
||||
|
||||
void set_unsupported_attribute_write_handler(attribute_write_handler_t handler)
|
||||
{
|
||||
s_unsupported_attribute_write_handler = handler;
|
||||
}
|
||||
|
||||
esp_err_t send_write_attr_command(uint64_t node_id, uint16_t endpoint_id, uint32_t cluster_id, uint32_t attribute_id,
|
||||
char *attribute_val_str)
|
||||
const char *attr_val_json_str)
|
||||
{
|
||||
esp_err_t err = ESP_OK;
|
||||
switch (cluster_id) {
|
||||
case OnOff::Id:
|
||||
err = clusters::on_off::write_attribute(node_id, endpoint_id, attribute_id, attribute_val_str);
|
||||
break;
|
||||
case LevelControl::Id:
|
||||
err = clusters::level_control::write_attribute(node_id, endpoint_id, attribute_id, attribute_val_str);
|
||||
break;
|
||||
case ColorControl::Id:
|
||||
err = clusters::color_control::write_attribute(node_id, endpoint_id, attribute_id, attribute_val_str);
|
||||
break;
|
||||
case AccessControl::Id:
|
||||
err = clusters::access_control::write_attribute(node_id, endpoint_id, attribute_id, attribute_val_str);
|
||||
break;
|
||||
case Binding::Id:
|
||||
err = clusters::binding::write_attribute(node_id, endpoint_id, attribute_id, attribute_val_str);
|
||||
break;
|
||||
case GroupKeyManagement::Id:
|
||||
err = clusters::group_key_management::write_attribute(node_id, endpoint_id, attribute_id, attribute_val_str);
|
||||
break;
|
||||
case OccupancySensing::Id:
|
||||
err = clusters::occupancy_sensing::write_attribute(node_id, endpoint_id, attribute_id, attribute_val_str);
|
||||
break;
|
||||
case WindowCovering::Id:
|
||||
err = clusters::window_covering::write_attribute(node_id, endpoint_id, attribute_id, attribute_val_str);
|
||||
break;
|
||||
case ThermostatUserInterfaceConfiguration::Id:
|
||||
err = clusters::thermostat_userinterface_configuration::write_attribute(node_id, endpoint_id, attribute_id,
|
||||
attribute_val_str);
|
||||
break;
|
||||
case Identify::Id:
|
||||
err = clusters::identify::write_attribute(node_id, endpoint_id, attribute_id, attribute_val_str);
|
||||
break;
|
||||
case Thermostat::Id:
|
||||
err = clusters::thermostat::write_attribute(node_id, endpoint_id, attribute_id, attribute_val_str);
|
||||
break;
|
||||
case DoorLock::Id:
|
||||
err = clusters::door_lock::write_attribute(node_id, endpoint_id, attribute_id, attribute_val_str);
|
||||
break;
|
||||
default:
|
||||
err = ESP_ERR_NOT_SUPPORTED;
|
||||
if (!attr_val_json_str) {
|
||||
ESP_LOGE(TAG, "attribute value json string cannot be NULL");
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
write_command *cmd =
|
||||
chip::Platform::New<write_command>(node_id, endpoint_id, cluster_id, attribute_id, attr_val_json_str);
|
||||
|
||||
if (!cmd) {
|
||||
ESP_LOGE(TAG, "Failed to alloc memory for cluster_command");
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
|
||||
if (err == ESP_ERR_NOT_SUPPORTED && s_unsupported_attribute_write_handler) {
|
||||
err = s_unsupported_attribute_write_handler(node_id, endpoint_id, cluster_id, attribute_id, attribute_val_str);
|
||||
}
|
||||
|
||||
return err;
|
||||
return cmd->send_command();
|
||||
}
|
||||
|
||||
} // namespace controller
|
||||
|
||||
@@ -17,10 +17,12 @@
|
||||
#include <app/ChunkedWriteCallback.h>
|
||||
#include <controller/CommissioneeDeviceProxy.h>
|
||||
#include <esp_matter.h>
|
||||
#include <esp_matter_mem.h>
|
||||
|
||||
namespace esp_matter {
|
||||
namespace controller {
|
||||
|
||||
using chip::Optional;
|
||||
using chip::ScopedNodeId;
|
||||
using chip::SessionHandle;
|
||||
using chip::app::AttributePathParams;
|
||||
@@ -29,49 +31,35 @@ using chip::app::ConcreteDataAttributePath;
|
||||
using chip::app::StatusIB;
|
||||
using chip::app::WriteClient;
|
||||
using chip::Messaging::ExchangeManager;
|
||||
using chip::TLV::TLVElementType;
|
||||
using esp_matter::client::peer_device_t;
|
||||
|
||||
typedef esp_err_t (*attribute_write_handler_t)(uint64_t node_id, uint16_t endpoint_id, uint32_t cluster_id,
|
||||
uint32_t attribute_id, char *attribute_val_str);
|
||||
constexpr char *k_null_attribute_val_str = "null";
|
||||
constexpr size_t k_attr_val_str_buf_size = CONFIG_ESP_MATTER_CONTROLLER_JSON_STRING_BUFFER_LEN;
|
||||
|
||||
template <class T>
|
||||
class write_command : public WriteClient::Callback {
|
||||
public:
|
||||
write_command(uint64_t node_id, uint16_t endpoint_id, uint32_t cluster_id, uint32_t attribute_id, T attribute_val)
|
||||
write_command(uint64_t node_id, uint16_t endpoint_id, uint32_t cluster_id, uint32_t attribute_id,
|
||||
const char *attribute_val_str)
|
||||
: m_node_id(node_id)
|
||||
, m_attr_path(endpoint_id, cluster_id, attribute_id)
|
||||
, m_chunked_callback(this)
|
||||
, m_attr_val(attribute_val)
|
||||
, m_attr_free(nullptr)
|
||||
, m_attr_free_ctx(nullptr)
|
||||
, on_device_connected_cb(on_device_connected_fcn, this)
|
||||
, on_device_connection_failure_cb(on_device_connection_failure_fcn, this)
|
||||
{
|
||||
}
|
||||
|
||||
~write_command()
|
||||
{
|
||||
if (m_attr_free && m_attr_free_ctx) {
|
||||
m_attr_free(m_attr_free_ctx);
|
||||
if (attribute_val_str) {
|
||||
strncpy(m_attr_val_str, attribute_val_str, k_attr_val_str_buf_size - 1);
|
||||
m_attr_val_str[strnlen(attribute_val_str, k_attr_val_str_buf_size - 1)] = 0;
|
||||
} else {
|
||||
strcpy(m_attr_val_str, k_null_attribute_val_str);
|
||||
m_attr_val_str[strlen(k_null_attribute_val_str)] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
AttributePathParams &get_attribute_path() { return m_attr_path; }
|
||||
|
||||
T &get_attribute_val() { return m_attr_val; }
|
||||
|
||||
ChunkedWriteCallback &get_chunked_write_callback() { return m_chunked_callback; }
|
||||
~write_command() {}
|
||||
|
||||
esp_err_t send_command();
|
||||
|
||||
using attribute_free_handler = void (*)(void *);
|
||||
esp_err_t set_attribute_free_handler(attribute_free_handler attr_free_handler, void *ctx)
|
||||
{
|
||||
m_attr_free = attr_free_handler;
|
||||
m_attr_free_ctx = ctx;
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
// WriteClient Callback Interface
|
||||
void OnResponse(const WriteClient *client, const ConcreteDataAttributePath &path, StatusIB status) override
|
||||
{
|
||||
@@ -97,24 +85,21 @@ private:
|
||||
uint64_t m_node_id;
|
||||
AttributePathParams m_attr_path;
|
||||
ChunkedWriteCallback m_chunked_callback;
|
||||
T m_attr_val;
|
||||
// We need to alloc memory for writing complex attributes asynchronously and free the memory when we delete the
|
||||
// write_command.
|
||||
attribute_free_handler m_attr_free;
|
||||
void *m_attr_free_ctx;
|
||||
char m_attr_val_str[k_attr_val_str_buf_size];
|
||||
|
||||
static void on_device_connected_fcn(void *context, ExchangeManager &exchangeMgr,
|
||||
const SessionHandle &sessionHandle);
|
||||
static void on_device_connection_failure_fcn(void *context, const ScopedNodeId &peerId, CHIP_ERROR error);
|
||||
|
||||
static esp_err_t encode_attribute_value(uint8_t *encoded_buf, size_t encoded_buf_size,
|
||||
const char *attr_val_json_str, TLVReader &out_reader);
|
||||
|
||||
chip::Callback::Callback<chip::OnDeviceConnected> on_device_connected_cb;
|
||||
chip::Callback::Callback<chip::OnDeviceConnectionFailure> on_device_connection_failure_cb;
|
||||
};
|
||||
|
||||
void set_unsupported_attribute_write_handler(attribute_write_handler_t handler);
|
||||
|
||||
esp_err_t send_write_attr_command(uint64_t node_id, uint16_t endpoint_id, uint32_t cluster_id, uint32_t attribute_id,
|
||||
char *attribute_val_str);
|
||||
const char *attr_val_json_str);
|
||||
|
||||
} // namespace controller
|
||||
} // namespace esp_matter
|
||||
|
||||
+41
-47
@@ -1186,8 +1186,8 @@ This section introduces the Matter controller example. Now this example supports
|
||||
- On-network pairing
|
||||
- Invoke cluster commands
|
||||
- Read attributes commands
|
||||
- Write attributes commands
|
||||
- Read events commands
|
||||
- Write attributes commands
|
||||
- Subscribe attributes commands
|
||||
- Subscribe events commands
|
||||
- Group settings command.
|
||||
@@ -1223,27 +1223,15 @@ The ``pairing`` commands are used for commissioning end-devices and are availabl
|
||||
|
||||
2.9.3 Cluster commands
|
||||
~~~~~~~~~~~~~~~~~~~~~~
|
||||
The ``invoke-cmd`` command is used for sending cluster commands to the end-devices. Currently the controller component has implemented the following commands for various clusters.
|
||||
The ``invoke-cmd`` command is used for sending cluster commands to the end-devices. It utilizes a ``cluster_command`` class to establish the sessions and send the command packets. The class constructor function could accept two callback inputs:
|
||||
|
||||
**Unicast commands**:
|
||||
- **Success callback**:
|
||||
This callback will be called upon the reception of the success response. It could be used to handle the response data for the command that requires a reponse. Now the default success callback will print the response data for GroupKeyManagement, Groups, Scenes, Thermostat, and DoorLock clusters. If you want to handle the response data in your example, you can register your success callback when creating the ``cluster_command`` object.
|
||||
|
||||
| **OnOff Cluster** (On, Off, Toggle)
|
||||
| **LevelControl Cluster** (Move, MoveToLevel, Step, Stop)
|
||||
| **ColorControl Cluster** (MoveToHue, MoveToSaturation, MoveToHueAndSaturation)
|
||||
| **GroupKeyManagement Cluster** (KeySetWrite, KeySetRead)
|
||||
| **Groups Cluster** (AddGroup, ViewGroup, RemoveGroup)
|
||||
| **Identify Cluster** (Identify, TriggerEffect)
|
||||
| **Scenes Cluster** (AddScene, ViewScene, RemoveScene, RemoveAllScenes, StoreScene, RecallScene, GetSceneMembership)
|
||||
| **Thermostat Cluster** (SetpointRaiseLower, SetWeeklySchedule, GetWeeklySchedule, ClearWeeklySchedule)
|
||||
| **DoorLock Cluster** (LockDoor, UnlockDoor, UnlockWithTimeout)
|
||||
| **WindowCovering Cluster** (UpOrOpen, DownOrClose, StopMotion, GoToLiftValue, GoToLiftPercentage, GoToTiltValue, GoToTiltPercentage)
|
||||
| **AdministratorCommissioning Cluster** (OpenCommissioningWindow, OpenBasicCommissioningWindow, RevokeCommissioning)
|
||||
- **Error callback**:
|
||||
This callback will be called upon the reception of the failure response or reponse timeout.
|
||||
|
||||
**Multicast commands**:
|
||||
|
||||
| **OnOff Cluster** (On, Off, Toggle)
|
||||
|
||||
If you want to utilize commands not list above, you can use ``esp_matter::controller::cluster_command::set_unsupported_cluster_command_handler()`` and ``esp_matter::controller::cluster_command::set_unsupported_cluster_group_command_handler()`` to set handlers for the commands that are not currently implemented.
|
||||
^^^^^^^^^^^^^^^^
|
||||
|
||||
- Send the cluster command:
|
||||
|
||||
@@ -1253,26 +1241,30 @@ If you want to utilize commands not list above, you can use ``esp_matter::contro
|
||||
|
||||
.. note::
|
||||
|
||||
- To use multicast commands, the ``group-id`` should begin with the ``0xFFFFFFFFFFFF`` prefix. And the ``endpoint-id`` is still required for multicast commands even if it will be ignored.
|
||||
- You can obtain the order of the command data parameters with an empty ``command-data``.
|
||||
- The ``command-data`` should utilize a JSON object string and the name of each item in this object should be ``\"<TagNumber>:<DataType>\"`` or ``\"<TagName>:<TagNumber>:<DataType>\"``. The TagNumber should be the same as the command parameter ID in SPEC and the supported DataTypes are listed in ``$ESP_MATTER_PATH/components/esp_matter/private/json_to_tlv.h``
|
||||
|
||||
For KeySetWrite command in Group Key Management cluster, the ``command-data`` should include an argument in JSON format:
|
||||
-For the DataType ``BYTES``, the value should be a Base64-Encoded string.
|
||||
|
||||
|
||||
Here are some examples of the ``command-data`` format.
|
||||
|
||||
- For MoveToLevel command in LevelControl cluster, the ``command-data`` (move to level 10 in trasition time 0) should be:
|
||||
|
||||
::
|
||||
|
||||
matter esp controller invoke-cmd <node-id> <endpoint-id> 63 0 "{\"groupKeySetID\": 42,\"groupKeySecurityPolicy\": 0, \"epochKey0\":\"d0d1d2d3d4d5d6d7d8d9dadbdcdddedf\", \"epochStartTime0\": 2220000 }"
|
||||
matter esp controller invoke-cmd <node-id> <endpoint-id> 8 0 "{\"0:UINT8\": 10, \"1:UINT16\": 0, \"2:UINT8\": 0, \"3:UINT8\": 0}"
|
||||
|
||||
For AddGroup command in Groups cluster, the ``command-data`` should include a string argument:
|
||||
- For KeySetWrite command in GroupKeyManagement cluster, the ``command-data`` should be:
|
||||
|
||||
::
|
||||
|
||||
matter esp controller invoke-cmd <node-id> <endpoint-id> 0x4 0 1 grp1
|
||||
matter esp controller invoke-cmd <node-id> <endpoint-id> 63 0 "{\"0:STRUCT\": {\"0:UINT16\": 42, \"1:UINT8\": 0, \"2:BYTES\": \"0NHS09TV1tfY2drb3N3e3w==\", \"3:UINT64\": 2220000, \"4:NULL\": null, \"5:NULL\": null, \"6:NULL\": null, \"7:NULL\": null}}"
|
||||
|
||||
For OpenCommissioningWindow command in Administrator Commissioning cluster, the ``command_data`` is simplied to ``commissioning-timeout iterations discriminator``:
|
||||
- For AddGroup command in Groups cluster, the ``command-data`` should be:
|
||||
|
||||
::
|
||||
|
||||
matter esp controller invoke-cmd <node-id> <endpoint-id> 0x3c 0 500 1000 3840
|
||||
matter esp controller invoke-cmd <node-id> <endpoint-id> 0x4 0 "{\"0:UINT16\": 1, \"1:STRING\": \"grp1\"}"
|
||||
|
||||
2.9.4 Read commands
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
@@ -1306,22 +1298,7 @@ The ``read-event`` commands are used for sending the commands of reading events
|
||||
|
||||
2.9.5 Write attribute commands
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
The ``write-attr`` command is used for sending the commands of writing attributes on the end-device. Currently the controller component has implemented the capability to write attributes of the following clusters.
|
||||
|
||||
| **OnOff Cluster**
|
||||
| **LevelControl Cluster**
|
||||
| **ColorControl Cluster**
|
||||
| **AccessControl Cluster**
|
||||
| **Binding Cluster**
|
||||
| **GroupKeyManagement Cluster**
|
||||
| **Identify Cluster**
|
||||
| **Thermostat Cluster**
|
||||
| **DoorLock Cluster**
|
||||
| **OccupancySensing Cluster**
|
||||
| **WindowCovering Cluster**
|
||||
| **ThermostatUserInterfaceConfiguration Cluster**
|
||||
|
||||
If you want to send the writing-attribute commands to the clusters not listed above, you could use ``esp_matter::controller::set_unsupported_attribute_write_handler()`` to set the handler for clusters that are not currently implemented.
|
||||
The ``write-attr`` command is used for sending the commands of writing attributes on the end-device.
|
||||
|
||||
- Send the write-attribute command:
|
||||
|
||||
@@ -1331,16 +1308,33 @@ If you want to send the writing-attribute commands to the clusters not listed ab
|
||||
|
||||
.. note::
|
||||
|
||||
``attribute_value`` could be formatted as JSON string, as an example, for Binding attribute of Binding cluster,
|
||||
you should use the follow JSON structure as the ``attribute_value`` : ``"[{\"node\":1, \"endpoint\":1, \"cluster\":6}]"``
|
||||
- ``attribute_value`` should utilize a JSON object string. And the format of this string is the same as the ``command_data`` in `cluster commands <./developing.rst#cluster-commands>`__. This JSON object should contain only one item that represents the attribute value.
|
||||
|
||||
|
||||
Here are some examples of the ``attribute_value`` format.
|
||||
|
||||
For StartUpOnOff attribute of OnOff Cluster, you should use the following JSON structures as the ``attribute_value`` to represent the StartUpOnOff ``2`` and ``null``:
|
||||
|
||||
::
|
||||
|
||||
matter esp controller write-attr <node_id> <endpoint_id> 6 0x4003 ``"{\"0:UINT8\": 2}"``
|
||||
matter esp controller write-attr <node_id> <endpoint_id> 6 0x4003 ``"{\"0:NULL\": null}"``
|
||||
|
||||
For Binding attribute of Binding cluster, you should use the following JSON structure as the ``attribute_value`` to represent the binding list ``[{"node":1, "endpoint":1, "cluster":6}]``:
|
||||
|
||||
::
|
||||
|
||||
matter esp controller write-attr <node_id> <endpoint_id> 30 0 ``"{\"0:ARRAY-STRUCT\":[{\"1:UINT64\":1, \"3:UINT16\":1, \"4:UINT32\": 6}]}"``
|
||||
|
||||
For ACL attribute of AccessControl cluster, you should use the following JSON structure as the ``attribute_value`` to represent the AccessControlList ``[{"privilege": 5, "authMode": 2, "subjects": [112233], "targets": null}, {"privilege": 4, "authMode": 3, "subjects": [1], "targets": null}]``:
|
||||
|
||||
::
|
||||
|
||||
matter esp controller write-attr <node_id> <endpoint_id> 30 0 "[{\"node\":1, \"endpoint\":1, \"cluster\":6}]"
|
||||
matter esp controller write-attr <node_id> <endpoint_id> 31 0 "{\"0:ARRAY-STRUCT\":[{\"1:UINT8\": 5, \"2:UINT8\": 2, \"3:ARRAY-UINT64\": [112233], \"4:NULL\": null}, {\"1:UINT8\": 4, \"2:UINT8\": 3, \"3:ARRAY-UINT64\": [1], \"4:NULL\": null}]}"
|
||||
|
||||
2.9.6 Subscribe commands
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
The ``subscribe_command`` class is used for sending subscribe commands to other end-devices. Its constructor function could accept four callback inputings:
|
||||
The ``subscribe_command`` class is used for sending subscribe commands to other end-devices. Its constructor function could accept four callback inputs:
|
||||
|
||||
- **Attribute report callback**:
|
||||
This callback will be invoked upon the reception of the attribute report for subscribe-attribute commands.
|
||||
|
||||
Reference in New Issue
Block a user