Merge branch 'feat/decode_command_response_as_json' into 'main'

components/esp_matter_controller: use generic TLV to JSON for command responses decoding

See merge request app-frameworks/esp-matter!1488
This commit is contained in:
Shu Chen
2026-04-07 02:42:29 +00:00
10 changed files with 703 additions and 160 deletions
+2
View File
@@ -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"
@@ -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 "."
+149
View File
@@ -0,0 +1,149 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <cJSON.h>
#include <esp_err.h>
#include <json_to_tlv.h>
#include <lib/core/TLV.h>
#include <tlv_to_json.h>
#include <unity.h>
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})");
}
@@ -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
@@ -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 <algorithm>
#include <cJSON.h>
#include <element_types.h>
#include <esp_check.h>
#include <esp_matter_mem.h>
#include <json_to_tlv.h>
@@ -79,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<double>(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) {
@@ -254,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;
@@ -266,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;
@@ -289,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<uint32_t>(val->valuedouble);
ESP_RETURN_ON_FALSE(writer.Put(tag, uint32_val) == CHIP_NO_ERROR, ESP_FAIL, TAG, "Failed to encode");
break;
}
@@ -302,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;
@@ -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.
@@ -17,34 +17,9 @@
#include "cJSON.h"
#include <esp_err.h>
#include <lib/core/TLV.h>
#include <string>
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
@@ -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 <cJSON.h>
#include <cmath>
#include <element_types.h>
#include <esp_check.h>
#include <esp_matter_mem.h>
#include <tlv_to_json.h>
#include <lib/support/Base64.h>
#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<TLVElementType>(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<char *>(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<uint16_t>(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<char *>(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<int>(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
@@ -0,0 +1,33 @@
// 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 <cJSON.h>
#include <esp_err.h>
#include <lib/core/TLV.h>
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
@@ -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 <commands/clusters/DataModelLogger.h>
#include <controller/CommissioneeDeviceProxy.h>
#include <cJSON.h>
#include <esp_check.h>
#include <esp_matter_controller_client.h>
#include <esp_matter_controller_cluster_command.h>
#include <esp_matter_controller_utils.h>
#include <esp_matter_mem.h>
#include <json_parser.h>
#include <tlv_to_json.h>
#include <app/server/Server.h>
#include <crypto/CHIPCryptoPAL.h>
@@ -28,117 +29,11 @@
#include <setup_payload/QRCodeSetupPayloadGenerator.h>
#include <setup_payload/SetupPayload.h>
using namespace chip::app::Clusters;
using namespace esp_matter::client;
static const char *TAG = "cluster_command";
namespace esp_matter {
namespace cluster {
template <typename CommandResponseObjectT>
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<GroupKeyManagement::Commands::KeySetRead::Type::ResponseType>(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<Groups::Commands::AddGroup::Type::ResponseType>(command_path, reader);
} else if (command_path.mCommandId == Groups::Commands::ViewGroupResponse::Id) {
decode_command_response<Groups::Commands::ViewGroup::Type::ResponseType>(command_path, reader);
} else if (command_path.mCommandId == Groups::Commands::RemoveGroupResponse::Id) {
decode_command_response<Groups::Commands::RemoveGroup::Type::ResponseType>(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<ScenesManagement::Commands::AddScene::Type::ResponseType>(command_path, reader);
} else if (command_path.mCommandId == ScenesManagement::Commands::ViewSceneResponse::Id) {
decode_command_response<ScenesManagement::Commands::ViewScene::Type::ResponseType>(command_path, reader);
} else if (command_path.mCommandId == ScenesManagement::Commands::RemoveSceneResponse::Id) {
decode_command_response<ScenesManagement::Commands::RemoveScene::Type::ResponseType>(command_path, reader);
} else if (command_path.mCommandId == ScenesManagement::Commands::RemoveAllScenesResponse::Id) {
decode_command_response<ScenesManagement::Commands::RemoveAllScenes::Type::ResponseType>(command_path, reader);
} else if (command_path.mCommandId == ScenesManagement::Commands::StoreSceneResponse::Id) {
decode_command_response<ScenesManagement::Commands::StoreScene::Type::ResponseType>(command_path, reader);
} else if (command_path.mCommandId == ScenesManagement::Commands::GetSceneMembershipResponse::Id) {
decode_command_response<ScenesManagement::Commands::GetSceneMembership::Type::ResponseType>(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<Thermostat::Commands::GetWeeklySchedule::Type::ResponseType>(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<DoorLock::Commands::GetWeekDaySchedule::Type::ResponseType>(command_path, reader);
} else if (command_path.mCommandId == DoorLock::Commands::GetYearDayScheduleResponse::Id) {
decode_command_response<DoorLock::Commands::GetYearDaySchedule::Type::ResponseType>(command_path, reader);
} else if (command_path.mCommandId == DoorLock::Commands::GetHolidayScheduleResponse::Id) {
decode_command_response<DoorLock::Commands::GetHolidaySchedule::Type::ResponseType>(command_path, reader);
} else if (command_path.mCommandId == DoorLock::Commands::GetUserResponse::Id) {
decode_command_response<DoorLock::Commands::GetUser::Type::ResponseType>(command_path, reader);
} else if (command_path.mCommandId == DoorLock::Commands::SetCredentialResponse::Id) {
decode_command_response<DoorLock::Commands::SetCredential::Type::ResponseType>(command_path, reader);
} else if (command_path.mCommandId == DoorLock::Commands::GetCredentialStatusResponse::Id) {
decode_command_response<DoorLock::Commands::GetCredentialStatus::Type::ResponseType>(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)
@@ -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')