From a06ec909f98a7a61e4d0e726d4e4e21ed40e47be Mon Sep 17 00:00:00 2001 From: "Zhibin (Ryan) Wen" Date: Thu, 26 Mar 2026 20:40:22 +0800 Subject: [PATCH 1/5] components/esp_matter: move jsontlv utilities to dedicated subdirectory Signed-off-by: Zhibin (Ryan) Wen --- components/esp_matter/CMakeLists.txt | 2 + .../esp_matter/utils/jsontlv/element_types.h | 43 +++++++++++++++++++ .../utils/{ => jsontlv}/json_to_tlv.cpp | 3 +- .../utils/{ => jsontlv}/json_to_tlv.h | 26 +---------- 4 files changed, 48 insertions(+), 26 deletions(-) create mode 100644 components/esp_matter/utils/jsontlv/element_types.h rename components/esp_matter/utils/{ => jsontlv}/json_to_tlv.cpp (99%) rename components/esp_matter/utils/{ => jsontlv}/json_to_tlv.h (66%) diff --git a/components/esp_matter/CMakeLists.txt b/components/esp_matter/CMakeLists.txt index 199303492..26be45c86 100644 --- a/components/esp_matter/CMakeLists.txt +++ b/components/esp_matter/CMakeLists.txt @@ -1,10 +1,12 @@ set(SRC_DIRS_LIST "." "utils" + "utils/jsontlv" "${MATTER_SDK_PATH}/zzz_generated/app-common/app-common/zap-generated/attributes" ) set(INCLUDE_DIRS_LIST "." "utils" + "utils/jsontlv" "data_model_provider" "${MATTER_SDK_PATH}/zzz_generated/app-common" "${MATTER_SDK_PATH}/third_party/nlfaultinjection/include" diff --git a/components/esp_matter/utils/jsontlv/element_types.h b/components/esp_matter/utils/jsontlv/element_types.h new file mode 100644 index 000000000..fdbcb32ba --- /dev/null +++ b/components/esp_matter/utils/jsontlv/element_types.h @@ -0,0 +1,43 @@ +// Copyright 2026 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 + +namespace esp_matter { + +namespace element_type { +// Supported Data Type +const char k_int8[] = "I8"; +const char k_int16[] = "I16"; +const char k_int32[] = "I32"; +const char k_int64[] = "I64"; +const char k_uint8[] = "U8"; +const char k_uint16[] = "U16"; +const char k_uint32[] = "U32"; +const char k_uint64[] = "U64"; +const char k_bool[] = "BOOL"; +const char k_float[] = "FP"; +const char k_double[] = "DFP"; +const char k_bytes[] = "BYT"; +const char k_string[] = "STR"; +const char k_null[] = "NULL"; +const char k_object[] = "OBJ"; +const char k_array[] = "ARR"; +const char k_empty[] = "?"; + +const char k_floating_point_positive_infinity[] = "INF"; +const char k_floating_point_negative_infinity[] = "-INF"; +} // namespace element_type + +} // namespace esp_matter diff --git a/components/esp_matter/utils/json_to_tlv.cpp b/components/esp_matter/utils/jsontlv/json_to_tlv.cpp similarity index 99% rename from components/esp_matter/utils/json_to_tlv.cpp rename to components/esp_matter/utils/jsontlv/json_to_tlv.cpp index d40619636..da43d7613 100644 --- a/components/esp_matter/utils/json_to_tlv.cpp +++ b/components/esp_matter/utils/jsontlv/json_to_tlv.cpp @@ -1,4 +1,4 @@ -// Copyright 2023 Espressif Systems (Shanghai) PTE LTD +// Copyright 2023-2026 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. @@ -14,6 +14,7 @@ #include #include +#include #include #include #include diff --git a/components/esp_matter/utils/json_to_tlv.h b/components/esp_matter/utils/jsontlv/json_to_tlv.h similarity index 66% rename from components/esp_matter/utils/json_to_tlv.h rename to components/esp_matter/utils/jsontlv/json_to_tlv.h index bd897d77d..7ea08f3e5 100644 --- a/components/esp_matter/utils/json_to_tlv.h +++ b/components/esp_matter/utils/jsontlv/json_to_tlv.h @@ -1,4 +1,4 @@ -// Copyright 2023 Espressif Systems (Shanghai) PTE LTD +// Copyright 2023-2026 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. @@ -21,30 +21,6 @@ namespace esp_matter { -namespace element_type { -// Supported Data Type -const char k_int8[] = "I8"; -const char k_int16[] = "I16"; -const char k_int32[] = "I32"; -const char k_int64[] = "I64"; -const char k_uint8[] = "U8"; -const char k_uint16[] = "U16"; -const char k_uint32[] = "U32"; -const char k_uint64[] = "U64"; -const char k_bool[] = "BOOL"; -const char k_float[] = "FP"; -const char k_double[] = "DFP"; -const char k_bytes[] = "BYT"; -const char k_string[] = "STR"; -const char k_null[] = "NULL"; -const char k_object[] = "OBJ"; -const char k_array[] = "ARR"; -const char k_empty[] = "?"; - -const char k_floating_point_positive_infinity[] = "INF"; -const char k_floating_point_negative_infinity[] = "-INF"; -} // namespace element_type - /** Convert a JSON object to the given TLVWriter * * @param[in] json_str The JSON string that represents a TLV structure From 387a2f9e44adf0232691a8bf9347a7dd0417e102 Mon Sep 17 00:00:00 2001 From: "Zhibin (Ryan) Wen" Date: Thu, 26 Mar 2026 20:42:39 +0800 Subject: [PATCH 2/5] components/esp_matter: add TLV to JSON conversion utility Add tlv_to_json.cpp/h which converts CHIP TLV data model payload to cJSON format for debugging and interoperability. Signed-off-by: Zhibin (Ryan) Wen --- .../esp_matter/utils/jsontlv/json_to_tlv.h | 1 - .../esp_matter/utils/jsontlv/tlv_to_json.cpp | 416 ++++++++++++++++++ .../esp_matter/utils/jsontlv/tlv_to_json.h | 34 ++ 3 files changed, 450 insertions(+), 1 deletion(-) create mode 100644 components/esp_matter/utils/jsontlv/tlv_to_json.cpp create mode 100644 components/esp_matter/utils/jsontlv/tlv_to_json.h diff --git a/components/esp_matter/utils/jsontlv/json_to_tlv.h b/components/esp_matter/utils/jsontlv/json_to_tlv.h index 7ea08f3e5..3d9daf7b6 100644 --- a/components/esp_matter/utils/jsontlv/json_to_tlv.h +++ b/components/esp_matter/utils/jsontlv/json_to_tlv.h @@ -17,7 +17,6 @@ #include "cJSON.h" #include #include -#include namespace esp_matter { diff --git a/components/esp_matter/utils/jsontlv/tlv_to_json.cpp b/components/esp_matter/utils/jsontlv/tlv_to_json.cpp new file mode 100644 index 000000000..6c251fa40 --- /dev/null +++ b/components/esp_matter/utils/jsontlv/tlv_to_json.cpp @@ -0,0 +1,416 @@ +// Copyright 2026 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 +#include +#include +#include +#include +#include +#include + +#include "support/CodeUtils.h" + +using namespace chip; +using chip::TLV::TLVElementType; + +constexpr char TAG[] = "TlvToJson"; + +namespace esp_matter { + +static TLVElementType get_tlv_element_type(const TLV::TLVReader &reader) +{ + return static_cast(reader.GetControlByte() & TLV::kTLVTypeMask); +} + +static esp_err_t tlv_element_type_to_type_str(TLVElementType type, const char **type_str) +{ + ESP_RETURN_ON_FALSE(type_str, ESP_ERR_INVALID_ARG, TAG, "type_str cannot be NULL"); + switch (type) { + case TLVElementType::Int8: + *type_str = element_type::k_int8; + return ESP_OK; + case TLVElementType::Int16: + *type_str = element_type::k_int16; + return ESP_OK; + case TLVElementType::Int32: + *type_str = element_type::k_int32; + return ESP_OK; + case TLVElementType::Int64: + *type_str = element_type::k_int64; + return ESP_OK; + case TLVElementType::UInt8: + *type_str = element_type::k_uint8; + return ESP_OK; + case TLVElementType::UInt16: + *type_str = element_type::k_uint16; + return ESP_OK; + case TLVElementType::UInt32: + *type_str = element_type::k_uint32; + return ESP_OK; + case TLVElementType::UInt64: + *type_str = element_type::k_uint64; + return ESP_OK; + case TLVElementType::BooleanTrue: + case TLVElementType::BooleanFalse: + *type_str = element_type::k_bool; + return ESP_OK; + case TLVElementType::FloatingPointNumber32: + *type_str = element_type::k_float; + return ESP_OK; + case TLVElementType::FloatingPointNumber64: + *type_str = element_type::k_double; + return ESP_OK; + case TLVElementType::UTF8String_1ByteLength: + case TLVElementType::UTF8String_2ByteLength: + case TLVElementType::UTF8String_4ByteLength: + case TLVElementType::UTF8String_8ByteLength: + *type_str = element_type::k_string; + return ESP_OK; + case TLVElementType::ByteString_1ByteLength: + case TLVElementType::ByteString_2ByteLength: + case TLVElementType::ByteString_4ByteLength: + case TLVElementType::ByteString_8ByteLength: + *type_str = element_type::k_bytes; + return ESP_OK; + case TLVElementType::Null: + *type_str = element_type::k_null; + return ESP_OK; + case TLVElementType::Structure: + *type_str = element_type::k_object; + return ESP_OK; + case TLVElementType::Array: + *type_str = element_type::k_array; + return ESP_OK; + case TLVElementType::NotSpecified: + *type_str = element_type::k_empty; + return ESP_OK; + default: + return ESP_ERR_INVALID_ARG; + }; +} + +static esp_err_t create_json_name(TLV::Tag tag, TLVElementType type, TLVElementType sub_type, char *json_name, + size_t json_name_size) +{ + ESP_RETURN_ON_FALSE(tag != TLV::AnonymousTag(), ESP_ERR_INVALID_ARG, TAG, "Anonymous tag is not supported"); + ESP_RETURN_ON_FALSE(json_name, ESP_ERR_INVALID_ARG, TAG, "json name cannot be NULL"); + + const char *type_str = nullptr; + ESP_RETURN_ON_ERROR(tlv_element_type_to_type_str(type, &type_str), TAG, "Unsupported tlv element type"); + + uint64_t tag_number = TLV::TagNumFromTag(tag); + if (type == TLVElementType::Array) { + const char *sub_type_str = nullptr; + ESP_RETURN_ON_ERROR(tlv_element_type_to_type_str(sub_type, &sub_type_str), TAG, "Unsupported array subtype"); + snprintf(json_name, json_name_size, "%" PRIu64 ":%s-%s", tag_number, type_str, sub_type_str); + } else { + snprintf(json_name, json_name_size, "%" PRIu64 ":%s", tag_number, type_str); + } + return ESP_OK; +} + +static esp_err_t encode_integer_string(int64_t value, cJSON **json) +{ + char value_str[32] = { 0 }; + snprintf(value_str, sizeof(value_str), "%" PRId64, value); + *json = cJSON_CreateString(value_str); + ESP_RETURN_ON_FALSE(*json, ESP_ERR_NO_MEM, TAG, "No memory"); + return ESP_OK; +} + +static esp_err_t encode_unsigned_integer_string(uint64_t value, cJSON **json) +{ + char value_str[32] = { 0 }; + snprintf(value_str, sizeof(value_str), "%" PRIu64, value); + *json = cJSON_CreateString(value_str); + ESP_RETURN_ON_FALSE(*json, ESP_ERR_NO_MEM, TAG, "No memory"); + return ESP_OK; +} + +static esp_err_t encode_byte_string(TLV::TLVReader &reader, cJSON **json) +{ + ByteSpan value; + ESP_RETURN_ON_FALSE(reader.Get(value) == CHIP_NO_ERROR, ESP_FAIL, TAG, "Failed to read byte string"); + + char *value_str = static_cast(esp_matter_mem_calloc(BASE64_ENCODED_LEN(value.size()) + 1, sizeof(char))); + ESP_RETURN_ON_FALSE(value_str, ESP_ERR_NO_MEM, TAG, "No memory"); + Base64Encode(value.data(), static_cast(value.size()), value_str); + + *json = cJSON_CreateString(value_str); + esp_matter_mem_free(value_str); + return ESP_OK; +} + +static esp_err_t encode_utf8_string(TLV::TLVReader &reader, cJSON **json) +{ + ByteSpan value; + ESP_RETURN_ON_FALSE(reader.Get(value) == CHIP_NO_ERROR, ESP_FAIL, TAG, "Failed to read byte string"); + + char *value_str = static_cast(esp_matter_mem_calloc(value.size() + 1, sizeof(char))); + ESP_RETURN_ON_FALSE(value_str, ESP_ERR_NO_MEM, TAG, "No memory"); + memcpy(value_str, value.data(), value.size()); + + *json = cJSON_CreateString(value_str); + esp_matter_mem_free(value_str); + ESP_RETURN_ON_FALSE(*json, ESP_ERR_NO_MEM, TAG, "No memory"); + return ESP_OK; +} + +static esp_err_t encode_floating_point(float value, cJSON **json) +{ + if (std::isinf(value)) { + *json = cJSON_CreateString(value > 0 ? element_type::k_floating_point_positive_infinity + : element_type::k_floating_point_negative_infinity); + } else { + *json = cJSON_CreateNumber(value); + } + ESP_RETURN_ON_FALSE(*json, ESP_ERR_NO_MEM, TAG, "No memory"); + return ESP_OK; +} + +static esp_err_t encode_floating_point(double value, cJSON **json) +{ + if (std::isinf(value)) { + *json = cJSON_CreateString(value > 0 ? element_type::k_floating_point_positive_infinity + : element_type::k_floating_point_negative_infinity); + } else { + *json = cJSON_CreateNumber(value); + } + ESP_RETURN_ON_FALSE(*json, ESP_ERR_NO_MEM, TAG, "No memory"); + return ESP_OK; +} + +static esp_err_t encode_tlv_node(TLV::TLVReader &reader, cJSON **json, TLVElementType &sub_type); + +static esp_err_t encode_tlv_object(TLV::TLVReader &reader, cJSON **json) +{ + esp_err_t ret = ESP_OK; + TLV::TLVType container_type; + cJSON *json_obj = cJSON_CreateObject(); + cJSON *json_obj_child = nullptr; + bool container_opened = false; + CHIP_ERROR err = CHIP_NO_ERROR; + + ESP_GOTO_ON_FALSE(json_obj, ESP_ERR_NO_MEM, cleanup, TAG, "No memory"); + err = reader.EnterContainer(container_type); + ESP_GOTO_ON_FALSE(err == CHIP_NO_ERROR, ESP_FAIL, cleanup, TAG, "Failed to enter container: %" CHIP_ERROR_FORMAT, err.Format()); + container_opened = true; + + while ((err = reader.Next()) == CHIP_NO_ERROR) { + TLV::Tag child_tag = reader.GetTag(); + TLVElementType child_type = get_tlv_element_type(reader); + TLVElementType child_sub_type = TLVElementType::NotSpecified; + ESP_GOTO_ON_ERROR(encode_tlv_node(reader, &json_obj_child, child_sub_type), cleanup, TAG, "Failed to encode tlv node"); + + char json_name[64] = { 0 }; + ESP_GOTO_ON_ERROR(create_json_name(child_tag, child_type, child_sub_type, json_name, sizeof(json_name)), + cleanup, TAG, "Failed to create json name"); + + cJSON_AddItemToObject(json_obj, json_name, json_obj_child); + json_obj_child = nullptr; + } + ESP_GOTO_ON_FALSE(err == CHIP_END_OF_TLV, ESP_FAIL, cleanup, TAG, "Failed to iterate container: %" CHIP_ERROR_FORMAT, err.Format()); + + err = reader.ExitContainer(container_type); + ESP_GOTO_ON_FALSE(err == CHIP_NO_ERROR, ESP_FAIL, cleanup, TAG, "Failed to exit container: %" CHIP_ERROR_FORMAT, err.Format()); + container_opened = false; + + *json = json_obj; + json_obj = nullptr; + +cleanup: + cJSON_Delete(json_obj_child); + cJSON_Delete(json_obj); + + if (container_opened) { + err = reader.ExitContainer(container_type); + if (err != CHIP_NO_ERROR) { + ESP_LOGE(TAG, "Failed to exit container: %" CHIP_ERROR_FORMAT, err.Format()); + } + } + return ret; +} + +static esp_err_t encode_tlv_array(TLV::TLVReader &reader, cJSON **json, TLVElementType &sub_type) +{ + esp_err_t ret = ESP_OK; + TLV::TLVType container_type; + cJSON *json_array = cJSON_CreateArray(); + cJSON *json_array_child = nullptr; + CHIP_ERROR err = CHIP_NO_ERROR; + bool container_opened = false; + + ESP_GOTO_ON_FALSE(json_array, ESP_ERR_NO_MEM, cleanup, TAG, "No memory"); + err = reader.EnterContainer(container_type); + ESP_GOTO_ON_FALSE(err == CHIP_NO_ERROR, ESP_FAIL, cleanup, TAG, "Failed to enter container: %" CHIP_ERROR_FORMAT, + err.Format()); + container_opened = true; + + sub_type = TLVElementType::NotSpecified; + while ((err = reader.Next()) == CHIP_NO_ERROR) { + TLVElementType child_sub_type = TLVElementType::NotSpecified; + ESP_GOTO_ON_ERROR(encode_tlv_node(reader, &json_array_child, child_sub_type), cleanup, TAG, "Failed to encode tlv node"); + + if (sub_type == TLVElementType::NotSpecified) { + sub_type = get_tlv_element_type(reader); + } + + cJSON_AddItemToArray(json_array, json_array_child); + json_array_child = nullptr; + } + ESP_GOTO_ON_FALSE(err == CHIP_END_OF_TLV, ESP_FAIL, cleanup, TAG, "Failed to iterate container: %" CHIP_ERROR_FORMAT, + err.Format()); + + err = reader.ExitContainer(container_type); + ESP_GOTO_ON_FALSE(err == CHIP_NO_ERROR, ESP_FAIL, cleanup, TAG, "Failed to exit container: %" CHIP_ERROR_FORMAT, err.Format()); + container_opened = false; + + *json = json_array; + json_array = nullptr; + +cleanup: + cJSON_Delete(json_array_child); + cJSON_Delete(json_array); + if (container_opened) { + err = reader.ExitContainer(container_type); + if (err != CHIP_NO_ERROR) { + ESP_LOGE(TAG, "Failed to exit container: %" CHIP_ERROR_FORMAT, err.Format()); + } + } + return ret; +} + +static esp_err_t encode_tlv_node(TLV::TLVReader &reader, cJSON **json, TLVElementType &sub_type) +{ + sub_type = TLVElementType::NotSpecified; + + switch (get_tlv_element_type(reader)) { + case TLVElementType::Int8: { + int8_t value = 0; + ESP_RETURN_ON_FALSE(reader.Get(value) == CHIP_NO_ERROR, ESP_FAIL, TAG, "Failed to read int8"); + *json = cJSON_CreateNumber(value); + break; + } + case TLVElementType::Int16: { + int16_t value = 0; + ESP_RETURN_ON_FALSE(reader.Get(value) == CHIP_NO_ERROR, ESP_FAIL, TAG, "Failed to read int16"); + *json = cJSON_CreateNumber(value); + break; + } + case TLVElementType::Int32: { + int32_t value = 0; + ESP_RETURN_ON_FALSE(reader.Get(value) == CHIP_NO_ERROR, ESP_FAIL, TAG, "Failed to read int32"); + *json = cJSON_CreateNumber(value); + break; + } + case TLVElementType::Int64: { + int64_t value = 0; + ESP_RETURN_ON_FALSE(reader.Get(value) == CHIP_NO_ERROR, ESP_FAIL, TAG, "Failed to read int64"); + return encode_integer_string(value, json); + } + case TLVElementType::UInt8: { + uint8_t value = 0; + ESP_RETURN_ON_FALSE(reader.Get(value) == CHIP_NO_ERROR, ESP_FAIL, TAG, "Failed to read uint8"); + *json = cJSON_CreateNumber(value); + break; + } + case TLVElementType::UInt16: { + uint16_t value = 0; + ESP_RETURN_ON_FALSE(reader.Get(value) == CHIP_NO_ERROR, ESP_FAIL, TAG, "Failed to read uint16"); + *json = cJSON_CreateNumber(value); + break; + } + case TLVElementType::UInt32: { + uint32_t value = 0; + ESP_RETURN_ON_FALSE(reader.Get(value) == CHIP_NO_ERROR, ESP_FAIL, TAG, "Failed to read uint32"); + *json = cJSON_CreateNumber(value); + break; + } + case TLVElementType::UInt64: { + uint64_t value = 0; + ESP_RETURN_ON_FALSE(reader.Get(value) == CHIP_NO_ERROR, ESP_FAIL, TAG, "Failed to read uint64"); + return encode_unsigned_integer_string(value, json); + } + case TLVElementType::BooleanFalse: + case TLVElementType::BooleanTrue: { + bool value = false; + ESP_RETURN_ON_FALSE(reader.Get(value) == CHIP_NO_ERROR, ESP_FAIL, TAG, "Failed to read bool"); + *json = cJSON_CreateBool(value); + break; + } + case TLVElementType::FloatingPointNumber32: { + float value = 0; + ESP_RETURN_ON_FALSE(reader.Get(value) == CHIP_NO_ERROR, ESP_FAIL, TAG, "Failed to read float"); + return encode_floating_point(value, json); + } + case TLVElementType::FloatingPointNumber64: { + double value = 0; + ESP_RETURN_ON_FALSE(reader.Get(value) == CHIP_NO_ERROR, ESP_FAIL, TAG, "Failed to read double"); + return encode_floating_point(value, json); + } + case TLVElementType::UTF8String_1ByteLength: + case TLVElementType::UTF8String_2ByteLength: + case TLVElementType::UTF8String_4ByteLength: + case TLVElementType::UTF8String_8ByteLength: + return encode_utf8_string(reader, json); + case TLVElementType::ByteString_1ByteLength: + case TLVElementType::ByteString_2ByteLength: + case TLVElementType::ByteString_4ByteLength: + case TLVElementType::ByteString_8ByteLength: + return encode_byte_string(reader, json); + case TLVElementType::Null: + *json = cJSON_CreateNull(); + break; + case TLVElementType::Structure: + return encode_tlv_object(reader, json); + case TLVElementType::Array: + case TLVElementType::List: + return encode_tlv_array(reader, json, sub_type); + default: + ESP_LOGE(TAG, "Unsupported tlv element type: %d", static_cast(get_tlv_element_type(reader))); + return ESP_ERR_NOT_SUPPORTED; + } + + ESP_RETURN_ON_FALSE(*json, ESP_ERR_NO_MEM, TAG, "No memory"); + return ESP_OK; +} + +esp_err_t tlv_to_json(TLV::TLVReader &reader, cJSON **json) +{ + ESP_RETURN_ON_FALSE(json, ESP_ERR_INVALID_ARG, TAG, "json cannot be NULL"); + + *json = nullptr; + + TLV::TLVReader reader_copy; + reader_copy.Init(reader); + + if (reader_copy.GetType() == TLV::kTLVType_NotSpecified) { + CHIP_ERROR chip_err = reader_copy.Next(); + if (chip_err != CHIP_NO_ERROR) { + ESP_LOGE(TAG, "Failed to move tlv reader: %" CHIP_ERROR_FORMAT, chip_err.Format()); + return ESP_FAIL; + } + } + + TLVElementType sub_type = TLVElementType::NotSpecified; + esp_err_t err = encode_tlv_node(reader_copy, json, sub_type); + if (err != ESP_OK) { + return err; + } + + return ESP_OK; +} + +} // namespace esp_matter diff --git a/components/esp_matter/utils/jsontlv/tlv_to_json.h b/components/esp_matter/utils/jsontlv/tlv_to_json.h new file mode 100644 index 000000000..74df957ae --- /dev/null +++ b/components/esp_matter/utils/jsontlv/tlv_to_json.h @@ -0,0 +1,34 @@ +// Copyright 2026 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 +#include +#include +#include + +namespace esp_matter { + +/** Convert TLV data model payload to cJSON. + * + * @param[in] reader The TLV reader positioned at the payload. + * @param[out] json The JSON object output. + * + * @return ESP_OK on success. + * @return error in case of failure. + */ +esp_err_t tlv_to_json(chip::TLV::TLVReader &reader, cJSON **json); + +} // namespace esp_matter From a33f7bff11fe10056277b58b6e0dee69bc0b6797 Mon Sep 17 00:00:00 2001 From: "Zhibin (Ryan) Wen" Date: Thu, 26 Mar 2026 20:44:26 +0800 Subject: [PATCH 3/5] components/esp_matter_controller: use generic TLV to JSON for command responses decoding Replace hardcoded cluster-specific decoders with a generic TLV-to-JSON conversion approach in default_success_fcn. Signed-off-by: Zhibin (Ryan) Wen --- .../esp_matter/utils/jsontlv/tlv_to_json.h | 1 - .../esp_matter_controller_cluster_command.cpp | 151 +++--------------- 2 files changed, 22 insertions(+), 130 deletions(-) diff --git a/components/esp_matter/utils/jsontlv/tlv_to_json.h b/components/esp_matter/utils/jsontlv/tlv_to_json.h index 74df957ae..c69ca71fa 100644 --- a/components/esp_matter/utils/jsontlv/tlv_to_json.h +++ b/components/esp_matter/utils/jsontlv/tlv_to_json.h @@ -17,7 +17,6 @@ #include #include #include -#include namespace esp_matter { diff --git a/components/esp_matter_controller/commands/esp_matter_controller_cluster_command.cpp b/components/esp_matter_controller/commands/esp_matter_controller_cluster_command.cpp index 980fbda3b..b6d29c317 100644 --- a/components/esp_matter_controller/commands/esp_matter_controller_cluster_command.cpp +++ b/components/esp_matter_controller/commands/esp_matter_controller_cluster_command.cpp @@ -1,4 +1,4 @@ -// Copyright 2022 Espressif Systems (Shanghai) PTE LTD +// Copyright 2022-2026 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. @@ -12,14 +12,15 @@ // See the License for the specific language governing permissions and // limitations under the License. -#include #include +#include #include #include #include #include #include #include +#include #include #include @@ -28,117 +29,11 @@ #include #include -using namespace chip::app::Clusters; using namespace esp_matter::client; static const char *TAG = "cluster_command"; namespace esp_matter { -namespace cluster { - -template -esp_err_t decode_command_response(const ConcreteCommandPath &command_path, TLVReader *reader) -{ - ESP_RETURN_ON_FALSE(reader, ESP_ERR_INVALID_ARG, TAG, "reader cannot be NULL"); - ESP_RETURN_ON_FALSE(command_path.mClusterId == CommandResponseObjectT::GetClusterId() && - command_path.mCommandId == CommandResponseObjectT::GetCommandId(), - ESP_ERR_INVALID_ARG, TAG, "Wrong command to decode"); - DataModelLogger::LogCommand(command_path, reader); - return ESP_OK; -} - -namespace group_key_management { -namespace command { - -void decode_response(const ConcreteCommandPath &command_path, TLVReader *reader) -{ - if (command_path.mCommandId == GroupKeyManagement::Commands::KeySetReadResponse::Id) { - decode_command_response(command_path, reader); - } -} - -} // namespace command -} // namespace group_key_management - -namespace groups { -namespace command { - -void decode_response(const ConcreteCommandPath &command_path, TLVReader *reader) -{ - if (command_path.mCommandId == Groups::Commands::AddGroupResponse::Id) { - decode_command_response(command_path, reader); - } else if (command_path.mCommandId == Groups::Commands::ViewGroupResponse::Id) { - decode_command_response(command_path, reader); - } else if (command_path.mCommandId == Groups::Commands::RemoveGroupResponse::Id) { - decode_command_response(command_path, reader); - } -} - -} // namespace command -} // namespace groups - -namespace scenes_management { -namespace command { - -void decode_response(const ConcreteCommandPath &command_path, TLVReader *reader) -{ - if (command_path.mCommandId == ScenesManagement::Commands::AddSceneResponse::Id) { - decode_command_response(command_path, reader); - } else if (command_path.mCommandId == ScenesManagement::Commands::ViewSceneResponse::Id) { - decode_command_response(command_path, reader); - } else if (command_path.mCommandId == ScenesManagement::Commands::RemoveSceneResponse::Id) { - decode_command_response(command_path, reader); - } else if (command_path.mCommandId == ScenesManagement::Commands::RemoveAllScenesResponse::Id) { - decode_command_response(command_path, reader); - } else if (command_path.mCommandId == ScenesManagement::Commands::StoreSceneResponse::Id) { - decode_command_response(command_path, reader); - } else if (command_path.mCommandId == ScenesManagement::Commands::GetSceneMembershipResponse::Id) { - decode_command_response(command_path, - reader); - } -} - -} // namespace command -} // namespace scenes_management - -namespace thermostat { -namespace command { - -void decode_response(const ConcreteCommandPath &command_path, TLVReader *reader) -{ - if (command_path.mCommandId == Thermostat::Commands::GetWeeklyScheduleResponse::Id) { - decode_command_response(command_path, reader); - } -} - -} // namespace command -} // namespace thermostat - -namespace door_lock { -namespace command { - -void decode_response(const ConcreteCommandPath &command_path, TLVReader *reader) -{ - if (command_path.mCommandId == DoorLock::Commands::GetWeekDayScheduleResponse::Id) { - decode_command_response(command_path, reader); - } else if (command_path.mCommandId == DoorLock::Commands::GetYearDayScheduleResponse::Id) { - decode_command_response(command_path, reader); - } else if (command_path.mCommandId == DoorLock::Commands::GetHolidayScheduleResponse::Id) { - decode_command_response(command_path, reader); - } else if (command_path.mCommandId == DoorLock::Commands::GetUserResponse::Id) { - decode_command_response(command_path, reader); - } else if (command_path.mCommandId == DoorLock::Commands::SetCredentialResponse::Id) { - decode_command_response(command_path, reader); - } else if (command_path.mCommandId == DoorLock::Commands::GetCredentialStatusResponse::Id) { - decode_command_response(command_path, reader); - } -} - -} // namespace command -} // namespace door_lock - -} // namespace cluster - namespace controller { void cluster_command::on_device_connected_fcn(void *context, ExchangeManager &exchangeMgr, @@ -169,28 +64,26 @@ void cluster_command::default_success_fcn(void *ctx, const ConcreteCommandPath & ESP_LOGI(TAG, "Some commands of specific clusters will have a response which is not NullObject, so we need to handle the " "response data for those commands. Here we print the response data."); - ESP_LOGI(TAG, - "If your command's response is not printed here, please register another success callback when creating " - "the cluster_command object to handle the response data."); - switch (command_path.mClusterId) { - case GroupKeyManagement::Id: - cluster::group_key_management::command::decode_response(command_path, response_data); - break; - case Groups::Id: - cluster::groups::command::decode_response(command_path, response_data); - break; - case ScenesManagement::Id: - cluster::scenes_management::command::decode_response(command_path, response_data); - break; - case Thermostat::Id: - cluster::thermostat::command::decode_response(command_path, response_data); - break; - case DoorLock::Id: - cluster::door_lock::command::decode_response(command_path, response_data); - break; - default: - break; + + if (!response_data) { + ESP_LOGI(TAG, "No response payload"); + return; } + + cJSON *decoded_json = nullptr; + if (tlv_to_json(*response_data, &decoded_json) != ESP_OK) { + ESP_LOGW(TAG, "Failed to convert response payload to JSON"); + cJSON_Delete(decoded_json); + return; + } + + char *formatted_response = cJSON_Print(decoded_json); + if (formatted_response) { + ESP_LOGI(TAG, "Response JSON:\n%s", formatted_response); + } + + cJSON_free(formatted_response); + cJSON_Delete(decoded_json); } void cluster_command::default_error_fcn(void *ctx, CHIP_ERROR error) From 95e523db2242bf6781a639763c718e3ab0da4757 Mon Sep 17 00:00:00 2001 From: "Zhibin (Ryan) Wen" Date: Tue, 31 Mar 2026 15:20:37 +0800 Subject: [PATCH 4/5] components/esp_matter: improve integer validation in JSON to TLV conversion - Add is_integral_json_number() helper to verify JSON numbers represent integral values before encoding. - Add range validation for Int32 and UInt32 types to prevent overflow. - Fix Int64 and UInt64 string parsing to properly validate input strings and detect conversion errors. Signed-off-by: Zhibin (Ryan) Wen --- .../esp_matter/utils/jsontlv/json_to_tlv.cpp | 31 ++++++++++++++++--- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/components/esp_matter/utils/jsontlv/json_to_tlv.cpp b/components/esp_matter/utils/jsontlv/json_to_tlv.cpp index da43d7613..dfb1829e3 100644 --- a/components/esp_matter/utils/jsontlv/json_to_tlv.cpp +++ b/components/esp_matter/utils/jsontlv/json_to_tlv.cpp @@ -80,6 +80,14 @@ static bool is_unsigned_integer(const char *str, size_t len) return true; } +static bool is_integral_json_number(const cJSON *val) +{ + if (val == nullptr || val->type != cJSON_Number) { + return false; + } + return static_cast(val->valueint) == val->valuedouble; +} + 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) { @@ -255,6 +263,9 @@ static esp_err_t encode_tlv_element(const cJSON *val, TLV::TLVWriter &writer, co } case TLVElementType::Int32: { ESP_RETURN_ON_FALSE(val->type == cJSON_Number, ESP_ERR_INVALID_ARG, TAG, "Invalid type"); + ESP_RETURN_ON_FALSE(is_integral_json_number(val), ESP_ERR_INVALID_ARG, TAG, "Invalid value"); + ESP_RETURN_ON_FALSE(val->valuedouble <= INT32_MAX && val->valuedouble >= INT32_MIN, ESP_ERR_INVALID_ARG, TAG, + "Invalid range"); 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; @@ -267,7 +278,12 @@ static esp_err_t encode_tlv_element(const cJSON *val, TLV::TLVWriter &writer, co int64_val = (val->valueint < INT32_MAX && val->valueint > INT32_MIN) ? val->valueint : (int64_t)val->valuedouble; } else { - int64_val = strtoll(val->valuestring, nullptr, 10); + ESP_RETURN_ON_FALSE(val->valuestring && val->valuestring[0] != '\0', ESP_ERR_INVALID_ARG, TAG, + "Invalid int64 string"); + char *end = nullptr; + int64_val = strtoll(val->valuestring, &end, 10); + ESP_RETURN_ON_FALSE(end != val->valuestring && end && *end == '\0', ESP_ERR_INVALID_ARG, TAG, + "Invalid int64 string"); } ESP_RETURN_ON_FALSE(writer.Put(tag, int64_val) == CHIP_NO_ERROR, ESP_FAIL, TAG, "Failed to encode"); break; @@ -290,8 +306,10 @@ static esp_err_t encode_tlv_element(const cJSON *val, TLV::TLVWriter &writer, co } 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(is_integral_json_number(val), ESP_ERR_INVALID_ARG, TAG, "Invalid value"); + ESP_RETURN_ON_FALSE(val->valuedouble >= 0 && val->valuedouble <= UINT32_MAX, ESP_ERR_INVALID_ARG, TAG, + "Invalid range"); + uint32_t uint32_val = static_cast(val->valuedouble); ESP_RETURN_ON_FALSE(writer.Put(tag, uint32_val) == CHIP_NO_ERROR, ESP_FAIL, TAG, "Failed to encode"); break; } @@ -303,7 +321,12 @@ static esp_err_t encode_tlv_element(const cJSON *val, TLV::TLVWriter &writer, co ESP_RETURN_ON_FALSE(val->valueint >= 0, ESP_ERR_INVALID_ARG, TAG, "Invalid range"); uint64_val = val->valueint < INT32_MAX ? val->valueint : (uint64_t)val->valuedouble; } else { - uint64_val = strtoull(val->valuestring, nullptr, 10); + ESP_RETURN_ON_FALSE(val->valuestring && val->valuestring[0] != '\0', ESP_ERR_INVALID_ARG, TAG, + "Invalid uint64 string"); + char *end = nullptr; + uint64_val = strtoull(val->valuestring, &end, 10); + ESP_RETURN_ON_FALSE(end != val->valuestring && end && *end == '\0', ESP_ERR_INVALID_ARG, TAG, + "Invalid uint64 string"); } ESP_RETURN_ON_FALSE(writer.Put(tag, uint64_val) == CHIP_NO_ERROR, ESP_FAIL, TAG, "Failed to encode"); break; From e48dc7139b28c4d620125b3bcf589e344296c6d7 Mon Sep 17 00:00:00 2001 From: "Zhibin (Ryan) Wen" Date: Tue, 31 Mar 2026 15:23:05 +0800 Subject: [PATCH 5/5] components/esp_matter/test: add jsontlv roundtrip and validation tests Signed-off-by: Zhibin (Ryan) Wen --- components/esp_matter/test/CMakeLists.txt | 1 + components/esp_matter/test/jsontlv.cpp | 149 ++++++++++++++++++ .../unit_test_app/pytest_unit_test_app.py | 7 + 3 files changed, 157 insertions(+) create mode 100644 components/esp_matter/test/jsontlv.cpp diff --git a/components/esp_matter/test/CMakeLists.txt b/components/esp_matter/test/CMakeLists.txt index 671198715..160cd97cb 100644 --- a/components/esp_matter/test/CMakeLists.txt +++ b/components/esp_matter/test/CMakeLists.txt @@ -1,6 +1,7 @@ list(APPEND srcs_list "attribute_get_val.cpp") list(APPEND srcs_list "attribute_get_val_type.cpp") list(APPEND srcs_list "attribute_report.cpp") +list(APPEND srcs_list "jsontlv.cpp") idf_component_register(SRCS ${srcs_list} INCLUDE_DIRS "." diff --git a/components/esp_matter/test/jsontlv.cpp b/components/esp_matter/test/jsontlv.cpp new file mode 100644 index 000000000..f6e450b17 --- /dev/null +++ b/components/esp_matter/test/jsontlv.cpp @@ -0,0 +1,149 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include +#include +#include + +static constexpr size_t k_tlv_buffer_size = 1024; + +static esp_err_t roundtrip_json_tree(const char *input_json, cJSON **output_json) +{ + if (output_json == nullptr) { + return ESP_ERR_INVALID_ARG; + } + + *output_json = nullptr; + + uint8_t buffer[k_tlv_buffer_size] = { 0 }; + chip::TLV::TLVWriter writer; + writer.Init(buffer, sizeof(buffer)); + + esp_err_t err = esp_matter::json_to_tlv(input_json, writer, chip::TLV::AnonymousTag()); + if (err != ESP_OK) { + return err; + } + + chip::TLV::TLVReader reader; + reader.Init(buffer, writer.GetLengthWritten()); + return esp_matter::tlv_to_json(reader, output_json); +} + +static void expect_roundtrip(const char *input_json, const char *expected_json) +{ + cJSON *actual_json = nullptr; + cJSON *expected_json_tree = cJSON_Parse(expected_json); + TEST_ASSERT_NOT_NULL(expected_json_tree); + + esp_err_t err = roundtrip_json_tree(input_json, &actual_json); + TEST_ASSERT_EQUAL(ESP_OK, err); + TEST_ASSERT_NOT_NULL(actual_json); + + char *actual_printed = cJSON_PrintUnformatted(actual_json); + char *expected_printed = cJSON_PrintUnformatted(expected_json_tree); + TEST_ASSERT_NOT_NULL(actual_printed); + TEST_ASSERT_NOT_NULL(expected_printed); + TEST_ASSERT_EQUAL_STRING(expected_printed, actual_printed); + + cJSON_free(actual_printed); + cJSON_free(expected_printed); + cJSON_Delete(actual_json); + cJSON_Delete(expected_json_tree); +} + +static void expect_json_to_tlv_failure(const char *input_json) +{ + uint8_t buffer[k_tlv_buffer_size] = { 0 }; + chip::TLV::TLVWriter writer; + writer.Init(buffer, sizeof(buffer)); + + esp_err_t err = esp_matter::json_to_tlv(input_json, writer, chip::TLV::AnonymousTag()); + TEST_ASSERT_TRUE(err != ESP_OK); +} + +TEST_CASE("jsontlv roundtrip scalar values", "[jsontlv][roundtrip]") +{ + expect_roundtrip( + R"({"5:STR":"chip","2:I16":-1234,"4:BOOL":true,"1:U8":42,"3:NULL":null})", + R"({"1:U8":42,"2:I16":-1234,"3:NULL":null,"4:BOOL":true,"5:STR":"chip"})"); + + expect_roundtrip( + R"({"2:OBJ":{"4:BOOL":false,"1:U8":7},"1:ARR-U8":[3,1,2]})", + R"({"1:ARR-U8":[3,1,2],"2:OBJ":{"1:U8":7,"4:BOOL":false}})"); + + expect_roundtrip( + R"({"1:BYT":"AQID","2:FP":"INF","3:DFP":"-INF"})", + R"({"1:BYT":"AQID","2:FP":"INF","3:DFP":"-INF"})"); + + expect_roundtrip( + R"({"1:I64":"-1234567890123456789","2:U64":"12345678901234567890"})", + R"({"1:I64":"-1234567890123456789","2:U64":"12345678901234567890"})"); +} + +TEST_CASE("jsontlv roundtrip container and ordering cases", "[jsontlv][roundtrip]") +{ + expect_roundtrip( + R"({"1:ARR-?":[]})", + R"({"1:ARR-?":[]})"); + + expect_roundtrip( + R"({"255:U16":65535,"1:U8":1})", + R"({"1:U8":1,"255:U16":65535})"); + + expect_roundtrip( + R"({"9:OBJ":{"3:STR":"c","1:STR":"a","2:STR":"b"},"2:U8":2,"1:U8":1})", + R"({"1:U8":1,"2:U8":2,"9:OBJ":{"1:STR":"a","2:STR":"b","3:STR":"c"}})"); + + expect_roundtrip( + R"({"7:ARR-STR":[]})", + R"({"7:ARR-?":[]})"); +} + +TEST_CASE("jsontlv roundtrip numeric float values", "[jsontlv][roundtrip]") +{ + cJSON *json = nullptr; + esp_err_t err = roundtrip_json_tree(R"({"1:FP":1.5,"2:DFP":-2.25})", &json); + TEST_ASSERT_EQUAL(ESP_OK, err); + TEST_ASSERT_NOT_NULL(json); + + cJSON *float_value = cJSON_GetObjectItemCaseSensitive(json, "1:FP"); + TEST_ASSERT_NOT_NULL(float_value); + TEST_ASSERT_TRUE(cJSON_IsNumber(float_value)); + TEST_ASSERT_DOUBLE_WITHIN(0.0001, 1.5, float_value->valuedouble); + + cJSON *double_value = cJSON_GetObjectItemCaseSensitive(json, "2:DFP"); + TEST_ASSERT_NOT_NULL(double_value); + TEST_ASSERT_TRUE(cJSON_IsNumber(double_value)); + TEST_ASSERT_DOUBLE_WITHIN(0.0001, -2.25, double_value->valuedouble); + + cJSON_Delete(json); +} + +TEST_CASE("jsontlv rejects invalid structure and type inputs", "[jsontlv][invalid]") +{ + expect_json_to_tlv_failure(R"({"1:ARR":[1,2,3]})"); + expect_json_to_tlv_failure(R"({"1:BYT":"not-base64"})"); + expect_json_to_tlv_failure(R"({"1":42})"); + expect_json_to_tlv_failure(R"({"1:BOOL":"true"})"); + expect_json_to_tlv_failure(R"({"1:FP":"NaN"})"); + expect_json_to_tlv_failure(R"({"1:I64":"123aaa"})"); + expect_json_to_tlv_failure(R"({"1:I64":"aaa123"})"); + expect_json_to_tlv_failure(R"({"1:U64":"123aaa"})"); + expect_json_to_tlv_failure(R"({"1:U64":"aaa123"})"); +} + +TEST_CASE("jsontlv rejects invalid numeric boundaries", "[jsontlv][invalid]") +{ + expect_json_to_tlv_failure(R"({"1:U8":256})"); + expect_json_to_tlv_failure(R"({"1:U32":-1})"); + expect_json_to_tlv_failure(R"({"1:I8":128})"); + expect_json_to_tlv_failure(R"({"4294967295:U8":1})"); + expect_json_to_tlv_failure(R"({"1:U32":1.5})"); + expect_json_to_tlv_failure(R"({"1:I32":2147483648})"); +} diff --git a/examples/unit_test_app/pytest_unit_test_app.py b/examples/unit_test_app/pytest_unit_test_app.py index 59df44f5b..4781fa730 100644 --- a/examples/unit_test_app/pytest_unit_test_app.py +++ b/examples/unit_test_app/pytest_unit_test_app.py @@ -42,3 +42,10 @@ def test_get_val_type(dut: QemuDut) -> None: def test_update_report(dut: QemuDut) -> None: run_group(dut, 'report') run_group(dut, 'update') + + +@pytest.mark.host_test +@pytest.mark.qemu +@pytest.mark.esp32c3 +def test_jsontlv(dut: QemuDut) -> None: + run_group(dut, 'jsontlv')