Skip to content

Commit

Permalink
Merge branch 'thread_br/custom_cluster' into 'main'
Browse files Browse the repository at this point in the history
thread_br: add thread br custom cluster

See merge request app-frameworks/esp-matter!590
chshu committed Apr 23, 2024
2 parents c0fb895 + ba95323 commit f7397d8
Showing 9 changed files with 477 additions and 1 deletion.
3 changes: 2 additions & 1 deletion components/esp_matter_thread_br/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -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()
78 changes: 78 additions & 0 deletions components/esp_matter_thread_br/README.md
Original file line number Diff line number Diff line change
@@ -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.
250 changes: 250 additions & 0 deletions components/esp_matter_thread_br/esp_matter_thread_br_cluster.cpp
Original file line number Diff line number Diff line change
@@ -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 <esp_check.h>
#include <esp_log.h>
#include <esp_matter_thread_br_cluster.h>
#include <esp_matter_thread_br_launcher.h>

#include <app/util/attribute-storage.h>

#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<chip::EndpointId>::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
72 changes: 72 additions & 0 deletions components/esp_matter_thread_br/esp_matter_thread_br_cluster.h
Original file line number Diff line number Diff line change
@@ -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 <esp_err.h>
#include <esp_matter.h>
#include <esp_matter_core.h>

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
12 changes: 12 additions & 0 deletions components/esp_matter_thread_br/esp_matter_thread_br_launcher.cpp
Original file line number Diff line number Diff line change
@@ -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;
Original file line number Diff line number Diff line change
@@ -18,6 +18,7 @@
#if CONFIG_OPENTHREAD_BR_AUTO_UPDATE_RCP
#include <esp_rcp_update.h>
#endif
#include <openthread/border_agent.h>

#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);
56 changes: 56 additions & 0 deletions components/esp_matter_thread_br/thread_br.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?xml version="1.0"?>
<!--
Copyright (c) 2023 Project CHIP Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<configurator>
<domain name="CHIP"/>
<enum name="ThreadRoleEnum" type="enum8">
<cluster code="0x131BFC02"/>
<item name="Disabled" value="0x00"/>
<item name="Detached" value="0x01"/>
<item name="Child" value="0x02"/>
<item name="Router" value="0x03"/>
<item name="Leader" value="0x04"/>
</enum>
<cluster>

<name>ThreadBR</name>
<domain>CHIP</domain>
<code>0x131BFC02</code>
<define>THREAD_BR_CLUSTER</define>

<description>Attributes and commands for ThreadBR cluster.</description>

<globalAttribute side="either" code="0xFFFD" value="2"/>

<attribute side="server" code="0x0000" define="DATASET_TLVS" type="octet_string" length="254">DatasetTlvs</attribute>
<attribute side="server" code="0x0001" define="ROLE" type="ThreadRoleEnum" min="0" max="4">Role</attribute>
<attribute side="server" code="0x0002" define="BORDER_AGENT_ID" type="octet_string" length="16">BorderAgentId</attribute>

<command source="client" code="0x00" name="ConfigureDatasetTlvs" optional="false">
<description>ConfigureDatasetTlvs command.</description>
<arg name="DatasetTlvsStr" type="char_string"/>
</command>

<command source="client" code="0x01" name="StartThread" optional="false">
<description>StartThread command.</description>
</command>

<command source="client" code="0x02" name="StopThread" optional="false">
<description>StopThread command.</description>
</command>

</cluster>
</configurator>
1 change: 1 addition & 0 deletions examples/controller/main/app_main.cpp
Original file line number Diff line number Diff line change
@@ -17,6 +17,7 @@
#include <esp_matter_controller_utils.h>
#include <esp_matter_ota.h>
#if CONFIG_OPENTHREAD_BORDER_ROUTER
#include <esp_matter_thread_br_cluster.h>
#include <esp_matter_thread_br_console.h>
#include <esp_matter_thread_br_launcher.h>
#include <esp_ot_config.h>
3 changes: 3 additions & 0 deletions examples/controller/sdkconfig.defaults.otbr
Original file line number Diff line number Diff line change
@@ -86,3 +86,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

0 comments on commit f7397d8

Please sign in to comment.