Merge branch 'rest_apis' into 'main'

controller: Move the code of controller custom cluster to rainmaker repo

See merge request app-frameworks/esp-matter!588
This commit is contained in:
Hrishikesh Dhayagude
2024-05-17 14:52:44 +08:00
8 changed files with 2 additions and 2235 deletions
@@ -1,6 +1,7 @@
set(src_dirs_list )
set(include_dirs_list )
set(exclude_srcs_list )
set(requires_list chip esp_matter esp_matter_console spiffs)
if (CONFIG_ESP_MATTER_CONTROLLER_ENABLE)
list(APPEND src_dirs_list "${CMAKE_CURRENT_SOURCE_DIR}/core"
@@ -10,11 +11,6 @@ if (CONFIG_ESP_MATTER_CONTROLLER_ENABLE)
"${CMAKE_CURRENT_SOURCE_DIR}/commands"
"${CMAKE_CURRENT_SOURCE_DIR}/logger")
if (CONFIG_ESP_MATTER_CONTROLLER_CUSTOM_CLUSTER_ENABLE)
list(APPEND src_dirs_list "${CMAKE_CURRENT_SOURCE_DIR}/controller_custom_cluster")
list(APPEND include_dirs_list "${CMAKE_CURRENT_SOURCE_DIR}/controller_custom_cluster")
endif()
if (CONFIG_ESP_MATTER_ENABLE_MATTER_SERVER)
list(APPEND exclude_srcs_list "${CMAKE_CURRENT_SOURCE_DIR}/core/esp_matter_controller_client.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/core/esp_matter_controller_credentials_issuer.cpp"
@@ -32,6 +28,6 @@ endif()
idf_component_register(SRC_DIRS ${src_dirs_list}
EXCLUDE_SRCS ${exclude_srcs_list}
INCLUDE_DIRS ${include_dirs_list}
REQUIRES chip esp_matter esp_matter_console json_parser spiffs esp_http_client json_generator)
REQUIRES ${requires_list})
idf_build_set_property(COMPILE_OPTIONS "-Wno-write-strings" APPEND)
-7
View File
@@ -28,13 +28,6 @@ menu "ESP Matter Controller"
Max JSON string buffer length. This buffer will be used to store the command data field
for cluster-invoked command or attribute value for write-attribute command.
config ESP_MATTER_CONTROLLER_CUSTOM_CLUSTER_ENABLE
bool "Enable controller custom cluster"
depends on ESP_MATTER_CONTROLLER_ENABLE && ESP_MATTER_ENABLE_MATTER_SERVER
default n
help
Enable the custom cluster of matter controller in the ESP Matter controller for Rainmaker Fabric suppport.
choice ESP_MATTER_COMMISSIONER_ATTESTATION_TRUST_STORE
prompt "Attestation Trust Store"
depends on ESP_MATTER_COMMISSIONER_ENABLE
@@ -1,110 +0,0 @@
# Matter Controller Cluster
The Matter Controller Cluster offers an interface for managing the ESP Matter Controller. It allows users to perform various tasks, such as authorizing the controller to login to the cloud, updating the controller's NOC to the Rainmaker-Fabric User NOC, and notifying the controller to update the device list.
## 1. Cluster Identifiers
| Identifier | Name |
|------------|-----------------------|
| 0x131BFC01 | **Matter Controller** |
## 2. Attributes
| ID | Name | Type | Constranint | Quality | Default | Access | Conformance |
|--------|-------------------------|----------------|-------------|---------|---------|--------|-------------|
| 0x0000 | **RefreshToken** | string | | N | | R V | M |
| 0x0001 | **AccessToken** | string | | N | | R V | M |
| 0x0002 | **Authorized** | bool | | N | false | R V | M |
| 0x0003 | **UserNOCInstalled** | bool | | N | false | R V | M |
| 0x0004 | **EndpointURL** | string | | N | | R V | M |
| 0x0005 | **RainmakerGroupId** | string | | N | | R V | M |
| 0x0006 | **UserNOCFabricIndex** | uint8_t | | N | | R V | M |
### 2.1 RefreshToken Attribute
This attribute stores the refresh token. For the HTTP REST Authenticated APIs, the refresh_token SHALL be used to fetch the access_token and the access_token will be passed in the "Authorization" HTTP header as the authentication token.
Note: The access_token validity will expire after 1 hour.
### 2.2 AccessToken Attribute
This attribute stores the access token. It will be updated after the controller is authorized successfully. For the HTTP REST Authenticated APIs, the access_token will be passed in "Authorization" HTTP header as the authentication token. If the HTTP REST Authenticated APIs fail with decription "Unauthorized", the access_token will be updated.
### 2.3 Authorized Attribute
This attribute indicates the authorization status of the controller after joining the Rainmaker Fabric. After the access_token is fetched, the Authorized value will be set to true. And it will be set to false after 1 hour. If the HTTP REST Authenticated APIs fail with decription "Unauthorized", this attribute will be also set to false.
### 2.4 UserNOCInstalled Attribute
This attribute indicates whether the User NOC is installed in the controller. After the controller is authorized, the controller will generate a CSR and send it with the RainmakerGroupId to the cloud. The response from the cloud will include the new User NOC which will be installed in the controller.
### 2.5 EndpointURL Attribute
This attribute stores the Endpoint URL that is used for HTTP REST APIs.
### 2.6 RainmakerGroupId Attribute
This attribute stores the Rainmaker Group Id which is bound to the Matter Fabric Id.
### 2.7 UserNOCFabricIndex Attribute
This attribute stores the fabric index of the fabric where the user NOC is installed. It will be updated after the user NOC is installed.
## 3. Commands
| ID | Name | Direction | Response | Access | Conformance |
|--------|--------------------------|----------------|----------|--------|-------------|
| 0x0000 | **AppendRefreshToken** | client->server | Y | A | M |
| 0x0001 | **ResetRefreshToken** | client->server | Y | A | M |
| 0x0002 | **Authorize** | client->server | Y | A | M |
| 0x0003 | **UpdateUserNOC** | client->server | Y | A | M |
| 0x0004 | **UpdateDeviceList** | client->server | Y | A | M |
### 3.1 AppendRefreshToken Command
The AppendRefreshToken command allows the controller to write the RefreshToken Attribute in several commands. It will append the current RefreshToken attribute. The RefreshToken is about 1700 Bytes which is much longer than the UDP MTU(1280 Bytes) so it should be send it separately.
The AppendRefreshToken command SHALL have the following data fields:
| ID | Name | Type | Constraint | Quality | Default | Comformance |
|----|--------------------------|--------|------------|---------|---------|-------------|
| 0 | **AppendedRefreshToken** | string | max 1024 | | | M |
#### 3.1.1 AppendedRefreshToken Field
This field is the string which will be appended to the current RefreshToken Attribute.
### 3.2 ResetRefreshToken Command
The ResetRefreshToken command allows devices to reset it RefreshToken to an empty string.
The ResetRefreshToken has no data field.
### 3.3 Authorize Command
The Authorize command allows the controller to login the cloud HTTP server and fetch the AccessToken with the HTTP REST Authenticated APIs.
After AccessToken is fetched, the controller will get the RainmakerGroupId according to the FabricId of the command object.
The Authorize command SHALL have the following data fields:
| ID | Name | Type | Constraint | Quality | Default | Comformance |
|----|------------------|--------|------------|---------|---------|-------------|
| 0 | **EndpointURL** | string | max 64 | | | M |
#### 3.3.1 EndpointURL Field
This field is the EndpointURL. After the controller is authorized successfully, this value will be written to the EndpointURL Attribute.
### 3.4 UpdateUserNOC Command
The UpdateUserNOC command allows the controller to fetch the Rainmaker User NOC after the is authorized. When receiving this command, the controller will generate a new CSR and send it with the RainmakerGroupId to the cloud. After receiving the response, it will install the RainmakerUserNOC in the response.
The UpdateUserNOC command has no data field.
### 3.5 UpdateDeviceList Command
The UpdateDeviceList command notifies the controller to update its maintaining Matter device list. After this command is received, the controller will query the device list in the Rainmaker Group(Matter Fabric) from the cloud. This command SHALL be called after the controller has updated its NOC and when a new Matter device is commissioned into the Matter Fabric.
The UpdateDeviceList command has no data field.
@@ -1,61 +0,0 @@
<?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"/>
<cluster>
<name>MatterController</name>
<domain>CHIP</domain>
<code>0x131BFC01</code>
<define>MATTER_CONTROLLER_CLUSTER</define>
<description>Attributes and commands for matter_controller cluster.</description>
<globalAttribute side="either" code="0xFFFD" value="2"/>
<attribute side="server" code="0x0000" define="REFRESH_TOKEN" type="long_char_string" length="2048">RefreshToken</attribute>
<attribute side="server" code="0x0001" define="ACCESS_TOKEN" type="long_char_string" length="2048">AccessToken</attribute>
<attribute side="server" code="0x0002" define="AUTHORIZED" type="boolean" default="0">Authorized</attribute>
<attribute side="server" code="0x0003" define="USER_NOC_INSTALLED" type="boolean" default="0">UserNOCInstalled</attribute>
<attribute side="server" code="0x0004" define="ENDPOINT_URL" type="char_string" length="64">EndpointUrl</attribute>
<attribute side="server" code="0x0005" define="RMAKER_GROUP_ID" type="char_string" length="24">RmakerGroupId</attribute>
<attribute side="server" code="0x0006" define="USER_NOC_FABRIC_INDEX" type="int8u" default="0">UserNOCFabricIndex</attribute>
<command source="client" code="0x00" name="AppendRefreshToken" optional="false">
<description>AppendRefreshToken command.</description>
<arg name="AppendedRefreshToken" type="char_string"/>
</command>
<command source="client" code="0x01" name="ResetRefreshToken" optional="false">
<description>ResetRefreshToken command.</description>
</command>
<command source="client" code="0x02" name="Authorize" optional="false">
<description>Authorize command.</description>
<arg name="EndpointUrl" type="char_string"/>
</command>
<command source="client" code="0x03" name="UpdateUserNOC" optional="false">
<description>UpdateUserNOC command.</description>
</command>
<command source="client" code="0x04" name="UpdateDeviceList" optional="false">
<description>UpdateDeviceList command.</description>
</command>
</cluster>
</configurator>
@@ -1,117 +0,0 @@
// 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>
#define ESP_MATTER_RAINMAKER_MAX_ACCESS_TOKEN_LEN 2048
#define ESP_MATTER_RAINMAKER_MAX_REFRESH_TOKEN_LEN 2048
#define ESP_MATTER_RAINMAKER_MAX_ENDPOINT_URL_LEN 64
#define ESP_MATTER_RAINMAKER_MAX_GROUP_ID_LEN 24
#define HTTP_API_VERSION "v1"
namespace esp_matter {
namespace controller {
esp_err_t controller_authorize(uint16_t endpoint_id);
}
namespace cluster {
namespace matter_controller {
static constexpr chip::ClusterId Id = 0x131BFC01;
namespace attribute {
namespace refresh_token {
static constexpr chip::AttributeId Id = 0x00000000;
} // namespace refresh_token
namespace access_token {
static constexpr chip::AttributeId Id = 0x00000001;
} // namespace access_token
namespace authorized {
static constexpr chip::AttributeId Id = 0x00000002;
} // namespace authorized
namespace user_noc_installed {
static constexpr chip::AttributeId Id = 0x00000003;
} // namespace user_noc_installed
namespace endpoint_url {
static constexpr chip::AttributeId Id = 0x00000004;
} // namespace endpoint_url
namespace rainmaker_group_id {
static constexpr chip::AttributeId Id = 0x00000005;
} // namespace rainmaker_group_id
namespace user_noc_fabric_index {
static constexpr chip::AttributeId Id = 0x00000006;
} // namespace user_noc_fabric_index
esp_err_t refresh_token_attribute_get(uint16_t endpoint_id, char *refresh_token);
attribute_t *create_refresh_token(cluster_t *cluster, char *value, uint16_t length);
esp_err_t access_token_attribute_get(uint16_t endpoint_id, char *access_token);
attribute_t *create_access_token(cluster_t *cluster, char *value, uint16_t length);
esp_err_t authorized_attribute_update(uint16_t endpoint_id, bool authorized);
esp_err_t authorized_attribute_get(uint16_t endpoint_id, bool &authorized);
attribute_t *create_authorized(cluster_t *cluster, bool value);
esp_err_t user_noc_installed_attribute_get(uint16_t endpoint_id, bool &user_noc_installed);
attribute_t *create_user_noc_installed(cluster_t *cluster, bool value);
esp_err_t endpoint_url_attribute_get(uint16_t endpoint_id, char *endpoint_url);
attribute_t *create_endpoint_url(cluster_t *cluster, char *value, uint16_t length);
esp_err_t rainmaker_group_id_attribute_get(uint16_t endpoint_id, char *group_id);
attribute_t *create_rainmaker_group_id(cluster_t *cluster, char *value, uint16_t length);
esp_err_t user_noc_fabric_index_attribute_get(uint16_t endpoint_id, uint8_t &user_noc_fabric_index);
attribute_t *create_user_noc_fabric_index(cluster_t *cluster, uint8_t user_noc_fabric_index);
} // namespace attribute
namespace command {
namespace append_refresh_token {
static constexpr chip::CommandId Id = 0x00000000;
} // namespace append_refresh_token
command_t *create_append_refresh_token(cluster_t *cluster);
namespace reset_refresh_token {
static constexpr chip::CommandId Id = 0x00000001;
} // namespace reset_refresh_token
command_t *create_reset_refresh_token(cluster_t *cluster);
namespace authorize {
static constexpr chip::CommandId Id = 0x00000002;
} // namespace authorize
command_t *create_authorize(cluster_t *cluster);
namespace update_user_noc {
static constexpr chip::CommandId Id = 0x00000003;
} // namespace update_user_noc
command_t *create_update_user_noc(cluster_t *cluster);
namespace update_device_list {
static constexpr chip::CommandId Id = 0x00000004;
} // namespace update_device_list
command_t *create_update_device_list(cluster_t *cluster);
} // namespace command
cluster_t *create(endpoint_t *endpoint, uint8_t flags);
} // namespace matter_controller
} // namespace cluster
} // namespace esp_matter
@@ -1,635 +0,0 @@
// 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_crt_bundle.h>
#include <esp_http_client.h>
#include <esp_matter_controller_utils.h>
#include <json_generator.h>
#include <json_parser.h>
#include <matter_controller_cluster.h>
#include <matter_controller_device_mgr.h>
#include <lib/support/ScopedBuffer.h>
using chip::Platform::ScopedMemoryBufferWithSize;
using namespace esp_matter::cluster::matter_controller::attribute;
#define TAG "controller_dev_mgr"
namespace esp_matter {
namespace controller {
namespace device_mgr {
static matter_device_t *s_matter_device_list = NULL;
static device_list_update_callback_t s_device_list_update_cb = NULL;
static QueueHandle_t s_task_queue = NULL;
static TaskHandle_t s_device_mgr_task = NULL;
static SemaphoreHandle_t s_device_mgr_mutex = NULL;
typedef esp_err_t (*esp_matter_device_mgr_task_t)(void *);
typedef struct {
esp_matter_device_mgr_task_t task;
void *arg;
} task_post_t;
class scoped_device_mgr_lock {
public:
scoped_device_mgr_lock()
{
if (s_device_mgr_mutex) {
xSemaphoreTake(s_device_mgr_mutex, portMAX_DELAY);
} else {
ESP_LOGE(TAG, "device mgr lock not initialized");
}
}
~scoped_device_mgr_lock()
{
if (s_device_mgr_mutex) {
xSemaphoreGive(s_device_mgr_mutex);
} else {
ESP_LOGE(TAG, "device mgr lock not initialized");
}
}
};
void free_device_list(matter_device_t *dev_list)
{
matter_device_t *current = dev_list;
while (current) {
dev_list = dev_list->next;
free(current);
current = dev_list;
}
}
void print_device_list(matter_device_t *dev_list)
{
matter_device_t *dev = dev_list;
uint8_t dev_index = 0;
while (dev) {
ESP_LOGI(TAG, "device %d : {", dev_index);
ESP_LOGI(TAG, " rainmaker_node_id: %s,", dev->rainmaker_node_id);
ESP_LOGI(TAG, " matter_node_id: 0x%llX,", dev->node_id);
ESP_LOGI(TAG, " is_rainmaker_device: %s,", dev->is_rainmaker_device ? "true" : "false");
ESP_LOGI(TAG, " is_online: %s,", dev->reachable ? "true" : "false");
ESP_LOGI(TAG, " endpoints : [");
for (size_t i = 0; i < dev->endpoint_count; ++i) {
ESP_LOGI(TAG, " {");
ESP_LOGI(TAG, " endpoint_id: %d,", dev->endpoints[i].endpoint_id);
ESP_LOGI(TAG, " device_type_id: 0x%lx,", dev->endpoints[i].device_type_id);
ESP_LOGI(TAG, " device_name: %s,", dev->endpoints[i].device_name);
ESP_LOGI(TAG, " },");
}
ESP_LOGI(TAG, " ]");
ESP_LOGI(TAG, "}");
dev = dev->next;
dev_index++;
}
}
static matter_device_t *clone_device(matter_device_t *dev)
{
matter_device_t *ret = (matter_device_t *)malloc(sizeof(matter_device_t));
if (!ret) {
ESP_LOGE(TAG, "Failed to allocate memory for matter device struct");
return NULL;
}
memcpy(ret, dev, sizeof(matter_device_t));
ret->next = NULL;
return ret;
}
matter_device_t *get_device_list_clone()
{
matter_device_t *ret = NULL;
scoped_device_mgr_lock dev_mgr_lock;
matter_device_t *current = s_matter_device_list;
while (current) {
matter_device_t *tmp = clone_device(current);
if (!tmp) {
free_device_list(ret);
return NULL;
}
tmp->next = ret;
ret = tmp;
current = current->next;
}
return ret;
}
static matter_device_t *get_device(uint64_t node_id)
{
matter_device_t *ret = s_matter_device_list;
while (ret) {
if (ret->node_id == node_id) {
break;
}
ret = ret->next;
}
return ret;
}
static matter_device_t *get_device(char *rainmaker_node_id)
{
if (!rainmaker_node_id) {
return NULL;
}
matter_device_t *ret = s_matter_device_list;
while (ret) {
if (strncmp(ret->rainmaker_node_id, rainmaker_node_id,
strnlen(ret->rainmaker_node_id, sizeof(ret->rainmaker_node_id))) == 0) {
break;
}
ret = ret->next;
}
return ret;
}
matter_device_t *get_device_clone(uint64_t node_id)
{
return clone_device(get_device(node_id));
}
matter_device_t *get_device_clone(char *rainmaker_node_id)
{
return clone_device(get_device(rainmaker_node_id));
}
static esp_err_t get_node_reachable(jparse_ctx_t *jctx, bool *value)
{
bool value_got = false;
if (json_obj_get_object(jctx, "status") == 0) {
if (json_obj_get_object(jctx, "connectivity") == 0) {
if (json_obj_get_bool(jctx, "connected", value) == 0) {
value_got = true;
}
json_obj_leave_object(jctx);
}
json_obj_leave_object(jctx);
}
return value_got ? ESP_OK : ESP_FAIL;
}
static esp_err_t get_metadata(jparse_ctx_t *jctx, matter_device_t *dev)
{
if (json_obj_get_object(jctx, "metadata") == 0) {
int device_type = 0;
if (json_obj_get_int(jctx, "deviceType", &device_type) == 0) {
dev->endpoints[0].device_type_id = device_type;
int ep_count = 0;
int ep_id = 1;
if (json_obj_get_array(jctx, "endpointsData", &ep_count) == 0) {
json_arr_get_int(jctx, 1, &ep_id);
dev->endpoints[0].endpoint_id = ep_id;
json_obj_leave_array(jctx);
}
dev->endpoint_count = 1;
int device_name_len = ESP_MATTER_CONTROLLER_MAX_DEVICE_NAME_LEN;
if (json_obj_get_strlen(jctx, "deviceName", &device_name_len) == 0 &&
device_name_len < ESP_MATTER_CONTROLLER_MAX_DEVICE_NAME_LEN) {
json_obj_get_string(jctx, "deviceName", dev->endpoints[0].device_name,
ESP_MATTER_CONTROLLER_MAX_DEVICE_NAME_LEN);
dev->endpoints[0].device_name[device_name_len] = 0;
}
}
json_obj_get_bool(jctx, "isRainmaker", &(dev->is_rainmaker_device));
json_obj_leave_object(jctx);
}
return ESP_OK;
}
static esp_err_t fetch_node_metadata(ScopedMemoryBufferWithSize<char> &endpoint_url,
ScopedMemoryBufferWithSize<char> &access_token, uint16_t endpoint_id)
{
esp_err_t ret = ESP_OK;
matter_device_t *dev = s_matter_device_list;
char url[200];
int http_len, http_status_code;
esp_http_client_config_t config = {
.url = url,
.transport_type = HTTP_TRANSPORT_OVER_SSL,
.buffer_size = 4096,
.buffer_size_tx = 2048,
.skip_cert_common_name_check = false,
.crt_bundle_attach = esp_crt_bundle_attach,
};
esp_http_client_handle_t client = NULL;
jparse_ctx_t jctx;
int node_count = 0;
int node_index = 0;
char id_str[24] = {0};
int id_str_len = 0;
ScopedMemoryBufferWithSize<char> http_payload;
http_payload.Calloc(4096);
ESP_RETURN_ON_FALSE(http_payload.Get(), ESP_ERR_NO_MEM, TAG, "Failed to allocate memory for http_payload");
scoped_device_mgr_lock dev_mgr_lock;
while (dev) {
snprintf(url, sizeof(url), "%s/%s/%s?node_id=%s&%s", endpoint_url.Get(), HTTP_API_VERSION, "user/nodes",
dev->rainmaker_node_id, "node_details=true&is_matter=true");
client = esp_http_client_init(&config);
ESP_RETURN_ON_FALSE(client, ESP_FAIL, TAG, "Failed to initialise HTTP Client.");
ESP_GOTO_ON_ERROR(esp_http_client_set_header(client, "accept", "application/json"), cleanup, TAG,
"Failed to set http header accept");
ESP_GOTO_ON_ERROR(esp_http_client_set_header(client, "Authorization", access_token.Get()), cleanup, TAG,
"Failed to set http header Authorization");
ESP_GOTO_ON_ERROR(esp_http_client_set_method(client, HTTP_METHOD_GET), cleanup, TAG,
"Failed to set http method");
// HTTP GET Method
ESP_GOTO_ON_ERROR(esp_http_client_open(client, 0), cleanup, TAG, "Failed to open http connection");
http_len = esp_http_client_fetch_headers(client);
http_status_code = esp_http_client_get_status_code(client);
// Read Response
if ((http_len > 0) && (http_status_code == 200)) {
http_len = esp_http_client_read_response(client, http_payload.Get(), http_payload.AllocatedSize() - 1);
http_payload[http_len] = '\0';
} else {
http_len = esp_http_client_read_response(client, http_payload.Get(), http_payload.AllocatedSize() - 1);
http_payload[http_len] = '\0';
ESP_LOGE(TAG, "Invalid response for %s", url);
ESP_LOGE(TAG, "Status = %d, Data = %s", http_status_code, http_len > 0 ? http_payload.Get() : "None");
if (http_status_code == 401) {
cluster::matter_controller::attribute::authorized_attribute_update(endpoint_id, false);
}
ret = ESP_FAIL;
goto close;
}
ESP_LOGD(TAG, "http response payload: %s", http_payload.Get());
// Parse the http response
ESP_GOTO_ON_FALSE(json_parse_start(&jctx, http_payload.Get(), strlen(http_payload.Get())) == 0, ESP_FAIL, close,
TAG, "Failed to parse the http response json on json_parse_start");
if (json_obj_get_array(&jctx, "node_details", &node_count) == 0) {
for (node_index = 0; node_index < node_count; ++node_index) {
if (json_arr_get_object(&jctx, node_index) == 0) {
if (json_obj_get_strlen(&jctx, "id", &id_str_len) == 0 &&
json_obj_get_string(&jctx, "id", id_str, id_str_len + 1) == 0) {
id_str[id_str_len] = '\0';
dev = get_device(id_str);
if (dev) {
get_node_reachable(&jctx, &(dev->reachable));
get_metadata(&jctx, dev);
}
}
json_arr_leave_object(&jctx);
}
}
json_obj_leave_array(&jctx);
}
json_parse_end(&jctx);
dev = dev->next;
esp_http_client_close(client);
esp_http_client_cleanup(client);
client = NULL;
}
print_device_list(s_matter_device_list);
close:
if (client) {
esp_http_client_close(client);
}
cleanup:
if (client) {
esp_http_client_cleanup(client);
}
return ret;
}
#define CONTROLLER_NODE_TYPE "Controller"
typedef enum {
FIND_NODE_DETAILS_ARRAY,
READ_NODE_STRUCTURE_ELEMENTS,
READ_END,
} read_node_list_status_t;
static const char *find_sub_string(const char *str, const char *sub_str)
{
if (!str || !sub_str || strlen(str) < strlen(sub_str)) {
return nullptr;
}
while (*str) {
if (strncmp(str, sub_str, strlen(sub_str)) == 0) {
return str;
}
str++;
}
return nullptr;
}
static char *consume_buffer(char *buffer, size_t buffer_len, size_t consumed_buffer_len)
{
const char *moved_buffer_ptr = buffer + consumed_buffer_len;
size_t moved_buffer_len = strlen(moved_buffer_ptr);
for (size_t i = 0; i < moved_buffer_len; ++i) {
buffer[i] = moved_buffer_ptr[i];
}
buffer[moved_buffer_len] = 0;
return &buffer[moved_buffer_len];
}
static const char *get_element_end(const char *element_start)
{
bool in_quotation = false;
while (*element_start) {
if (*element_start == '}' && !in_quotation) {
return element_start;
}
if (*element_start == '"' && *(element_start - 1) != '\\') {
in_quotation = !in_quotation;
}
element_start++;
}
return nullptr;
}
static matter_device_t *parse_matter_device_element(const char *element_start, const char *element_end)
{
if (!element_start || !element_end || element_end <= element_start) {
return nullptr;
}
jparse_ctx_t jctx;
matter_device_t *device_element = nullptr;
int str_len;
char node_type_str[32];
if (json_parse_start(&jctx, element_start, element_end - element_start + 1) == 0) {
if (json_obj_get_strlen(&jctx, "type", &str_len) == 0 &&
json_obj_get_string(&jctx, "type", node_type_str, str_len + 1) == 0) {
if (strncmp(node_type_str, CONTROLLER_NODE_TYPE, strlen(CONTROLLER_NODE_TYPE)) == 0) {
// Skip the controller node
json_parse_end(&jctx);
return nullptr;
}
}
char matter_node_id_str[17];
if (json_obj_get_strlen(&jctx, "matter_node_id", &str_len) == 0 &&
json_obj_get_string(&jctx, "matter_node_id", matter_node_id_str, str_len + 1) == 0) {
matter_node_id_str[str_len] = '\0';
device_element = (matter_device_t *)calloc(1, sizeof(matter_device_t));
if (!device_element) {
ESP_LOGE(TAG, "Failed to alloc memory for device element");
json_parse_end(&jctx);
return nullptr;
}
device_element->node_id = strtoull(matter_node_id_str, NULL, 16);
if (json_obj_get_strlen(&jctx, "node_id", &str_len) == 0) {
json_obj_get_string(&jctx, "node_id", device_element->rainmaker_node_id, str_len + 1);
}
}
json_parse_end(&jctx);
}
return device_element;
}
static esp_err_t read_node_list(esp_http_client_handle_t client, matter_device_t **device_list)
{
ScopedMemoryBufferWithSize<char> http_response;
http_response.Alloc(1024);
ESP_RETURN_ON_FALSE(http_response.Get(), ESP_ERR_NO_MEM, TAG, "Failed to allocate buffer for http response");
int buffer_len_to_read = http_response.AllocatedSize() - 1;
char *buffer_ptr_to_read = http_response.Get();
read_node_list_status_t status = FIND_NODE_DETAILS_ARRAY;
bool is_complete_response_received = false;
do {
int read_len = 0;
if (!is_complete_response_received) {
read_len = esp_http_client_read_response(client, buffer_ptr_to_read, buffer_len_to_read);
buffer_ptr_to_read[read_len] = 0;
}
if (read_len < buffer_len_to_read) {
is_complete_response_received = true;
}
if (status == FIND_NODE_DETAILS_ARRAY) {
const char *node_details_ptr = find_sub_string(http_response.Get(), "\"node_details\":[{");
if (!node_details_ptr && is_complete_response_received) {
ESP_LOGE(TAG, "Cannot find node_details from the http response");
return ESP_ERR_NOT_FOUND;
}
if (node_details_ptr) {
status = READ_NODE_STRUCTURE_ELEMENTS;
}
// If node_details is not found, we need to read more response to the buffer to get to node_details.
// Otherwise we should consume the buffer before the first node structure element
size_t cusumed_buffer_size =
node_details_ptr ? node_details_ptr - http_response.Get() + 15 : http_response.AllocatedSize() - 40;
buffer_ptr_to_read =
consume_buffer(http_response.Get(), http_response.AllocatedSize() - 1, cusumed_buffer_size);
} else if (status == READ_NODE_STRUCTURE_ELEMENTS) {
if (http_response[0] == '[' || http_response[0] == ',') {
const char *element_start = http_response.Get() + 1;
if (*element_start != '{') {
return ESP_FAIL;
}
const char *element_end = get_element_end(element_start);
if (!element_end) {
return ESP_FAIL;
}
matter_device_t *device_entry = parse_matter_device_element(element_start, element_end);
if (device_entry) {
device_entry->next = *device_list;
*device_list = device_entry;
}
// Consume the parsed node element
buffer_ptr_to_read = consume_buffer(http_response.Get(), http_response.AllocatedSize() - 1,
element_end + 1 - http_response.Get());
} else if (http_response[0] == ']') {
// Node elements array ends with ']'
status = READ_END;
} else {
return ESP_FAIL;
}
}
// Try to fill all the buffer in the next response read.
buffer_len_to_read = http_response.Get() + http_response.AllocatedSize() - buffer_ptr_to_read - 1;
} while (status != READ_END);
return ESP_OK;
}
static esp_err_t fetch_node_list(ScopedMemoryBufferWithSize<char> &endpoint_url,
ScopedMemoryBufferWithSize<char> &access_token,
ScopedMemoryBufferWithSize<char> &rainmaker_group_id, uint16_t endpoint_id)
{
esp_err_t ret = ESP_OK;
char url[200];
int http_len, http_status_code;
ScopedMemoryBufferWithSize<char> http_payload;
matter_device_t *new_device_list = NULL;
snprintf(url, sizeof(url), "%s/%s/%s=%s&%s", endpoint_url.Get(), HTTP_API_VERSION, "user/node_group?group_id",
rainmaker_group_id.Get(), "node_details=true&sub_groups=false&node_list=true&is_matter=true");
esp_http_client_config_t config = {
.url = url,
.transport_type = HTTP_TRANSPORT_OVER_SSL,
.buffer_size = 1024,
.buffer_size_tx = 1536,
.skip_cert_common_name_check = false,
.crt_bundle_attach = esp_crt_bundle_attach,
};
esp_http_client_handle_t client = esp_http_client_init(&config);
ESP_RETURN_ON_FALSE(client, ESP_FAIL, TAG, "Failed to initialise HTTP Client.");
ESP_GOTO_ON_ERROR(esp_http_client_set_header(client, "accept", "application/json"), cleanup, TAG,
"Failed to set http header accept");
ESP_GOTO_ON_ERROR(esp_http_client_set_header(client, "Authorization", access_token.Get()), cleanup, TAG,
"Failed to set http header Authorization");
ESP_GOTO_ON_ERROR(esp_http_client_set_method(client, HTTP_METHOD_GET), cleanup, TAG, "Failed to set http method");
// HTTP GET Method
ESP_GOTO_ON_ERROR(esp_http_client_open(client, 0), cleanup, TAG, "Failed to open http connection");
http_len = esp_http_client_fetch_headers(client);
http_status_code = esp_http_client_get_status_code(client);
http_payload.Calloc(512);
ESP_GOTO_ON_FALSE(http_payload.Get(), ESP_ERR_NO_MEM, close, TAG, "Failed to allocate memory for http_payload");
// Read Response
if ((http_len == 0) || (http_status_code != 200)) {
http_len = esp_http_client_read_response(client, http_payload.Get(), http_payload.AllocatedSize() - 1);
http_payload[http_len] = '\0';
ESP_LOGE(TAG, "Invalid response for %s", url);
ESP_LOGE(TAG, "Status = %d, Data = %s", http_status_code, http_len > 0 ? http_payload.Get() : "None");
if (http_status_code == 401) {
cluster::matter_controller::attribute::authorized_attribute_update(endpoint_id, false);
}
ret = ESP_FAIL;
goto close;
}
// Read the http response
if (read_node_list(client, &new_device_list) == ESP_OK) {
scoped_device_mgr_lock dev_mgr_lock;
free_device_list(s_matter_device_list);
s_matter_device_list = new_device_list;
} else {
free_device_list(new_device_list);
}
close:
esp_http_client_close(client);
cleanup:
esp_http_client_cleanup(client);
return ret;
}
static esp_err_t update_device_list_task(void *endpoint_id_ptr)
{
ScopedMemoryBufferWithSize<char> endpoint_url;
ScopedMemoryBufferWithSize<char> rainmaker_group_id;
ScopedMemoryBufferWithSize<char> access_token;
uint16_t endpoint_id = *(uint16_t *)endpoint_id_ptr;
free(endpoint_id_ptr);
endpoint_url.Calloc(ESP_MATTER_RAINMAKER_MAX_ENDPOINT_URL_LEN);
ESP_RETURN_ON_FALSE(endpoint_url.Get(), ESP_ERR_NO_MEM, TAG, "Failed to alloc memory for endpoint_url");
rainmaker_group_id.Calloc(ESP_MATTER_RAINMAKER_MAX_GROUP_ID_LEN);
ESP_RETURN_ON_FALSE(rainmaker_group_id.Get(), ESP_ERR_NO_MEM, TAG, "Failed to alloc memory for rainmaker_group_id");
access_token.Calloc(ESP_MATTER_RAINMAKER_MAX_ACCESS_TOKEN_LEN);
ESP_RETURN_ON_FALSE(access_token.Get(), ESP_ERR_NO_MEM, TAG, "Failed to alloc memory for access_token");
ESP_RETURN_ON_ERROR(endpoint_url_attribute_get(endpoint_id, endpoint_url.Get()), TAG,
"Failed to get the endpoint_url");
ESP_RETURN_ON_ERROR(rainmaker_group_id_attribute_get(endpoint_id, rainmaker_group_id.Get()), TAG,
"Failed to get the rainmaker_group_id");
ESP_RETURN_ON_ERROR(access_token_attribute_get(endpoint_id, access_token.Get()), TAG,
"Failed to get the access_token");
ESP_RETURN_ON_ERROR(fetch_node_list(endpoint_url, access_token, rainmaker_group_id, endpoint_id), TAG,
"Failed to fetch the node list");
ESP_RETURN_ON_ERROR(fetch_node_metadata(endpoint_url, access_token, endpoint_id), TAG,
"Failed to fetch the node metadata");
if (s_device_list_update_cb) {
s_device_list_update_cb();
}
return ESP_OK;
}
esp_err_t update_device_list(uint16_t endpoint_id)
{
if (!s_task_queue) {
ESP_LOGE(TAG, "Failed to update device list as the task queue is not initialized");
return ESP_ERR_INVALID_STATE;
}
uint16_t *endpoint_id_ptr = (uint16_t *)malloc(sizeof(uint16_t));
*endpoint_id_ptr = endpoint_id;
task_post_t task_post = {
.task = update_device_list_task,
.arg = endpoint_id_ptr,
};
if (xQueueSend(s_task_queue, &task_post, portMAX_DELAY) != pdTRUE) {
free(endpoint_id_ptr);
ESP_LOGE(TAG, "Failed send update device list task");
return ESP_FAIL;
}
return ESP_OK;
}
static void device_mgr_task(void *aContext)
{
s_task_queue = xQueueCreate(8 /* Queue Size */, sizeof(task_post_t));
if (!s_task_queue) {
ESP_LOGE(TAG, "Failed to create device mgr task queue");
return;
}
s_device_mgr_mutex = xSemaphoreCreateRecursiveMutex();
if (!s_device_mgr_mutex) {
ESP_LOGE(TAG, "Failed to create device mgr lock");
vQueueDelete(s_task_queue);
return;
}
task_post_t task_post;
while (true) {
if (xQueueReceive(s_task_queue, &task_post, portMAX_DELAY) == pdTRUE) {
task_post.task(task_post.arg);
}
}
vQueueDelete(s_task_queue);
vSemaphoreDelete(s_device_mgr_mutex);
vTaskDelete(NULL);
}
esp_err_t init(uint16_t endpoint_id, device_list_update_callback_t dev_list_update_cb)
{
if (s_device_mgr_task) {
return ESP_OK;
}
uint8_t fabric_index;
bool user_noc_installed = false;
s_device_list_update_cb = dev_list_update_cb;
if (xTaskCreate(device_mgr_task, "device_mgr", 4096, NULL, 5, &s_device_mgr_task) != pdTRUE) {
ESP_LOGE(TAG, "Failed to create device mgr task");
return ESP_ERR_NO_MEM;
}
ESP_RETURN_ON_ERROR(user_noc_installed_attribute_get(endpoint_id, user_noc_installed), TAG,
"Failed to get the user_noc_installed");
if (user_noc_installed) {
// get the user_noc_fabric_index and pass it to the controller.
cluster::matter_controller::attribute::user_noc_fabric_index_attribute_get(endpoint_id, fabric_index);
esp_matter::controller::set_fabric_index(fabric_index);
// Do authorizing before update the device list
ESP_RETURN_ON_ERROR(controller_authorize(endpoint_id), TAG, "Failed to do authorizing");
ESP_RETURN_ON_ERROR(controller::device_mgr::update_device_list(endpoint_id), TAG,
"Failed to update device list");
}
return ESP_OK;
}
} // namespace device_mgr
} // namespace controller
} // namespace esp_matter
@@ -1,59 +0,0 @@
// 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>
namespace esp_matter {
namespace controller {
namespace device_mgr {
#define ESP_MATTER_DEVICE_MAX_ENDPOINT 8
#define ESP_MATTER_CONTROLLER_MAX_DEVICE_NAME_LEN 32
typedef struct endpoint_entry {
uint16_t endpoint_id;
uint32_t device_type_id;
char device_name[ESP_MATTER_CONTROLLER_MAX_DEVICE_NAME_LEN];
} endpoint_entry_t;
typedef struct matter_device {
uint64_t node_id;
char rainmaker_node_id[24];
uint8_t endpoint_count;
endpoint_entry_t endpoints[ESP_MATTER_DEVICE_MAX_ENDPOINT];
bool reachable;
bool is_rainmaker_device;
struct matter_device *next;
} matter_device_t;
typedef void (*device_list_update_callback_t)(void);
void free_device_list(matter_device_t *dev_list);
void print_device_list(matter_device_t *dev_list);
matter_device_t *get_device_list_clone();
matter_device_t *get_device_clone(uint64_t node_id);
matter_device_t *get_device_clone(char *rainmaker_node_id);
esp_err_t update_device_list(uint16_t endpoint_id);
esp_err_t init(uint16_t endpoint_id, device_list_update_callback_t dev_list_update_cb);
} // namespace device_mgr
} // namespace controller
} // namespace esp_matter