From ba953233f5e1c66ec6816a91cf9ed0e0b9751939 Mon Sep 17 00:00:00 2001 From: Wang Qixiang Date: Tue, 23 Apr 2024 10:13:29 +0800 Subject: [PATCH] thread_br: add thread br custom cluster --- .../esp_matter_thread_br/CMakeLists.txt | 3 +- components/esp_matter_thread_br/README.md | 78 ++++++ .../esp_matter_thread_br_cluster.cpp | 250 ++++++++++++++++++ .../esp_matter_thread_br_cluster.h | 72 +++++ .../esp_matter_thread_br_launcher.cpp | 12 + .../esp_matter_thread_br_launcher.h | 3 + components/esp_matter_thread_br/thread_br.xml | 56 ++++ examples/controller/main/app_main.cpp | 1 + examples/controller/sdkconfig.defaults.otbr | 3 + 9 files changed, 477 insertions(+), 1 deletion(-) create mode 100644 components/esp_matter_thread_br/README.md create mode 100644 components/esp_matter_thread_br/esp_matter_thread_br_cluster.cpp create mode 100644 components/esp_matter_thread_br/esp_matter_thread_br_cluster.h create mode 100644 components/esp_matter_thread_br/thread_br.xml diff --git a/components/esp_matter_thread_br/CMakeLists.txt b/components/esp_matter_thread_br/CMakeLists.txt index f0826fddc..60eab8bd6 100644 --- a/components/esp_matter_thread_br/CMakeLists.txt +++ b/components/esp_matter_thread_br/CMakeLists.txt @@ -1,7 +1,8 @@ set(SRCS_LIST ) if (CONFIG_OPENTHREAD_BORDER_ROUTER) - list(APPEND SRCS_LIST "esp_matter_thread_br_launcher.cpp") + list(APPEND SRCS_LIST "esp_matter_thread_br_cluster.cpp" + "esp_matter_thread_br_launcher.cpp") if (CONFIG_ENABLE_CHIP_SHELL AND CONFIG_OPENTHREAD_CLI) list(APPEND SRCS_LIST "esp_matter_thread_br_console.cpp") endif() diff --git a/components/esp_matter_thread_br/README.md b/components/esp_matter_thread_br/README.md new file mode 100644 index 000000000..2ea9a1eb4 --- /dev/null +++ b/components/esp_matter_thread_br/README.md @@ -0,0 +1,78 @@ +# Thread Border Router Cluster + +The Thread Border Router (BR) Cluster offers an interface for managing the ESP Thread BR. It allows users to perform various tasks such as configuring the dataset of the Thread network that the BR will form or join, start or stop Thread network. + +## 1. Cluster Identifiers + +| Identifier | Name | +|------------|--------------------------| +| 0x131BFC02 | **Thread Border Router** | + +## 2. Data Types + +### 2.1 ThreadRoleEnum Type + +This data type is derived from enum8. + +| Value | Name | Summary | Conformance | +|-------|----------|-------------------------------------------|-------------| +| 0 | Disabled | The Thread is disabled | M | +| 1 | Detached | The Node is detached to a Thread network | M | +| 2 | Child | The Node acts as a Child Role | M | +| 3 | Router | The Node acts as a Router Role | M | +| 4 | Leader | The Node acts as a Leader Role | M | + +## 2. Attributes + +| ID | Name | Type | Constranint | Quality | Default | Access | Conformance | +|--------|-------------------|----------------|-------------|---------|---------|--------|-------------| +| 0x0000 | **DatasetTlvs** | octstr | max254 | N | | R V | M | +| 0x0001 | **Role** | ThreadRoleEnum | | | | R V | M | +| 0x0002 | **BorderAgentId** | octstr | 16 | N | | R V | M | + +### 2.1 DatasetTlvs Attribute + +This attribute stores the dataset Tlvs of the Thread network that the BR will form or join. It will be updated after the ConfigureDatasetTlvs command is handled and the dataset is successfully committed. + +### 2.2 Role Attribute + +This attribute stores the Thread network role of the Thread BR. + +### 2.3 BorderAgentId Attribute + +This attribute stores the the randomly generated Border Agent ID. The typical use case of the ID is to be published in the MeshCoP mDNS service as the `id` TXT value for the client to identify this Border Router/Agent device. + +## 3. Commands + +| ID | Name | Direction | Response | Access | Conformance | +|--------|--------------------------|----------------|----------|--------|-------------| +| 0x0000 | **ConfigureDatasetTlvs** | client->server | Y | A | M | +| 0x0001 | **StartThread** | client->server | Y | A | M | +| 0x0002 | **StopThread** | client->server | Y | A | M | + + +### 3.1 ConfigureDatasetTlvs Command + +The ConfigureDatasetTlvs command allows the Thread BR to configure the dataset Tlvs of its Thread network. The DatasetTlvs Attribute will be updated after the dataset is commited. + +The ConfigureDatasetTlvs command SHALL have the following data fields: + +| ID | Name | Type | Constraint | Quality | Default | Comformance | +|----|--------------------|--------|------------|---------|---------|-------------| +| 0 | **DatasetTlvsStr** | string | max508 | | | M | + +#### 3.1.1 DatasetTlvsStr Field + +This field is the dataset tlvs string which will be conmmited. + +### 3.2 StartThread Command + +The StartThread command allows devices to form or join Thread network. + +The StartThread command has no data field. + +### 3.3 StopThread Command + +The StopThread command allows devices to stop its Thread network. + +The StopThread command has no data field. diff --git a/components/esp_matter_thread_br/esp_matter_thread_br_cluster.cpp b/components/esp_matter_thread_br/esp_matter_thread_br_cluster.cpp new file mode 100644 index 000000000..b426e1306 --- /dev/null +++ b/components/esp_matter_thread_br/esp_matter_thread_br_cluster.cpp @@ -0,0 +1,250 @@ +// 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 +#include +#include +#include + +#include + +#define TAG "thread_br_custom_cluster" + +namespace esp_matter { + +static int hex_digit_to_int(char hex) +{ + if ('A' <= hex && hex <= 'F') { + return 10 + hex - 'A'; + } else if ('a' <= hex && hex <= 'f') { + return 10 + hex - 'a'; + } else if ('0' <= hex && hex <= '9') { + return hex - '0'; + } + return -1; +} + +static size_t hex_str_to_binary(const char *str, size_t str_len, uint8_t *buf, uint8_t buf_size) +{ + if (str_len % 2 != 0 || str_len / 2 > buf_size) { + return 0; + } + for (size_t index = 0; index < str_len / 2; ++index) { + int byte_h = hex_digit_to_int(str[2 * index]); + int byte_l = hex_digit_to_int(str[2 * index + 1]); + if (byte_h < 0 || byte_l < 0) { + return 0; + } + buf[index] = (byte_h << 4) + byte_l; + } + return str_len / 2; +} + +namespace cluster { +namespace thread_br { + +namespace attribute { + +attribute_t *create_dataset_tlvs(cluster_t *cluster, uint8_t *value, uint16_t length) +{ + return esp_matter::attribute::create(cluster, dataset_tlvs::Id, ATTRIBUTE_FLAG_NONVOLATILE, + esp_matter_octet_str(value, length)); +} + +attribute_t *create_role(cluster_t *cluster, uint8_t value) +{ + return esp_matter::attribute::create(cluster, role::Id, ATTRIBUTE_FLAG_NONE, esp_matter_enum8(value)); +} + +attribute_t *create_border_agent_id(cluster_t *cluster, uint8_t *value, uint16_t length) +{ + return esp_matter::attribute::create(cluster, border_agent_id::Id, ATTRIBUTE_FLAG_NONVOLATILE, + esp_matter_octet_str(value, length)); +} + +} // namespace attribute + +namespace command { + +static esp_err_t configure_dataset_tlvs_command_callback(const ConcreteCommandPath &command_path, TLVReader &tlv_data, + void *opaque_ptr) +{ + uint16_t endpoint_id = command_path.mEndpointId; + uint32_t cluster_id = command_path.mClusterId; + uint32_t command_id = command_path.mCommandId; + + if (cluster_id != cluster::thread_br::Id || command_id != cluster::thread_br::command::configure_dataset_tlvs::Id) { + ESP_LOGE(TAG, "Got thread_br command callback for some other command. This should not happen."); + return ESP_FAIL; + } + + chip::CharSpan dataset_tlvs_str; + chip::TLV::TLVType outer; + ESP_RETURN_ON_FALSE(tlv_data.GetType() == chip::TLV::kTLVType_Structure, ESP_FAIL, TAG, + "TLV data is not a structure"); + ESP_RETURN_ON_FALSE(tlv_data.EnterContainer(outer) == CHIP_NO_ERROR, ESP_FAIL, TAG, "Failed to enter container"); + while (tlv_data.Next() == CHIP_NO_ERROR) { + if (!chip::TLV::IsContextTag(tlv_data.GetTag())) { + continue; + } + if (chip::TLV::TagNumFromTag(tlv_data.GetTag()) == 0 && tlv_data.GetType() == chip::TLV::kTLVType_UTF8String) { + chip::app::DataModel::Decode(tlv_data, dataset_tlvs_str); + } + } + ESP_RETURN_ON_FALSE(tlv_data.ExitContainer(outer) == CHIP_NO_ERROR, ESP_FAIL, TAG, "Failed to exit container"); + const char *data = dataset_tlvs_str.data(); + size_t size = dataset_tlvs_str.size(); + otOperationalDatasetTlvs dataset_tlvs; + dataset_tlvs.mLength = hex_str_to_binary(data, size, dataset_tlvs.mTlvs, sizeof(dataset_tlvs.mTlvs)); + ESP_RETURN_ON_FALSE(dataset_tlvs.mLength > 0, ESP_ERR_INVALID_ARG, TAG, "Failed to parse dataset tlvs"); + ESP_RETURN_ON_ERROR(set_thread_dataset_tlvs(&dataset_tlvs), TAG, "Failed to set Thread DatasetTlvs"); + + return ESP_OK; +} + +static esp_err_t start_thread_command_callback(const ConcreteCommandPath &command_path, TLVReader &tlv_data, + void *opaque_ptr) +{ + uint32_t cluster_id = command_path.mClusterId; + uint32_t command_id = command_path.mCommandId; + + if (cluster_id != cluster::thread_br::Id || command_id != cluster::thread_br::command::start_thread::Id) { + ESP_LOGE(TAG, "Got thread_br command callback for some other command. This should not happen."); + return ESP_FAIL; + } + + return set_thread_enabled(true); +} + +static esp_err_t stop_thread_command_callback(const ConcreteCommandPath &command_path, TLVReader &tlv_data, + void *opaque_ptr) +{ + uint32_t cluster_id = command_path.mClusterId; + uint32_t command_id = command_path.mCommandId; + + if (cluster_id != cluster::thread_br::Id || command_id != cluster::thread_br::command::stop_thread::Id) { + ESP_LOGE(TAG, "Got thread_br command callback for some other command. This should not happen."); + return ESP_FAIL; + } + + return set_thread_enabled(false); +} + +command_t *create_configure_dataset_tlvs(cluster_t *cluster) +{ + return esp_matter::command::create(cluster, configure_dataset_tlvs::Id, COMMAND_FLAG_ACCEPTED | COMMAND_FLAG_CUSTOM, + configure_dataset_tlvs_command_callback); +} + +command_t *create_start_thread(cluster_t *cluster) +{ + return esp_matter::command::create(cluster, start_thread::Id, COMMAND_FLAG_ACCEPTED | COMMAND_FLAG_CUSTOM, + start_thread_command_callback); +} + +command_t *create_stop_thread(cluster_t *cluster) +{ + return esp_matter::command::create(cluster, stop_thread::Id, COMMAND_FLAG_ACCEPTED | COMMAND_FLAG_CUSTOM, + stop_thread_command_callback); +} + +} // namespace command + +using chip::app::AttributeAccessInterface; +using chip::app::AttributeValueDecoder; +using chip::app::AttributeValueEncoder; +using chip::app::ConcreteDataAttributePath; +using chip::app::ConcreteReadAttributePath; + +class ThreadBRAttrAccess : public AttributeAccessInterface { +public: + ThreadBRAttrAccess() + : AttributeAccessInterface(chip::Optional::Missing(), cluster::thread_br::Id) + { + } + + CHIP_ERROR Read(const ConcreteReadAttributePath &aPath, AttributeValueEncoder &aEncoder) override + { + if (aPath.mClusterId != cluster::thread_br::Id) { + return CHIP_ERROR_INVALID_ARGUMENT; + } + esp_err_t err = ESP_OK; + if (aPath.mAttributeId == cluster::thread_br::attribute::dataset_tlvs::Id) { + otOperationalDatasetTlvs dataset_tlvs; + err = get_thread_dataset_tlvs(&dataset_tlvs); + if (err != ESP_OK) { + return CHIP_ERROR_INTERNAL; + } + return aEncoder.Encode(chip::ByteSpan(dataset_tlvs.mTlvs, dataset_tlvs.mLength)); + } else if (aPath.mAttributeId == cluster::thread_br::attribute::role::Id) { + uint8_t role = get_thread_role(); + return aEncoder.Encode(role); + } else if (aPath.mAttributeId == cluster::thread_br::attribute::border_agent_id::Id) { + otBorderAgentId border_agent_id; + err = get_border_agent_id(&border_agent_id); + if (err != ESP_OK) { + return CHIP_ERROR_INTERNAL; + } + return aEncoder.Encode(chip::ByteSpan(border_agent_id.mId, sizeof(border_agent_id.mId))); + } + return CHIP_NO_ERROR; + } + + CHIP_ERROR Write(const ConcreteDataAttributePath &aPath, AttributeValueDecoder &aDecoder) override + { + return CHIP_NO_ERROR; + } +}; + +ThreadBRAttrAccess g_attr_access; + +void thread_br_cluster_plugin_server_init_callback() +{ + registerAttributeAccessOverride(&g_attr_access); +} + +const function_generic_t function_list[] = {}; +const int function_flags = CLUSTER_FLAG_NONE; + +cluster_t *create(endpoint_t *endpoint, uint8_t flags) +{ + cluster_t *cluster = esp_matter::cluster::create(endpoint, Id, CLUSTER_FLAG_SERVER); + if (!cluster) { + ESP_LOGE(TAG, "Could not create cluster"); + return NULL; + } + + set_plugin_server_init_callback(cluster, thread_br_cluster_plugin_server_init_callback); + add_function_list(cluster, function_list, function_flags); + + global::attribute::create_cluster_revision(cluster, 1); + global::attribute::create_feature_map(cluster, 0); + global::attribute::create_event_list(cluster, NULL, 0, 0); + + // Attribute managed internally + attribute::create_dataset_tlvs(cluster, NULL, 0); + attribute::create_role(cluster, 0); + attribute::create_border_agent_id(cluster, NULL, 0); + + command::create_configure_dataset_tlvs(cluster); + command::create_start_thread(cluster); + command::create_stop_thread(cluster); + + return cluster; +} + +} // namespace thread_br +} // namespace cluster + +} // namespace esp_matter diff --git a/components/esp_matter_thread_br/esp_matter_thread_br_cluster.h b/components/esp_matter_thread_br/esp_matter_thread_br_cluster.h new file mode 100644 index 000000000..754358f7b --- /dev/null +++ b/components/esp_matter_thread_br/esp_matter_thread_br_cluster.h @@ -0,0 +1,72 @@ +// 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 +#include +#include + +namespace esp_matter { + +namespace cluster { +namespace thread_br { + +static constexpr chip::ClusterId Id = 0x131BFC02; + +namespace attribute { + +namespace dataset_tlvs { +static constexpr chip::AttributeId Id = 0x00000000; +} // namespace dataset_tlvs +attribute_t *create_dataset_tlvs(cluster_t *cluster, uint8_t *value, uint16_t length); + +namespace role { +static constexpr chip::AttributeId Id = 0x00000001; +} // namespace role +attribute_t *create_role(cluster_t *cluster, uint8_t role); + +namespace border_agent_id { +static constexpr chip::AttributeId Id = 0x00000002; +} // namespace border_agent_id +attribute_t *create_border_agent_id(cluster_t *cluster, uint8_t *value, uint16_t length); + + +} // namespace attribute + +namespace command { + +namespace configure_dataset_tlvs { +static constexpr chip::CommandId Id = 0x00000000; +} // namespace configure_dataset_tlvs +command_t *create_configure_dataset_tlvs(cluster_t *cluster); + +namespace start_thread { +static constexpr chip::CommandId Id = 0x00000001; +} // namespace start_thread +command_t *create_start_thread(cluster_t *cluster); + +namespace stop_thread { +static constexpr chip::CommandId Id = 0x00000002; +} // namespace stop_thread +command_t *create_stop_thread(cluster_t *cluster); + +} // namespace command + +cluster_t *create(endpoint_t *endpoint, uint8_t flags); + +} // namespace thread_br +} // namespace cluster + +} // namespace esp_matter diff --git a/components/esp_matter_thread_br/esp_matter_thread_br_launcher.cpp b/components/esp_matter_thread_br/esp_matter_thread_br_launcher.cpp index 202087bf2..778309455 100644 --- a/components/esp_matter_thread_br/esp_matter_thread_br_launcher.cpp +++ b/components/esp_matter_thread_br/esp_matter_thread_br_launcher.cpp @@ -275,6 +275,18 @@ esp_err_t get_thread_dataset_tlvs(otOperationalDatasetTlvs *dataset_tlvs) return ESP_OK; } + +esp_err_t get_border_agent_id(otBorderAgentId *border_agent_id) +{ + if (!border_agent_id) { + return ESP_ERR_INVALID_ARG; + } + scoped_thread_lock lock; + ESP_RETURN_ON_FALSE(otBorderAgentGetId(esp_openthread_get_instance(), border_agent_id) == OT_ERROR_NONE, ESP_FAIL, + TAG, "Failed to get Border Agent Id"); + return ESP_OK; +} + uint8_t get_thread_role() { scoped_thread_lock lock; diff --git a/components/esp_matter_thread_br/esp_matter_thread_br_launcher.h b/components/esp_matter_thread_br/esp_matter_thread_br_launcher.h index b8e7a10d9..2e55b82ee 100644 --- a/components/esp_matter_thread_br/esp_matter_thread_br_launcher.h +++ b/components/esp_matter_thread_br/esp_matter_thread_br_launcher.h @@ -18,6 +18,7 @@ #if CONFIG_OPENTHREAD_BR_AUTO_UPDATE_RCP #include #endif +#include #define OPENTHREAD_CLI_BUFFER_LENGTH 255 @@ -40,6 +41,8 @@ esp_err_t set_thread_dataset_tlvs(otOperationalDatasetTlvs *dataset_tlvs); esp_err_t get_thread_dataset_tlvs(otOperationalDatasetTlvs *dataset_tlvs); +esp_err_t get_border_agent_id(otBorderAgentId *border_agent_id); + uint8_t get_thread_role(); esp_err_t cli_transmit_task_post(ot_cli_buffer_t &cli_buf); diff --git a/components/esp_matter_thread_br/thread_br.xml b/components/esp_matter_thread_br/thread_br.xml new file mode 100644 index 000000000..d588f4e4f --- /dev/null +++ b/components/esp_matter_thread_br/thread_br.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + ThreadBR + CHIP + 0x131BFC02 + THREAD_BR_CLUSTER + + Attributes and commands for ThreadBR cluster. + + + + DatasetTlvs + Role + BorderAgentId + + + ConfigureDatasetTlvs command. + + + + + StartThread command. + + + + StopThread command. + + + + diff --git a/examples/controller/main/app_main.cpp b/examples/controller/main/app_main.cpp index 812855e28..4acc6accb 100644 --- a/examples/controller/main/app_main.cpp +++ b/examples/controller/main/app_main.cpp @@ -17,6 +17,7 @@ #include #include #if CONFIG_OPENTHREAD_BORDER_ROUTER +#include #include #include #include diff --git a/examples/controller/sdkconfig.defaults.otbr b/examples/controller/sdkconfig.defaults.otbr index 5513e8585..86f08c2ac 100644 --- a/examples/controller/sdkconfig.defaults.otbr +++ b/examples/controller/sdkconfig.defaults.otbr @@ -89,3 +89,6 @@ CONFIG_SPIRAM_MALLOC_ALWAYSINTERNAL=512 CONFIG_SPIRAM_MALLOC_RESERVE_INTERNAL=8192 CONFIG_SPIRAM_ALLOW_BSS_SEG_EXTERNAL_MEMORY=y CONFIG_SPIRAM_TRY_ALLOCATE_WIFI_LWIP=y + +# Enable HKDF for mbedtls +CONFIG_MBEDTLS_HKDF_C=y