From a54560b836eb1ce2d08ad3d3636ae33d5a4b9353 Mon Sep 17 00:00:00 2001 From: WanqQixiang Date: Fri, 22 Dec 2023 14:15:08 +0800 Subject: [PATCH] example: Add component controller_rest_apis and controller_custom_cluster for matter controller usage --- .../controller_custom_cluster/CMakeLists.txt | 11 + .../common/controller_custom_cluster/Kconfig | 9 + .../controller_custom_cluster/README.md | 110 ++ .../include/matter_controller_cluster.h | 117 ++ .../include/matter_controller_device_mgr.h | 50 + .../matter_controller.xml | 61 + .../matter_controller_cluster.cpp | 796 +++++++++ .../matter_controller_device_mgr.cpp | 239 +++ .../controller_rest_apis/CMakeLists.txt | 7 + .../common/controller_rest_apis/Kconfig | 8 + .../controller_rest_apis.c | 1432 +++++++++++++++++ .../include/controller_rest_apis.h | 141 ++ .../include/matter_device.h | 61 + .../controller_rest_apis/matter_device.c | 54 + .../matter/matter_controller/CMakeLists.txt | 1 + .../matter_controller/sdkconfig.defaults | 2 +- .../CMakeLists.txt | 1 + .../main/linker.lf | 10 +- .../sdkconfig.defaults | 2 +- 19 files changed, 3101 insertions(+), 11 deletions(-) create mode 100644 examples/matter/common/controller_custom_cluster/CMakeLists.txt create mode 100644 examples/matter/common/controller_custom_cluster/Kconfig create mode 100644 examples/matter/common/controller_custom_cluster/README.md create mode 100644 examples/matter/common/controller_custom_cluster/include/matter_controller_cluster.h create mode 100644 examples/matter/common/controller_custom_cluster/include/matter_controller_device_mgr.h create mode 100644 examples/matter/common/controller_custom_cluster/matter_controller.xml create mode 100644 examples/matter/common/controller_custom_cluster/matter_controller_cluster.cpp create mode 100644 examples/matter/common/controller_custom_cluster/matter_controller_device_mgr.cpp create mode 100644 examples/matter/common/controller_rest_apis/CMakeLists.txt create mode 100644 examples/matter/common/controller_rest_apis/Kconfig create mode 100644 examples/matter/common/controller_rest_apis/controller_rest_apis.c create mode 100644 examples/matter/common/controller_rest_apis/include/controller_rest_apis.h create mode 100644 examples/matter/common/controller_rest_apis/include/matter_device.h create mode 100644 examples/matter/common/controller_rest_apis/matter_device.c diff --git a/examples/matter/common/controller_custom_cluster/CMakeLists.txt b/examples/matter/common/controller_custom_cluster/CMakeLists.txt new file mode 100644 index 0000000..9ccecea --- /dev/null +++ b/examples/matter/common/controller_custom_cluster/CMakeLists.txt @@ -0,0 +1,11 @@ +set(SRCS_LIST ) +set(INCLUDE_DIRS_LIST ) + +if (CONFIG_CONTROLLER_CUSTOM_CLUSTER_ENABLE) + list(APPEND SRCS_LIST "matter_controller_cluster.cpp" "matter_controller_device_mgr.cpp") + list(APPEND INCLUDE_DIRS_LIST "include") +endif() + +idf_component_register(SRCS ${SRCS_LIST} + INCLUDE_DIRS ${INCLUDE_DIRS_LIST} + REQUIRES controller_rest_apis chip esp_matter esp_matter_controller) diff --git a/examples/matter/common/controller_custom_cluster/Kconfig b/examples/matter/common/controller_custom_cluster/Kconfig new file mode 100644 index 0000000..813122f --- /dev/null +++ b/examples/matter/common/controller_custom_cluster/Kconfig @@ -0,0 +1,9 @@ +menu "Custom Controller Cluster" + config CONTROLLER_CUSTOM_CLUSTER_ENABLE + bool "Enable controller custom cluster" + depends on ESP_MATTER_CONTROLLER_ENABLE && ESP_MATTER_ENABLE_MATTER_SERVER && RMAKER_REST_API_ENABLED + default n + help + Enable the custom cluster of matter controller for Rainmaker Fabric suppport. + +endmenu diff --git a/examples/matter/common/controller_custom_cluster/README.md b/examples/matter/common/controller_custom_cluster/README.md new file mode 100644 index 0000000..7e212dc --- /dev/null +++ b/examples/matter/common/controller_custom_cluster/README.md @@ -0,0 +1,110 @@ +# 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 NOC with CAT of administer privilege, 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 | **RefreshTokenVerified** | bool | | N | false | 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 RefreshTokenVerified Attribute + +This attribute indicates whether current refresh_token is verified. It will be updated to **True** after success authorization and set to **False** after receiving ResetRefreshToken. + +### 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. diff --git a/examples/matter/common/controller_custom_cluster/include/matter_controller_cluster.h b/examples/matter/common/controller_custom_cluster/include/matter_controller_cluster.h new file mode 100644 index 0000000..5ca0fa0 --- /dev/null +++ b/examples/matter/common/controller_custom_cluster/include/matter_controller_cluster.h @@ -0,0 +1,117 @@ +// 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 + +#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); +const char *get_current_access_token(); +} + +namespace cluster { +namespace matter_controller { + +static constexpr chip::ClusterId Id = 0x131BFC01; + +namespace attribute { + +namespace refresh_token { +static constexpr chip::AttributeId Id = 0x00000000; +esp_err_t get(uint16_t endpoint_id, char *refresh_token); +attribute_t *create(cluster_t *cluster, char *value, uint16_t length); +} // namespace refresh_token + +namespace refresh_token_verified { +static constexpr chip::AttributeId Id = 0x00000001; +esp_err_t get(uint16_t endpoint_id, bool &refresh_token_verified); +attribute_t *create(cluster_t *cluster, bool refresh_token_verified); +} // namespace refresh_token_verified + +namespace authorized { +static constexpr chip::AttributeId Id = 0x00000002; +esp_err_t update(uint16_t endpoint_id, bool authorized); +esp_err_t get(uint16_t endpoint_id, bool &authorized); +attribute_t *create(cluster_t *cluster, bool value); +} // namespace authorized + +namespace user_noc_installed { +static constexpr chip::AttributeId Id = 0x00000003; +esp_err_t get(uint16_t endpoint_id, bool &user_noc_installed); +attribute_t *create(cluster_t *cluster, bool value); +} // namespace user_noc_installed + +namespace endpoint_url { +static constexpr chip::AttributeId Id = 0x00000004; +esp_err_t get(uint16_t endpoint_id, char *endpoint_url); +attribute_t *create(cluster_t *cluster, char *value, uint16_t length); +} // namespace endpoint_url + +namespace rainmaker_group_id { +static constexpr chip::AttributeId Id = 0x00000005; +esp_err_t get(uint16_t endpoint_id, char *group_id); +attribute_t *create(cluster_t *cluster, char *value, uint16_t length); +} // namespace rainmaker_group_id + +namespace user_noc_fabric_index { +static constexpr chip::AttributeId Id = 0x00000006; +esp_err_t get(uint16_t endpoint_id, uint8_t &user_noc_fabric_index); +attribute_t *create(cluster_t *cluster, uint8_t user_noc_fabric_index); +} // namespace user_noc_fabric_index + +} // namespace attribute + +namespace command { + +namespace append_refresh_token { +static constexpr chip::CommandId Id = 0x00000000; +command_t *create(cluster_t *cluster); +} // namespace append_refresh_token + +namespace reset_refresh_token { +static constexpr chip::CommandId Id = 0x00000001; +command_t *create(cluster_t *cluster); +} // namespace reset_refresh_token + +namespace authorize { +static constexpr chip::CommandId Id = 0x00000002; +command_t *create(cluster_t *cluster); +} // namespace authorize + +namespace update_user_noc { +static constexpr chip::CommandId Id = 0x00000003; +command_t *create(cluster_t *cluster); +} // namespace update_user_noc + +namespace update_device_list { +static constexpr chip::CommandId Id = 0x00000004; +command_t *create(cluster_t *cluster); +} // namespace update_device_list + +} // namespace command +cluster_t *create(endpoint_t *endpoint, uint8_t flags); +} // namespace matter_controller +} // namespace cluster +} // namespace esp_matter diff --git a/examples/matter/common/controller_custom_cluster/include/matter_controller_device_mgr.h b/examples/matter/common/controller_custom_cluster/include/matter_controller_device_mgr.h new file mode 100644 index 0000000..ab3dd8d --- /dev/null +++ b/examples/matter/common/controller_custom_cluster/include/matter_controller_device_mgr.h @@ -0,0 +1,50 @@ +// 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 + +namespace esp_matter { +namespace controller { +namespace device_mgr { + +using endpoint_entry_t = endpoint_entry_t; +using matter_device_t = matter_device_t; + +typedef void (*device_list_update_callback_t)(void); + +inline void free_device_list(matter_device_t *dev_list) +{ + free_matter_device_list(dev_list); +} + +inline void print_device_list(matter_device_t *dev_list) +{ + print_matter_device_list(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 diff --git a/examples/matter/common/controller_custom_cluster/matter_controller.xml b/examples/matter/common/controller_custom_cluster/matter_controller.xml new file mode 100644 index 0000000..10c30e5 --- /dev/null +++ b/examples/matter/common/controller_custom_cluster/matter_controller.xml @@ -0,0 +1,61 @@ + + + + + + + MatterController + CHIP + 0x131BFC01 + MATTER_CONTROLLER_CLUSTER + + Attributes and commands for matter_controller cluster. + + + + RefreshToken + RefreshTokenVerified + Authorized + UserNOCInstalled + EndpointUrl + RmakerGroupId + UserNOCFabricIndex + + + AppendRefreshToken command. + + + + + ResetRefreshToken command. + + + + Authorize command. + + + + + UpdateUserNOC command. + + + + UpdateDeviceList command. + + + + diff --git a/examples/matter/common/controller_custom_cluster/matter_controller_cluster.cpp b/examples/matter/common/controller_custom_cluster/matter_controller_cluster.cpp new file mode 100644 index 0000000..4f1d5c5 --- /dev/null +++ b/examples/matter/common/controller_custom_cluster/matter_controller_cluster.cpp @@ -0,0 +1,796 @@ +// 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_err.h" +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#define TAG "controller_custom_cluster" + +using chip::ByteSpan; +using chip::MutableByteSpan; +using chip::to_underlying; +using chip::app::ConcreteCommandPath; +using chip::Platform::ScopedMemoryBufferWithSize; +using chip::TLV::TLVReader; +using namespace chip::DeviceLayer; +using chip::System::Clock::Seconds32; + +constexpr char *controller_namespace = "controller"; + +namespace esp_matter { +namespace cluster { +namespace matter_controller { + +namespace attribute { + +namespace refresh_token { + +static esp_err_t update(uint16_t endpoint_id, const char *refresh_token) +{ + ESP_RETURN_ON_FALSE(refresh_token, ESP_ERR_INVALID_ARG, TAG, "refresh_token cannot be NULL"); + nvs_handle_t handle; + esp_err_t err = + nvs_open_from_partition(CONFIG_ESP_MATTER_NVS_PART_NAME, controller_namespace, NVS_READWRITE, &handle); + ESP_RETURN_ON_ERROR(err, TAG, "Failed to open controller namespace"); + char nvs_key[16]; + snprintf(nvs_key, 16, "rf_tk/%x", endpoint_id); + if ((err = nvs_set_str(handle, nvs_key, refresh_token)) != ESP_OK) { + ESP_LOGE(TAG, "Failed to set refresh_token"); + nvs_close(handle); + return err; + } + if ((err = nvs_commit(handle)) != ESP_OK) { + ESP_LOGE(TAG, "Failed to commit nvs"); + } + nvs_close(handle); + return err; +} + +esp_err_t get(uint16_t endpoint_id, char *refresh_token) +{ + ESP_RETURN_ON_FALSE(refresh_token, ESP_ERR_INVALID_ARG, TAG, "refresh_token cannot be NULL"); + nvs_handle_t handle; + esp_err_t err = + nvs_open_from_partition(CONFIG_ESP_MATTER_NVS_PART_NAME, controller_namespace, NVS_READONLY, &handle); + ESP_RETURN_ON_ERROR(err, TAG, "Failed to open controller namespace"); + char nvs_key[16]; + snprintf(nvs_key, 16, "rf_tk/%x", endpoint_id); + size_t refresh_token_len = ESP_MATTER_RAINMAKER_MAX_REFRESH_TOKEN_LEN; + err = nvs_get_str(handle, nvs_key, refresh_token, &refresh_token_len); + if (err == ESP_ERR_NVS_NOT_FOUND) { + refresh_token_len = 0; + err = ESP_OK; + } + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to get refresh_token"); + } + refresh_token[refresh_token_len] = 0; + nvs_close(handle); + return err; +} + +attribute_t *create(cluster_t *cluster, char *value, uint16_t length) +{ + return esp_matter::attribute::create(cluster, refresh_token::Id, ATTRIBUTE_FLAG_NONVOLATILE, + esp_matter_long_char_str(value, length)); +} + +} // namespace refresh_token + +namespace refresh_token_verified { + +static esp_err_t update(uint16_t endpoint_id, bool refresh_token_verified) +{ + esp_matter_attr_val_t val = esp_matter_bool(refresh_token_verified); + uint32_t cluster_id = matter_controller::Id; + uint32_t attribute_id = refresh_token_verified::Id; + return esp_matter::attribute::update(endpoint_id, cluster_id, attribute_id, &val); +} + +esp_err_t get(uint16_t endpoint_id, bool &refresh_token_verified) +{ + node_t *node = node::get(); + endpoint_t *endpoint = endpoint::get(node, endpoint_id); + cluster_t *cluster = cluster::get(endpoint, matter_controller::Id); + attribute_t *attribute = esp_matter::attribute::get(cluster, refresh_token_verified::Id); + ESP_RETURN_ON_FALSE(attribute, ESP_FAIL, TAG, "Could not find refresh_token_verified attribue"); + esp_matter_attr_val_t raw_val = esp_matter_invalid(NULL); + esp_matter::attribute::get_val(attribute, &raw_val); + ESP_RETURN_ON_FALSE(raw_val.type == ESP_MATTER_VAL_TYPE_BOOLEAN, ESP_FAIL, TAG, "Invalid Attribute type"); + refresh_token_verified = raw_val.val.b; + return ESP_OK; +} + +attribute_t *create(cluster_t *cluster, bool value) +{ + return esp_matter::attribute::create(cluster, refresh_token_verified::Id, ATTRIBUTE_FLAG_NONVOLATILE, + esp_matter_bool(value)); +} + +} // namespace refresh_token_verified + +namespace authorized { + +esp_err_t update(uint16_t endpoint_id, bool authorized) +{ + esp_matter_attr_val_t val = esp_matter_bool(authorized); + uint32_t cluster_id = matter_controller::Id; + uint32_t attribute_id = authorized::Id; + return esp_matter::attribute::update(endpoint_id, cluster_id, attribute_id, &val); +} + +esp_err_t get(uint16_t endpoint_id, bool &authorized) +{ + node_t *node = node::get(); + endpoint_t *endpoint = endpoint::get(node, endpoint_id); + cluster_t *cluster = cluster::get(endpoint, matter_controller::Id); + attribute_t *attribute = esp_matter::attribute::get(cluster, authorized::Id); + ESP_RETURN_ON_FALSE(attribute, ESP_FAIL, TAG, "Could not find authorized attribue"); + esp_matter_attr_val_t raw_val = esp_matter_invalid(NULL); + esp_matter::attribute::get_val(attribute, &raw_val); + ESP_RETURN_ON_FALSE(raw_val.type == ESP_MATTER_VAL_TYPE_BOOLEAN, ESP_FAIL, TAG, "Invalid Attribute type"); + authorized = raw_val.val.b; + return ESP_OK; +} + +attribute_t *create(cluster_t *cluster, bool value) +{ + return esp_matter::attribute::create(cluster, authorized::Id, ATTRIBUTE_FLAG_NONVOLATILE, esp_matter_bool(value)); +} + +} // namespace authorized + +namespace user_noc_installed { + +static esp_err_t update(uint16_t endpoint_id, bool user_noc_installed) +{ + esp_matter_attr_val_t val = esp_matter_bool(user_noc_installed); + uint32_t cluster_id = matter_controller::Id; + uint32_t attribute_id = user_noc_installed::Id; + return esp_matter::attribute::update(endpoint_id, cluster_id, attribute_id, &val); +} + +esp_err_t get(uint16_t endpoint_id, bool &user_noc_installed) +{ + node_t *node = node::get(); + endpoint_t *endpoint = endpoint::get(node, endpoint_id); + cluster_t *cluster = cluster::get(endpoint, matter_controller::Id); + attribute_t *attribute = esp_matter::attribute::get(cluster, user_noc_installed::Id); + ESP_RETURN_ON_FALSE(attribute, ESP_FAIL, TAG, "Could not find user_noc_installed attribue"); + esp_matter_attr_val_t raw_val = esp_matter_invalid(NULL); + esp_matter::attribute::get_val(attribute, &raw_val); + ESP_RETURN_ON_FALSE(raw_val.type == ESP_MATTER_VAL_TYPE_BOOLEAN, ESP_FAIL, TAG, "Invalid Attribute type"); + user_noc_installed = raw_val.val.b; + return ESP_OK; +} + +attribute_t *create(cluster_t *cluster, bool value) +{ + return esp_matter::attribute::create(cluster, user_noc_installed::Id, ATTRIBUTE_FLAG_NONVOLATILE, + esp_matter_bool(value)); +} + +} // namespace user_noc_installed + +namespace endpoint_url { + +static esp_err_t update(uint16_t endpoint_id, const char *endpoint_url) +{ + ESP_RETURN_ON_FALSE(endpoint_url, ESP_ERR_INVALID_ARG, TAG, "endpoint_url cannot be NULL"); + nvs_handle_t handle; + esp_err_t err = + nvs_open_from_partition(CONFIG_ESP_MATTER_NVS_PART_NAME, controller_namespace, NVS_READWRITE, &handle); + ESP_RETURN_ON_ERROR(err, TAG, "Failed to open controller namespace"); + char nvs_key[16]; + snprintf(nvs_key, 16, "ep_url/%x", endpoint_id); + if ((err = nvs_set_str(handle, nvs_key, endpoint_url)) != ESP_OK) { + ESP_LOGE(TAG, "Failed to set endpoint_url"); + nvs_close(handle); + return err; + } + if ((err = nvs_commit(handle)) != ESP_OK) { + ESP_LOGE(TAG, "Failed to commit nvs"); + } + nvs_close(handle); + return err; +} + +esp_err_t get(uint16_t endpoint_id, char *endpoint_url) +{ + ESP_RETURN_ON_FALSE(endpoint_url, ESP_ERR_INVALID_ARG, TAG, "endpoint_url cannot be NULL"); + nvs_handle_t handle; + esp_err_t err = + nvs_open_from_partition(CONFIG_ESP_MATTER_NVS_PART_NAME, controller_namespace, NVS_READONLY, &handle); + ESP_RETURN_ON_ERROR(err, TAG, "Failed to open controller namespace"); + char nvs_key[16]; + snprintf(nvs_key, 16, "ep_url/%x", endpoint_id); + size_t endpoint_url_len = ESP_MATTER_RAINMAKER_MAX_ENDPOINT_URL_LEN; + if ((err = nvs_get_str(handle, nvs_key, endpoint_url, &endpoint_url_len)) != ESP_OK) { + endpoint_url_len = 0; + ESP_LOGE(TAG, "Failed to get endpoint_url"); + } + nvs_close(handle); + endpoint_url[endpoint_url_len] = 0; + return ESP_OK; +} + +attribute_t *create(cluster_t *cluster, char *value, uint16_t length) +{ + return esp_matter::attribute::create(cluster, endpoint_url::Id, ATTRIBUTE_FLAG_NONVOLATILE, + esp_matter_char_str(value, length)); +} + +} // namespace endpoint_url + +namespace rainmaker_group_id { + +static esp_err_t update(uint16_t endpoint_id, const char *group_id) +{ + ESP_RETURN_ON_FALSE(group_id, ESP_ERR_INVALID_ARG, TAG, "group_id cannot be NULL"); + nvs_handle_t handle; + esp_err_t err = + nvs_open_from_partition(CONFIG_ESP_MATTER_NVS_PART_NAME, controller_namespace, NVS_READWRITE, &handle); + ESP_RETURN_ON_ERROR(err, TAG, "Failed to open controller namespace"); + char nvs_key[16]; + snprintf(nvs_key, 16, "rmk_gid/%x", endpoint_id); + if ((err = nvs_set_str(handle, nvs_key, group_id)) != ESP_OK) { + ESP_LOGE(TAG, "Failed to set rmaker group_id"); + nvs_close(handle); + return err; + } + if ((err = nvs_commit(handle)) != ESP_OK) { + ESP_LOGE(TAG, "Failed to commit nvs"); + } + nvs_close(handle); + return err; +} + +esp_err_t get(uint16_t endpoint_id, char *group_id) +{ + ESP_RETURN_ON_FALSE(group_id, ESP_ERR_INVALID_ARG, TAG, "endpoint_url cannot be NULL"); + nvs_handle_t handle; + esp_err_t err = + nvs_open_from_partition(CONFIG_ESP_MATTER_NVS_PART_NAME, controller_namespace, NVS_READONLY, &handle); + ESP_RETURN_ON_ERROR(err, TAG, "Failed to open controller namespace"); + char nvs_key[16]; + snprintf(nvs_key, 16, "rmk_gid/%x", endpoint_id); + size_t group_id_len = ESP_MATTER_RAINMAKER_MAX_GROUP_ID_LEN; + if ((err = nvs_get_str(handle, nvs_key, group_id, &group_id_len)) != ESP_OK) { + group_id_len = 0; + ESP_LOGE(TAG, "Failed to get rmaker group_id"); + } + nvs_close(handle); + group_id[group_id_len] = 0; + return ESP_OK; +} + +attribute_t *create(cluster_t *cluster, char *value, uint16_t length) +{ + return esp_matter::attribute::create(cluster, rainmaker_group_id::Id, ATTRIBUTE_FLAG_NONVOLATILE, + esp_matter_char_str(value, length)); +} + +} // namespace rainmaker_group_id + +namespace user_noc_fabric_index { + +static esp_err_t update(uint16_t endpoint_id, uint8_t fabric_index) +{ + esp_matter_attr_val_t val = esp_matter_uint8(fabric_index); + uint32_t cluster_id = matter_controller::Id; + uint32_t attribute_id = user_noc_fabric_index::Id; + return esp_matter::attribute::update(endpoint_id, cluster_id, attribute_id, &val); +} + +esp_err_t get(uint16_t endpoint_id, uint8_t &fabric_index) +{ + node_t *node = node::get(); + endpoint_t *endpoint = endpoint::get(node, endpoint_id); + cluster_t *cluster = cluster::get(endpoint, matter_controller::Id); + attribute_t *attribute = esp_matter::attribute::get(cluster, user_noc_fabric_index::Id); + ESP_RETURN_ON_FALSE(attribute, ESP_FAIL, TAG, "Could not find group_id attribue"); + esp_matter_attr_val_t raw_val = esp_matter_invalid(NULL); + esp_matter::attribute::get_val(attribute, &raw_val); + ESP_RETURN_ON_FALSE(raw_val.type == ESP_MATTER_VAL_TYPE_UINT8, ESP_FAIL, TAG, "Invalid Attribute type"); + fabric_index = raw_val.val.u8; + return ESP_OK; +} + +attribute_t *create(cluster_t *cluster, uint8_t value) +{ + return esp_matter::attribute::create(cluster, user_noc_fabric_index::Id, ATTRIBUTE_FLAG_NONVOLATILE, + esp_matter_uint8(value)); +} + +} // namespace user_noc_fabric_index + +} // namespace attribute + +namespace command { + +esp_err_t parse_string_from_tlv(TLVReader &tlv_data, ScopedMemoryBufferWithSize &str) +{ + chip::TLV::TLVType outer; + chip::CharSpan str_span; + if (chip::TLV::kTLVType_Structure != tlv_data.GetType()) { + return ESP_FAIL; + } + if (tlv_data.EnterContainer(outer) != CHIP_NO_ERROR) { + return ESP_FAIL; + } + while (tlv_data.Next() == CHIP_NO_ERROR) { + if (!chip::TLV::IsContextTag(tlv_data.GetTag())) { + continue; + } + if (chip::TLV::TagNumFromTag(tlv_data.GetTag()) == 0) { + chip::app::DataModel::Decode(tlv_data, str_span); + } + } + tlv_data.ExitContainer(outer); + ESP_RETURN_ON_FALSE(str_span.data() && str_span.size() > 0, ESP_FAIL, TAG, "Failed to decode the tlv_data"); + strncpy(str.Get(), str_span.data(), str_span.size()); + str[str_span.size()] = 0; + return ESP_OK; +} + +static esp_err_t append_refresh_token_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; + + // Return if this is not the controller authorize command + if (cluster_id != matter_controller::Id || command_id != matter_controller::command::append_refresh_token::Id) { + ESP_LOGE(TAG, "Got matter_controller command callback for some other command. This should not happen."); + return ESP_FAIL; + } + + ScopedMemoryBufferWithSize append_str; + append_str.Calloc(1025); + ESP_RETURN_ON_FALSE(append_str.Get(), ESP_ERR_NO_MEM, TAG, "Failed to allocate memory for append_str"); + ESP_RETURN_ON_ERROR(parse_string_from_tlv(tlv_data, append_str), TAG, + "Failed to parse appended_refresh_token from tlv_data"); + + ScopedMemoryBufferWithSize refresh_token; + refresh_token.Calloc(ESP_MATTER_RAINMAKER_MAX_REFRESH_TOKEN_LEN); + ESP_RETURN_ON_FALSE(refresh_token.Get(), ESP_ERR_NO_MEM, TAG, "Failed to allocate memory for refresh_token"); + ESP_RETURN_ON_ERROR(attribute::refresh_token::get(endpoint_id, refresh_token.Get()), TAG, + "Failed to get refresh_token"); + + size_t current_len = strnlen(refresh_token.Get(), refresh_token.AllocatedSize() - 1); + size_t append_len = strnlen(append_str.Get(), 1024); + strncpy(&refresh_token[current_len], append_str.Get(), append_len); + refresh_token[current_len + append_len] = 0; + ESP_RETURN_ON_ERROR(attribute::refresh_token::update(endpoint_id, refresh_token.Get()), TAG, + "Failed to update refresh_token"); + return ESP_OK; +} + +static esp_err_t reset_refresh_token_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; + + // Return if this is not the controller authorize command + if (cluster_id != matter_controller::Id || command_id != matter_controller::command::reset_refresh_token::Id) { + ESP_LOGE(TAG, "Got matter_controller command callback for some other command. This should not happen."); + return ESP_FAIL; + } + const char *empty_str = ""; + attribute::refresh_token::update(endpoint_id, empty_str); + attribute::refresh_token_verified::update(endpoint_id, false); + return ESP_OK; +} + +static esp_err_t authorize_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; + chip::app::CommandHandler *command_obj = (chip::app::CommandHandler *)opaque_ptr; + + // Return if this is not the controller authorize command + if (cluster_id != matter_controller::Id || command_id != matter_controller::command::authorize::Id) { + ESP_LOGE(TAG, "Got matter_controller command callback for some other command. This should not happen."); + return ESP_FAIL; + } + ScopedMemoryBufferWithSize endpoint_url; + + // Alloc memory for ScopedMemoryBuffers + 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"); + + // Parse the tlv data + ESP_RETURN_ON_ERROR(parse_string_from_tlv(tlv_data, endpoint_url), TAG, + "Failed to parse authorize command tlv data"); + // Flush acks before really slow work + command_obj->FlushAcksRightAwayOnSlowCommand(); + // Update the endpoint URL + ESP_RETURN_ON_ERROR(attribute::endpoint_url::update(endpoint_id, endpoint_url.Get()), TAG, + "Failed to update endpoint_url"); + + ESP_RETURN_ON_ERROR(controller::controller_authorize(endpoint_id), TAG, "Failed to authorize the controller"); + + return ESP_OK; +} + +static esp_err_t install_user_noc(uint16_t endpoint_id, uint8_t fabric_index) +{ + auto &fabric_table = chip::Server::GetInstance().GetFabricTable(); + const chip::FabricInfo *fabric_info = fabric_table.FindFabricWithIndex(fabric_index); + uint64_t fabric_id = fabric_info->GetFabricId(); + uint8_t csr_der_buf[chip::Crypto::kMIN_CSR_Buffer_Size]; + MutableByteSpan csr_span(csr_der_buf); + ScopedMemoryBufferWithSize rainmaker_group_id; + ScopedMemoryBufferWithSize endpoint_url; + ScopedMemoryBufferWithSize noc_der; + ScopedMemoryBufferWithSize noc_matter_cert; + MutableByteSpan matter_cert_noc; + size_t noc_der_len; + const char *access_token = controller::get_current_access_token(); + esp_err_t err = ESP_OK; + + // Alloc memory for ScopedMemoryBuffers + 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"); + 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"); + ESP_RETURN_ON_ERROR(attribute::endpoint_url::get(endpoint_id, endpoint_url.Get()), TAG, + "Failed to get endpoint_url"); + noc_der.Calloc(chip::Credentials::kMaxDERCertLength); + ESP_RETURN_ON_FALSE(noc_der.Get(), ESP_ERR_NO_MEM, TAG, "Failed to alloc memory for noc_der"); + noc_der_len = noc_der.AllocatedSize(); + noc_matter_cert.Calloc(chip::Credentials::kMaxCHIPCertLength); + ESP_RETURN_ON_FALSE(noc_matter_cert.Get(), ESP_ERR_NO_MEM, TAG, "Failed to alloc memory for noc_matter_cert"); + matter_cert_noc = MutableByteSpan(noc_matter_cert.Get(), noc_matter_cert.AllocatedSize()); + + // Fetch ther rainmaker_group_id + err = fetch_rainmaker_group_id(endpoint_url.Get(), access_token, fabric_id, rainmaker_group_id.Get(), + rainmaker_group_id.AllocatedSize()); + if (err == ESP_ERR_INVALID_STATE) { + attribute::authorized::update(endpoint_id, false); + } + ESP_RETURN_ON_ERROR(err, TAG, "Failed to fetch rainmaker_group_id"); + // Update the rainmaker_group_id + ESP_RETURN_ON_ERROR(attribute::rainmaker_group_id::update(endpoint_id, rainmaker_group_id.Get()), TAG, + "Failed to update rainmaker_group_id"); + // Allocate Pending CSR + chip::Server::GetInstance().GetFabricTable().AllocatePendingOperationalKey(chip::MakeOptional(fabric_index), + csr_span); + // Issue Controller NOC + uint64_t matter_node_id = fabric_info->GetNodeId(); + err = issue_noc_with_csr(endpoint_url.Get(), access_token, CSR_TYPE_CONTROLLER, csr_span.data(), csr_span.size(), + rainmaker_group_id.Get(), &matter_node_id, noc_der.Get(), &noc_der_len); + if (err == ESP_ERR_INVALID_STATE) { + attribute::authorized::update(endpoint_id, false); + } + ESP_RETURN_ON_ERROR(err, TAG, "Failed to issue user NOC"); + + // Convert DER-Formated NOC to Matter Cert + ESP_RETURN_ON_FALSE(chip::Credentials::ConvertX509CertToChipCert( + ByteSpan{reinterpret_cast(noc_der.Get()), noc_der_len}, + matter_cert_noc) == CHIP_NO_ERROR, + ESP_FAIL, TAG, "Failed to convert DER-Formated NOC to Matter Cert"); + + // Update NOC + ESP_RETURN_ON_FALSE(fabric_table.UpdatePendingFabricWithOperationalKeystore(fabric_index, matter_cert_noc, + ByteSpan{}) == CHIP_NO_ERROR, + ESP_FAIL, TAG, "Failed to update the Fabric NOC"); + ESP_RETURN_ON_FALSE(fabric_table.CommitPendingFabricData() == CHIP_NO_ERROR, ESP_FAIL, TAG, + "Failed to commit the pending Fabric data"); + + // Start DNS server to advertise the new node-id of the new NOC + chip::app::DnssdServer::Instance().StartServer(); + + // Update attribute + ESP_RETURN_ON_ERROR(attribute::user_noc_installed::update(endpoint_id, true), TAG, + "Failed to update user_noc_installed"); + ESP_RETURN_ON_ERROR(attribute::user_noc_fabric_index::update(endpoint_id, fabric_index), TAG, + "Failed to update user_noc_fabric_index"); + // Update controller fabric index + esp_matter::controller::set_fabric_index(fabric_index); + + return ESP_OK; +} + +static esp_err_t update_user_noc_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; + chip::app::CommandHandler *command_obj = (chip::app::CommandHandler *)opaque_ptr; + uint8_t fabric_index = command_obj->GetAccessingFabricIndex(); + + // Return if this is not the controller authorize command + if (cluster_id != matter_controller::Id || command_id != matter_controller::command::update_user_noc::Id) { + ESP_LOGE(TAG, "Got matter_controller command callback for some other command. This should not happen."); + return ESP_FAIL; + } + + // Get the authorized + bool authorized = false; + ESP_RETURN_ON_ERROR(attribute::authorized::get(endpoint_id, authorized), TAG, "Failed to get authorized attribute"); + if (!authorized) { + return ESP_ERR_INVALID_STATE; + } + + // Flush acks before really slow work + command_obj->FlushAcksRightAwayOnSlowCommand(); + // Install user NOC + ESP_RETURN_ON_ERROR(install_user_noc(endpoint_id, fabric_index), TAG, "Failed to install user NOC"); + + return ESP_OK; +} + +static esp_err_t update_device_list_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; + chip::app::CommandHandler *command_obj = (chip::app::CommandHandler *)opaque_ptr; + + // Return if this is not the controller authorize command + if (cluster_id != matter_controller::Id || command_id != matter_controller::command::update_device_list::Id) { + ESP_LOGE(TAG, "Got matter_controller command callback for some other command. This should not happen."); + return ESP_FAIL; + } + // Flush acks before really slow work + command_obj->FlushAcksRightAwayOnSlowCommand(); + + ESP_RETURN_ON_ERROR(controller::device_mgr::update_device_list(endpoint_id), TAG, "Failed to update device list"); + + return ESP_OK; +} + +namespace append_refresh_token { +command_t *create(cluster_t *cluster) +{ + return esp_matter::command::create(cluster, append_refresh_token::Id, COMMAND_FLAG_ACCEPTED | COMMAND_FLAG_CUSTOM, + append_refresh_token_command_callback); +} +} // namespace append_refresh_token + +namespace reset_refresh_token { +command_t *create(cluster_t *cluster) +{ + return esp_matter::command::create(cluster, reset_refresh_token::Id, COMMAND_FLAG_ACCEPTED | COMMAND_FLAG_CUSTOM, + reset_refresh_token_command_callback); +} +} // namespace reset_refresh_token + +namespace authorize { +command_t *create(cluster_t *cluster) +{ + return esp_matter::command::create(cluster, authorize::Id, COMMAND_FLAG_ACCEPTED | COMMAND_FLAG_CUSTOM, + authorize_command_callback); +} +} // namespace authorize + +namespace update_user_noc { +command_t *create(cluster_t *cluster) +{ + return esp_matter::command::create(cluster, update_user_noc::Id, COMMAND_FLAG_ACCEPTED | COMMAND_FLAG_CUSTOM, + update_user_noc_command_callback); +} +} // namespace update_user_noc + +namespace update_device_list { +command_t *create(cluster_t *cluster) +{ + return esp_matter::command::create(cluster, update_device_list::Id, COMMAND_FLAG_ACCEPTED | COMMAND_FLAG_CUSTOM, + update_device_list_command_callback); +} +} // namespace update_device_list + +} // namespace command + +using chip::app::AttributeAccessInterface; +using chip::app::AttributeValueDecoder; +using chip::app::AttributeValueEncoder; +using chip::app::ConcreteDataAttributePath; +using chip::app::ConcreteReadAttributePath; + +class MatterControllerAttrAccess : public AttributeAccessInterface { +public: + MatterControllerAttrAccess() + : AttributeAccessInterface(chip::Optional::Missing(), cluster::matter_controller::Id) + { + } + + CHIP_ERROR Read(const ConcreteReadAttributePath &aPath, AttributeValueEncoder &aEncoder) override; + CHIP_ERROR Write(const ConcreteDataAttributePath &aPath, AttributeValueDecoder &aDecoder) override; +}; + +static esp_err_t encode_string_on_success(esp_err_t err, AttributeValueEncoder encoder, char *str, size_t max_buf_size) +{ + ESP_RETURN_ON_ERROR(err, TAG, "error before encode string"); + ESP_RETURN_ON_FALSE(encoder.Encode(chip::CharSpan(str, strnlen(str, max_buf_size))) == CHIP_NO_ERROR, ESP_FAIL, TAG, + "Failed to encode string"); + return ESP_OK; +} + +CHIP_ERROR MatterControllerAttrAccess::Read(const ConcreteReadAttributePath &aPath, AttributeValueEncoder &aEncoder) +{ + if (aPath.mClusterId != cluster::matter_controller::Id) { + return CHIP_ERROR_INVALID_ARGUMENT; + } + uint16_t endpoint_id = aPath.mEndpointId; + esp_err_t err = ESP_OK; + switch (aPath.mAttributeId) { + case attribute::refresh_token::Id: { + ScopedMemoryBufferWithSize refresh_token; + refresh_token.Alloc(ESP_MATTER_RAINMAKER_MAX_REFRESH_TOKEN_LEN); + if (!refresh_token.Get()) { + return CHIP_ERROR_NO_MEMORY; + } + err = attribute::refresh_token::get(endpoint_id, refresh_token.Get()); + err = encode_string_on_success(err, aEncoder, refresh_token.Get(), refresh_token.AllocatedSize()); + break; + } + case attribute::endpoint_url::Id: { + ScopedMemoryBufferWithSize endpoint_url; + endpoint_url.Alloc(ESP_MATTER_RAINMAKER_MAX_ENDPOINT_URL_LEN); + if (!endpoint_url.Get()) { + return CHIP_ERROR_NO_MEMORY; + } + err = attribute::endpoint_url::get(endpoint_id, endpoint_url.Get()); + err = encode_string_on_success(err, aEncoder, endpoint_url.Get(), endpoint_url.AllocatedSize()); + break; + } + case attribute::rainmaker_group_id::Id: { + ScopedMemoryBufferWithSize rainmaker_group_id; + rainmaker_group_id.Alloc(ESP_MATTER_RAINMAKER_MAX_GROUP_ID_LEN); + if (!rainmaker_group_id.Get()) { + return CHIP_ERROR_NO_MEMORY; + } + err = attribute::rainmaker_group_id::get(endpoint_id, rainmaker_group_id.Get()); + err = encode_string_on_success(err, aEncoder, rainmaker_group_id.Get(), rainmaker_group_id.AllocatedSize()); + break; + } + default: + break; + } + if (err != ESP_OK) { + return CHIP_ERROR_INTERNAL; + } + return CHIP_NO_ERROR; +} + +CHIP_ERROR MatterControllerAttrAccess::Write(const ConcreteDataAttributePath &aPath, AttributeValueDecoder &aDecoder) +{ + if (aPath.mClusterId != cluster::matter_controller::Id) { + return CHIP_ERROR_INVALID_ARGUMENT; + } + return CHIP_NO_ERROR; +} + +MatterControllerAttrAccess g_attr_access; + +void controller_cluster_plugin_server_init_callback() +{ + registerAttributeAccessOverride(&g_attr_access); +} + +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, controller_cluster_plugin_server_init_callback); + add_function_list(cluster, NULL, 0); + + global::attribute::create_cluster_revision(cluster, 2); + global::attribute::create_feature_map(cluster, 0); + attribute::authorized::create(cluster, false); + attribute::user_noc_installed::create(cluster, false); + attribute::user_noc_fabric_index::create(cluster, 0); + // Attributes managed internally + attribute::refresh_token::create(cluster, NULL, 0); + attribute::refresh_token_verified::create(cluster, false); + attribute::endpoint_url::create(cluster, NULL, 0); + attribute::rainmaker_group_id::create(cluster, NULL, 0); + + command::append_refresh_token::create(cluster); + command::reset_refresh_token::create(cluster); + command::authorize::create(cluster); + command::update_user_noc::create(cluster); + command::update_device_list::create(cluster); + + return cluster; +} + +} // namespace matter_controller +} // namespace cluster + +namespace controller { + +static uint16_t s_access_token_expired_timer_endpoint_id = chip::kInvalidEndpointId; +static char s_access_token[ESP_MATTER_RAINMAKER_MAX_ACCESS_TOKEN_LEN]; + +static void access_token_expired_callback(chip::System::Layer *systemLayer, void *appState) +{ + uint16_t endpoint_id = s_access_token_expired_timer_endpoint_id; + if (cluster::matter_controller::attribute::authorized::update(endpoint_id, false) != ESP_OK) { + ESP_LOGE(TAG, "Failed to update authorized attribute"); + return; + } + if (controller_authorize(endpoint_id) != ESP_OK) { + ESP_LOGE(TAG, "Failed to do authorizing"); + } +} + +esp_err_t controller_authorize(uint16_t endpoint_id) +{ + ScopedMemoryBufferWithSize refresh_token; + ScopedMemoryBufferWithSize endpoint_url; + + // Alloc memory for ScopedMemoryBuffers + refresh_token.Calloc(ESP_MATTER_RAINMAKER_MAX_REFRESH_TOKEN_LEN); + ESP_RETURN_ON_FALSE(refresh_token.Get(), ESP_ERR_NO_MEM, TAG, "Failed to alloc memory for refresh_token"); + 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"); + + ESP_RETURN_ON_ERROR(cluster::matter_controller::attribute::endpoint_url::get(endpoint_id, endpoint_url.Get()), TAG, + "Failed to get the endpoint_url"); + ESP_RETURN_ON_ERROR(cluster::matter_controller::attribute::refresh_token::get(endpoint_id, refresh_token.Get()), + TAG, "Failed to get the refresh_token"); + + // Fetch the access_token + ESP_RETURN_ON_ERROR( + fetch_access_token(endpoint_url.Get(), refresh_token.Get(), s_access_token, sizeof(s_access_token)), + TAG, "Failed to fetch access_token for authorizing"); + // Update the authorized attribute + ESP_RETURN_ON_ERROR(cluster::matter_controller::attribute::authorized::update(endpoint_id, true), TAG, + "Failed to update authorized attribute"); + ESP_RETURN_ON_ERROR(cluster::matter_controller::attribute::refresh_token_verified::update(endpoint_id, true), + TAG, "Failed to update refresh_token_verified attribute"); + // The access token will be expired after one hour + s_access_token_expired_timer_endpoint_id = endpoint_id; + SystemLayer().CancelTimer(access_token_expired_callback, NULL); + SystemLayer().StartTimer(Seconds32(3550), access_token_expired_callback, NULL); + + return ESP_OK; +} + +const char *get_current_access_token() +{ + return s_access_token; +} + +} // namespace controller +} // namespace esp_matter diff --git a/examples/matter/common/controller_custom_cluster/matter_controller_device_mgr.cpp b/examples/matter/common/controller_custom_cluster/matter_controller_device_mgr.cpp new file mode 100644 index 0000000..0d85037 --- /dev/null +++ b/examples/matter/common/controller_custom_cluster/matter_controller_device_mgr.cpp @@ -0,0 +1,239 @@ +// 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 + +#include + +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"); + } + } +}; + +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_matter_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 update_device_list_task(void *endpoint_id_ptr) { + esp_err_t err = ESP_OK; + ScopedMemoryBufferWithSize endpoint_url; + ScopedMemoryBufferWithSize rainmaker_group_id; + 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"); + + ESP_RETURN_ON_ERROR(endpoint_url::get(endpoint_id, endpoint_url.Get()), TAG, + "Failed to get the endpoint_url"); + ESP_RETURN_ON_ERROR( + rainmaker_group_id::get(endpoint_id, rainmaker_group_id.Get()), TAG, + "Failed to get the rainmaker_group_id"); + const char *access_token = controller::get_current_access_token(); + + matter_device_t *dev_list = NULL; + err = fetch_matter_device_list(endpoint_url.Get(), access_token, + rainmaker_group_id.Get(), &dev_list); + if (err == ESP_ERR_INVALID_STATE) { + authorized::update(endpoint_id, false); + } + ESP_RETURN_ON_ERROR(err, TAG, "Failed to get the matter device list"); + print_matter_device_list(dev_list); + { + scoped_device_mgr_lock lock; + s_matter_device_list = dev_list; + } + 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::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::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 diff --git a/examples/matter/common/controller_rest_apis/CMakeLists.txt b/examples/matter/common/controller_rest_apis/CMakeLists.txt new file mode 100644 index 0000000..e2a2b51 --- /dev/null +++ b/examples/matter/common/controller_rest_apis/CMakeLists.txt @@ -0,0 +1,7 @@ +if (CONFIG_RMAKER_REST_API_ENABLED) + list(APPEND SRCS_LIST "controller_rest_apis.c" "matter_device.c") + list(APPEND INCLUDE_DIRS_LIST "include") +endif() +idf_component_register(SRCS ${SRCS_LIST} + INCLUDE_DIRS ${INCLUDE_DIRS_LIST} + REQUIRES mbedtls json_parser esp_http_client json_generator rmaker_common) diff --git a/examples/matter/common/controller_rest_apis/Kconfig b/examples/matter/common/controller_rest_apis/Kconfig new file mode 100644 index 0000000..ee2e1f9 --- /dev/null +++ b/examples/matter/common/controller_rest_apis/Kconfig @@ -0,0 +1,8 @@ +menu "RMaker REST API" + config RMAKER_REST_API_ENABLED + bool "Enable RMaker REST API" + default y + help + Enable RainMaker REST API + +endmenu diff --git a/examples/matter/common/controller_rest_apis/controller_rest_apis.c b/examples/matter/common/controller_rest_apis/controller_rest_apis.c new file mode 100644 index 0000000..f590b4e --- /dev/null +++ b/examples/matter/common/controller_rest_apis/controller_rest_apis.c @@ -0,0 +1,1432 @@ +// 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 +#include +#include +#include +#include +#include +#include +#include +#include + +#define TAG "RMAKER_REST" +#define HTTP_API_VERSION "v1" +#define IPK_BYTES_LEN 16 + +esp_err_t fetch_access_token(const char *endpoint_url, + const char *refresh_token, char *access_token, + size_t access_token_buf_len) { + ESP_RETURN_ON_FALSE(endpoint_url, ESP_ERR_INVALID_ARG, TAG, + "endpoint_url cannot be NULL"); + ESP_RETURN_ON_FALSE(refresh_token, ESP_ERR_INVALID_ARG, TAG, + "refresh_token cannot be NULL"); + ESP_RETURN_ON_FALSE(access_token, ESP_ERR_INVALID_ARG, TAG, + "access_token cannot be NULL"); + + esp_err_t ret = ESP_OK; + char url[100]; + snprintf(url, sizeof(url), "%s/%s/%s", endpoint_url, HTTP_API_VERSION, + "login2"); + esp_http_client_config_t config = { + .url = url, + .transport_type = HTTP_TRANSPORT_OVER_SSL, + .buffer_size = 2048, + .skip_cert_common_name_check = false, + .crt_bundle_attach = esp_crt_bundle_attach, + }; + char *http_payload = NULL; + const size_t http_payload_size = 4096; + size_t http_payload_len = 0; + int http_len, http_status_code; + int access_token_len; + json_gen_str_t jstr; + jparse_ctx_t jctx; + + // Initialize http client + 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, "Content-Type", "application/json"), + cleanup, TAG, "Failed to set http header Content-Type"); + ESP_GOTO_ON_ERROR(esp_http_client_set_method(client, HTTP_METHOD_POST), + cleanup, TAG, "Failed to set http method"); + + // Prepare the payload for http write and read + // The http response will include id_token and access_token, so we allocate 4K + // Bytes buffer for the reponse. + http_payload = (char *)MEM_CALLOC_EXTRAM(http_payload_size, sizeof(char)); + ESP_GOTO_ON_FALSE(http_payload, ESP_ERR_NO_MEM, cleanup, TAG, + "Failed to alloc memory for http_payload"); + json_gen_str_start(&jstr, http_payload, http_payload_size - 1, NULL, NULL); + json_gen_start_object(&jstr); + json_gen_obj_set_string(&jstr, "refreshtoken", (char *)refresh_token); + json_gen_end_object(&jstr); + json_gen_str_end(&jstr); + ESP_LOGD(TAG, "http write payload: %s", http_payload); + + // Send POST data + http_payload_len = strnlen(http_payload, http_payload_size - 1); + ESP_GOTO_ON_ERROR(esp_http_client_open(client, http_payload_len), cleanup, + TAG, "Failed to open http connection"); + http_len = esp_http_client_write(client, http_payload, http_payload_len); + ESP_GOTO_ON_FALSE(http_len == http_payload_len, ESP_FAIL, close, TAG, + "Failed to write Payload. Returned len = %d", http_len); + + // Get response data + http_len = esp_http_client_fetch_headers(client); + http_status_code = esp_http_client_get_status_code(client); + if ((http_len > 0) && (http_status_code == 200)) { + http_len = esp_http_client_read_response(client, http_payload, + http_payload_size - 1); + http_payload[http_len] = 0; + } else { + http_len = esp_http_client_read_response(client, http_payload, + http_payload_size - 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 : "None"); + ret = ESP_FAIL; + goto close; + } + + // Parse the response payload + ESP_LOGD(TAG, "http response:%s", http_payload); + ESP_GOTO_ON_FALSE( + json_parse_start(&jctx, http_payload, http_len) == 0, ESP_FAIL, close, + TAG, "Failed to parse the http response json on json_parse_start"); + if (json_obj_get_strlen(&jctx, "accesstoken", &access_token_len) != 0 || + access_token_len >= access_token_buf_len || + json_obj_get_string(&jctx, "accesstoken", access_token, + access_token_buf_len - 1) != 0) { + ESP_LOGE(TAG, + "Failed to parse the access token from the http response json"); + ret = ESP_FAIL; + } else { + access_token[access_token_len] = 0; + } + json_parse_end(&jctx); +close: + esp_http_client_close(client); +cleanup: + esp_http_client_cleanup(client); + if (http_payload) { + free(http_payload); + } + return ret; +} + +static esp_err_t fetch_rainmaker_group_id_pagination( + const char *endpoint_url, const char *access_token, + const uint64_t fabric_id, char *group_id, size_t group_id_buf_len, + char *start_id, size_t start_id_buf_len, const int num_records, + int *group_count) { + ESP_RETURN_ON_FALSE(endpoint_url, ESP_ERR_INVALID_ARG, TAG, + "endpoint_url cannot be NULL"); + ESP_RETURN_ON_FALSE(access_token, ESP_ERR_INVALID_ARG, TAG, + "access_token cannot be NULL"); + ESP_RETURN_ON_FALSE(group_id, ESP_ERR_INVALID_ARG, TAG, + "group_id cannot be NULL"); + ESP_RETURN_ON_FALSE(start_id, ESP_ERR_INVALID_ARG, TAG, + "start_id cannot be NULL"); + ESP_RETURN_ON_FALSE(group_count, ESP_ERR_INVALID_ARG, TAG, + "group_count cannot be NULL"); + + esp_err_t ret = ESP_OK; + char url[256] = {0}; + if (start_id[0] != 0 && strnlen(start_id, start_id_buf_len - 1) > 0) { + snprintf(url, sizeof(url), "%s/%s/%s?%s&start_id=%s&num_records=%d", + endpoint_url, HTTP_API_VERSION, "user/node_group", + "node_list=false&sub_groups=false&node_details=false&is_matter=" + "true&fabric_details=false", + start_id, num_records); + } else { + snprintf(url, sizeof(url), "%s/%s/%s?%s&num_records=%d", endpoint_url, + HTTP_API_VERSION, "user/node_group", + "node_list=false&sub_groups=false&node_details=false&is_matter=" + "true&fabric_details=false", + num_records); + } + + esp_http_client_config_t config = { + .url = url, + .transport_type = HTTP_TRANSPORT_OVER_SSL, + .buffer_size = 1526, + .buffer_size_tx = 2048, + .skip_cert_common_name_check = false, + .crt_bundle_attach = esp_crt_bundle_attach, + }; + int http_len, http_status_code; + jparse_ctx_t jctx; + int group_index; + char fabric_id_str[17]; + int rainmaker_group_id_len = 0; + int start_id_len = 0; + char *http_payload = NULL; + const size_t http_payload_size = 1024; + + 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), + cleanup, TAG, "Failed to set http header Authorization"); + + // HTTP GET + ESP_GOTO_ON_ERROR(esp_http_client_set_method(client, HTTP_METHOD_GET), + cleanup, TAG, "Failed to set http method"); + ESP_GOTO_ON_ERROR(esp_http_client_open(client, 0), cleanup, TAG, + "Failed to open http connection"); + + // Read response + http_payload = (char *)MEM_CALLOC_EXTRAM(http_payload_size, sizeof(char)); + ESP_GOTO_ON_FALSE(http_payload, ESP_ERR_NO_MEM, close, TAG, + "Failed to alloc memory for http_payload"); + http_len = esp_http_client_fetch_headers(client); + http_status_code = esp_http_client_get_status_code(client); + if ((http_len > 0) && (http_status_code == 200)) { + http_len = esp_http_client_read_response(client, http_payload, + http_payload_size - 1); + http_payload[http_len] = 0; + } else { + http_len = esp_http_client_read_response(client, http_payload, + http_payload_size - 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 : "None"); + ret = http_status_code == 401 ? ESP_ERR_INVALID_STATE : ESP_FAIL; + goto close; + } + + // Parse the response payload + ESP_LOGD(TAG, "http response:%s", http_payload); + ESP_GOTO_ON_FALSE( + json_parse_start(&jctx, http_payload, http_len) == 0, ESP_FAIL, close, + TAG, "Failed to parse the http response json on json_parse_start"); + if (json_obj_get_array(&jctx, "groups", group_count) != 0) { + ESP_LOGE(TAG, "Failed to parse the groups array from the http response"); + json_parse_end(&jctx); + ret = ESP_FAIL; + goto close; + } + for (group_index = 0; group_index < *group_count; ++group_index) { + if (json_arr_get_object(&jctx, group_index) == 0) { + if (json_obj_get_string(&jctx, "fabric_id", fabric_id_str, + sizeof(fabric_id_str)) == 0) { + if (strtoull(fabric_id_str, NULL, 16) == fabric_id) { + if (json_obj_get_strlen(&jctx, "group_id", &rainmaker_group_id_len) != + 0 || + json_obj_get_string(&jctx, "group_id", group_id, + group_id_buf_len - 1) != 0) { + ESP_LOGE(TAG, "Failed to parse the group_id for fabric: 0x%llu", + fabric_id); + ret = ESP_FAIL; + } else { + group_id[rainmaker_group_id_len] = 0; + } + json_arr_leave_object(&jctx); + break; + } + } + json_arr_leave_object(&jctx); + } + } + json_obj_leave_array(&jctx); + // Get the next_id for the next fetch + if (group_index == *group_count) { + ret = ESP_ERR_NOT_FOUND; + if (json_obj_get_strlen(&jctx, "next_id", &start_id_len) == 0 && + json_obj_get_string(&jctx, "next_id", start_id, start_id_buf_len - 1) == + 0) { + start_id[start_id_len] = 0; + } else { + ret = ESP_FAIL; + } + } + json_parse_end(&jctx); +close: + esp_http_client_close(client); +cleanup: + esp_http_client_cleanup(client); + if (http_payload) { + free(http_payload); + } + return ret; +} + +esp_err_t fetch_rainmaker_group_id(const char *endpoint_url, + const char *access_token, + const uint64_t fabric_id, char *group_id, + size_t group_id_buf_len) { + esp_err_t err = ESP_OK; + char next_group_id[24] = {0}; + int group_count = 0; + const int num_records = 4; + + do { + err = fetch_rainmaker_group_id_pagination( + endpoint_url, access_token, fabric_id, group_id, group_id_buf_len, + next_group_id, sizeof(next_group_id), num_records, &group_count); + } while (err == ESP_ERR_NOT_FOUND && group_count == num_records); + + return err; +} + +esp_err_t fetch_matter_fabric_id(const char *endpoint_url, + const char *access_token, const char *group_id, + uint64_t *fabric_id) { + ESP_RETURN_ON_FALSE(endpoint_url, ESP_ERR_INVALID_ARG, TAG, + "endpoint_url cannot be NULL"); + ESP_RETURN_ON_FALSE(access_token, ESP_ERR_INVALID_ARG, TAG, + "access_token cannot be NULL"); + ESP_RETURN_ON_FALSE(group_id, ESP_ERR_INVALID_ARG, TAG, + "group_id cannot be NULL"); + ESP_RETURN_ON_FALSE(fabric_id, ESP_ERR_INVALID_ARG, TAG, + "fabric_id cannot be NULL"); + + esp_err_t ret = ESP_OK; + char url[256]; + snprintf(url, sizeof(url), + "%s/%s/" + "%s?group_id=%s&node_list=false&sub_groups=false&node_details=false&" + "is_matter=true&" + "fabric_details=false", + endpoint_url, HTTP_API_VERSION, "user/node_group", group_id); + esp_http_client_config_t config = { + .url = url, + .transport_type = HTTP_TRANSPORT_OVER_SSL, + .buffer_size = 1024, + .buffer_size_tx = 2048, + .skip_cert_common_name_check = false, + .crt_bundle_attach = esp_crt_bundle_attach, + }; + char *http_payload = NULL; + const size_t http_payload_size = 1024; + char fabric_id_str[17] = {0}; + int group_size = 0, fabric_id_str_len = 0; + int http_len, http_status_code; + jparse_ctx_t jctx; + + // Initialize http client + 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, "Content-Type", "application/json"), + cleanup, TAG, "Failed to set http header Content-Type"); + ESP_GOTO_ON_ERROR( + esp_http_client_set_header(client, "Authorization", access_token), + cleanup, TAG, "Failed to set http header Authorization"); + + // HTTP GET + ESP_GOTO_ON_ERROR(esp_http_client_set_method(client, HTTP_METHOD_GET), + cleanup, TAG, "Failed to set http method"); + ESP_GOTO_ON_ERROR(esp_http_client_open(client, 0), cleanup, TAG, + "Failed to open http connection"); + + // Read response + http_payload = (char *)MEM_CALLOC_EXTRAM(http_payload_size, sizeof(char)); + ESP_GOTO_ON_FALSE(http_payload, ESP_ERR_NO_MEM, close, TAG, + "Failed to alloc memory for http_payload"); + http_len = esp_http_client_fetch_headers(client); + http_status_code = esp_http_client_get_status_code(client); + if ((http_len > 0) && (http_status_code == 200)) { + http_len = esp_http_client_read_response(client, http_payload, + http_payload_size - 1); + http_payload[http_len] = 0; + } else { + http_len = esp_http_client_read_response(client, http_payload, + http_payload_size - 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 : "None"); + ret = http_status_code == 401 ? ESP_ERR_INVALID_STATE : ESP_FAIL; + goto close; + } + + // Parse the response payload + ESP_LOGI(TAG, "http response:%s", http_payload); + ret = ESP_FAIL; + ESP_GOTO_ON_FALSE( + json_parse_start(&jctx, http_payload, http_len) == 0, ESP_FAIL, close, + TAG, "Failed to parse the http response json on json_parse_start"); + if (json_obj_get_array(&jctx, "groups", &group_size) == 0 && + group_size == 1) { + if (json_arr_get_object(&jctx, 0) == 0) { + if (json_obj_get_strlen(&jctx, "fabric_id", &fabric_id_str_len) == 0 && + fabric_id_str_len == 16 && + json_obj_get_string(&jctx, "fabric_id", fabric_id_str, + sizeof(fabric_id_str)) == 0) { + fabric_id_str[fabric_id_str_len] = 0; + *fabric_id = strtoull(fabric_id_str, NULL, 16); + if (*fabric_id != 0) { + ret = ESP_OK; + } + } + json_arr_leave_object(&jctx); + } + json_obj_leave_array(&jctx); + } + json_parse_end(&jctx); +close: + esp_http_client_close(client); +cleanup: + esp_http_client_cleanup(client); + if (http_payload) { + free(http_payload); + } + return ret; +} + +#define PEM_BEGIN_CSR "-----BEGIN CERTIFICATE REQUEST-----\n" +#define PEM_END_CSR "-----END CERTIFICATE REQUEST-----\n" + +static size_t format_csr(const char *input, char *output, + size_t output_buf_size) { + size_t output_len = 0; + // replace '\n' to '\\'+'n' so it can be the JSON input of http client + while (*input) { + if (*input != '\n') { + if (output_len >= output_buf_size - 1) { + return 0; + } + output[output_len] = *input; + output_len++; + } else { + if (output_len >= output_buf_size - 2) { + return 0; + } + output[output_len] = '\\'; + output[output_len + 1] = 'n'; + output_len += 2; + } + input++; + } + output[output_len] = 0; + return output_len; +} + +static size_t deformat_cert(const char *input, char *output, + size_t output_buf_size) { + size_t output_len = 0; + // replace '\\'+'n' to '\n' so it can be converted to DER-Encoded certificate + while (*input) { + if (*input == '\\' && *(input + 1) == 'n') { + output[output_len] = '\n'; + input += 2; + } else { + output[output_len] = *input; + input++; + } + output_len++; + if (output_len >= output_buf_size) { + return 0; + } + } + output[output_len] = 0; + return output_len; +} + +static esp_err_t convert_der_to_pem(const uint8_t *der, size_t der_len, + char *pem_buf, size_t pem_buf_size) { + ESP_RETURN_ON_FALSE(der && der_len > 0, ESP_ERR_INVALID_ARG, TAG, + "der cannot be NULL"); + ESP_RETURN_ON_FALSE(pem_buf && pem_buf_size > der_len, ESP_ERR_INVALID_ARG, + TAG, "pem_buf cannot be NULL"); + size_t pem_len = pem_buf_size; + // copy the csr_der to the end of the csr_pem buffer + memcpy(pem_buf + pem_buf_size - der_len, der, der_len); + ESP_RETURN_ON_FALSE(mbedtls_pem_write_buffer( + PEM_BEGIN_CSR, PEM_END_CSR, + (uint8_t *)(pem_buf + pem_buf_size - der_len), + der_len, (uint8_t *)(pem_buf), 1024, &pem_len) == 0, + ESP_FAIL, TAG, + "Failed to convert DER-Encoded CSR to PEM-Encoded CSR"); + return ESP_OK; +} + +static esp_err_t convert_pem_to_der(const char *pem, uint8_t *der_buf, + size_t *der_len) { + ESP_RETURN_ON_FALSE(pem && strlen(pem) > 0, ESP_ERR_INVALID_ARG, TAG, + "pem cannot be NULL"); + ESP_RETURN_ON_FALSE(der_buf && der_len, ESP_ERR_INVALID_ARG, TAG, + "der_buf cannot be NULL"); + size_t pem_len = strlen(pem); + size_t len = 0; + const char *s1, *s2, *end = pem + pem_len; + s1 = (char *)strstr(pem, "-----BEGIN"); + if (s1 == NULL) { + return ESP_FAIL; + } + s2 = (char *)strstr(pem, "-----END"); + if (s2 == NULL) { + return ESP_FAIL; + } + s1 += 10; + while (s1 < end && *s1 != '-') { + s1++; + } + while (s1 < end && *s1 == '-') { + s1++; + } + if (*s1 == '\r') { + s1++; + } + if (*s1 == '\n') { + s1++; + } + int ret = + mbedtls_base64_decode(NULL, 0, &len, (const unsigned char *)s1, s2 - s1); + if (ret == MBEDTLS_ERR_BASE64_INVALID_CHARACTER) { + return ESP_FAIL; + } + if (len > *der_len) { + return ESP_FAIL; + } + if ((ret = mbedtls_base64_decode(der_buf, len, &len, + (const unsigned char *)s1, s2 - s1)) != 0) { + return ESP_FAIL; + } + *der_len = len; + return ESP_OK; +} + +static int convert_char_to_digit(char ch) { + if (ch >= '0' && ch <= '9') { + return ch - '0'; + } else if (ch >= 'a' && ch <= 'f') { + return ch - 'a' + 10; + } else if (ch >= 'A' && ch <= 'F') { + return ch - 'A' + 10; + } + return -1; +} + +static bool convert_string_to_bytes(const char *str, uint8_t *bytes, + size_t bytes_len) { + if (strlen(str) != bytes_len * 2) { + return false; + } + for (size_t i = 0; i < bytes_len; ++i) { + int byte_h = convert_char_to_digit(str[2 * i]); + int byte_l = convert_char_to_digit(str[2 * i + 1]); + if (byte_h < 0 || byte_l < 0) { + return false; + } + bytes[i] = (((uint8_t)byte_h) << 4) + (uint8_t)byte_l; + } + return true; +} + +static esp_err_t fetch_fabric_rcac_pem(const char *endpoint_url, + const char *access_token, + const char *group_id, char *rcac_pem, + size_t rcac_pem_buf_size) { + ESP_RETURN_ON_FALSE(endpoint_url, ESP_ERR_INVALID_ARG, TAG, + "endpoint_url cannot be NULL"); + ESP_RETURN_ON_FALSE(access_token, ESP_ERR_INVALID_ARG, TAG, + "access_token cannot be NULL"); + ESP_RETURN_ON_FALSE(group_id, ESP_ERR_INVALID_ARG, TAG, + "group_id cannot be NULL"); + ESP_RETURN_ON_FALSE(rcac_pem, ESP_ERR_INVALID_ARG, TAG, + "rcac_pem cannot be NULL"); + + esp_err_t ret = ESP_OK; + char url[256]; + snprintf(url, sizeof(url), + "%s/%s/" + "%s?group_id=%s&node_list=false&sub_groups=false&node_details=false&" + "is_matter=true&" + "fabric_details=true", + endpoint_url, HTTP_API_VERSION, "user/node_group", group_id); + esp_http_client_config_t config = { + .url = url, + .transport_type = HTTP_TRANSPORT_OVER_SSL, + .buffer_size = 1024, + .buffer_size_tx = 2048, + .skip_cert_common_name_check = false, + .crt_bundle_attach = esp_crt_bundle_attach, + }; + char *http_payload = NULL; + const size_t http_payload_size = 1536; + char *rcac_pem_formatted = NULL; + const size_t rcac_pem_formatted_size = 1024; + int group_size = 0, rcac_pem_len = 0; + int http_len, http_status_code; + jparse_ctx_t jctx; + bool rcac_fetched = false; + + // Initialize http client + 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, "Content-Type", "application/json"), + cleanup, TAG, "Failed to set http header Content-Type"); + ESP_GOTO_ON_ERROR( + esp_http_client_set_header(client, "Authorization", access_token), + cleanup, TAG, "Failed to set http header Authorization"); + + // HTTP GET + ESP_GOTO_ON_ERROR(esp_http_client_set_method(client, HTTP_METHOD_GET), + cleanup, TAG, "Failed to set http method"); + ESP_GOTO_ON_ERROR(esp_http_client_open(client, 0), cleanup, TAG, + "Failed to open http connection"); + + // Read response + http_payload = (char *)MEM_CALLOC_EXTRAM(http_payload_size, sizeof(char)); + ESP_GOTO_ON_FALSE(http_payload, ESP_ERR_NO_MEM, close, TAG, + "Failed to alloc memory for http_payload"); + rcac_pem_formatted = + (char *)MEM_CALLOC_EXTRAM(rcac_pem_formatted_size, sizeof(char)); + ESP_GOTO_ON_FALSE(rcac_pem_formatted, ESP_ERR_NO_MEM, close, TAG, + "Failed to alloc memory for rcac_pem_formatted"); + http_len = esp_http_client_fetch_headers(client); + http_status_code = esp_http_client_get_status_code(client); + if ((http_len > 0) && (http_status_code == 200)) { + http_len = esp_http_client_read_response(client, http_payload, + http_payload_size - 1); + http_payload[http_len] = 0; + } else { + http_len = esp_http_client_read_response(client, http_payload, + http_payload_size - 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 : "None"); + ret = http_status_code == 401 ? ESP_ERR_INVALID_STATE : ESP_FAIL; + goto close; + } + + // Parse the response payload + ESP_LOGD(TAG, "http response:%s", http_payload); + ESP_GOTO_ON_FALSE( + json_parse_start(&jctx, http_payload, http_len) == 0, ESP_FAIL, close, + TAG, "Failed to parse the http response json on json_parse_start"); + if (json_obj_get_array(&jctx, "groups", &group_size) == 0 && + group_size == 1) { + if (json_arr_get_object(&jctx, 0) == 0) { + if (json_obj_get_object(&jctx, "fabric_details") == 0) { + if (json_obj_get_strlen(&jctx, "root_ca", &rcac_pem_len) == 0 && + rcac_pem_len < rcac_pem_formatted_size) { + if (json_obj_get_string(&jctx, "root_ca", rcac_pem_formatted, + rcac_pem_formatted_size) == 0) { + deformat_cert(rcac_pem_formatted, rcac_pem, rcac_pem_buf_size); + rcac_fetched = true; + } + } + json_obj_leave_object(&jctx); + } + json_arr_leave_object(&jctx); + } + json_obj_leave_array(&jctx); + } + json_parse_end(&jctx); + ret = rcac_fetched ? ESP_OK : ESP_FAIL; +close: + esp_http_client_close(client); +cleanup: + esp_http_client_cleanup(client); + if (http_payload) { + free(http_payload); + } + if (rcac_pem_formatted) { + free(rcac_pem_formatted); + } + return ret; +} + +static esp_err_t fetch_device_noc(const char *endpoint_url, + const char *access_token, csr_type_t csr_type, + const char *csr_pem_formatted, + uint64_t *matter_node_id, + const char *rainmaker_group_id, char *noc_pem, + size_t noc_pem_buf_size) { + ESP_RETURN_ON_FALSE(endpoint_url, ESP_ERR_INVALID_ARG, TAG, + "endpoint_url cannot be NULL"); + ESP_RETURN_ON_FALSE(access_token, ESP_ERR_INVALID_ARG, TAG, + "access_token cannot be NULL"); + ESP_RETURN_ON_FALSE(csr_pem_formatted, ESP_ERR_INVALID_ARG, TAG, + "csr_pem_formatted cannot be NULL"); + ESP_RETURN_ON_FALSE(rainmaker_group_id, ESP_ERR_INVALID_ARG, TAG, + "rainmaker_group_id cannot be NULL"); + ESP_RETURN_ON_FALSE(noc_pem, ESP_ERR_INVALID_ARG, TAG, + "noc_pem cannot be NULL"); + ESP_RETURN_ON_FALSE(matter_node_id, ESP_ERR_INVALID_ARG, TAG, + "matter_node_id cannot be NULL"); + + esp_err_t ret = ESP_OK; + char url[256]; + snprintf(url, sizeof(url), "%s/%s/%s", endpoint_url, HTTP_API_VERSION, + "user/node_group"); + esp_http_client_config_t config = { + .url = url, + .transport_type = HTTP_TRANSPORT_OVER_SSL, + .buffer_size = 2048, + .buffer_size_tx = 2048, + .skip_cert_common_name_check = false, + .crt_bundle_attach = esp_crt_bundle_attach, + }; + char matter_node_id_str[17] = {0}; + snprintf(matter_node_id_str, sizeof(matter_node_id_str), "%016llX", + *matter_node_id); + char noc_user_id[17] = {0}; + json_gen_str_t jstr; + jparse_ctx_t jctx; + int http_len, http_status_code, cert_count, noc_pem_formatted_len; + char *http_payload = NULL; + const size_t http_payload_size = 2048; + size_t http_payload_len = 0; + char *noc_pem_formatted = NULL; + const size_t noc_pem_formatted_size = 1024; + + 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), + cleanup, TAG, "Failed to set http header Authorization"); + ESP_GOTO_ON_ERROR( + esp_http_client_set_header(client, "Content-Type", "application/json"), + cleanup, TAG, "Failed to set http header Content-Type"); + ESP_GOTO_ON_ERROR(esp_http_client_set_method(client, HTTP_METHOD_PUT), + cleanup, TAG, "Failed to set http method"); + + http_payload = (char *)MEM_CALLOC_EXTRAM(http_payload_size, sizeof(char)); + ESP_GOTO_ON_FALSE(http_payload, ESP_ERR_NO_MEM, cleanup, TAG, + "Failed to alloc memory for http_payload"); + noc_pem_formatted = + (char *)MEM_CALLOC_EXTRAM(noc_pem_formatted_size, sizeof(char)); + ESP_GOTO_ON_FALSE(noc_pem_formatted, ESP_ERR_NO_MEM, cleanup, TAG, + "Failed to alloc memory for noc_pem_formatted"); + + // Generate the JSON payload + json_gen_str_start(&jstr, http_payload, http_payload_size - 1, NULL, NULL); + json_gen_start_object(&jstr); + json_gen_obj_set_string(&jstr, "operation", "add"); + json_gen_push_array(&jstr, "csr_requests"); + json_gen_start_object(&jstr); + if (csr_type == CSR_TYPE_CONTROLLER) { + json_gen_obj_set_string(&jstr, "role", "secondary"); + json_gen_obj_set_string(&jstr, "matter_node_id", matter_node_id_str); + } + json_gen_obj_set_string(&jstr, "group_id", (char *)rainmaker_group_id); + json_gen_obj_set_string(&jstr, "csr", (char *)csr_pem_formatted); + json_gen_end_object(&jstr); + json_gen_pop_array(&jstr); + if (csr_type == CSR_TYPE_CONTROLLER) { + json_gen_obj_set_string(&jstr, "csr_type", "controller"); + } else if (csr_type == CSR_TYPE_USER) { + json_gen_obj_set_string(&jstr, "csr_type", "user"); + } + json_gen_end_object(&jstr); + json_gen_str_end(&jstr); + ESP_LOGD(TAG, "http write payload: %s", http_payload); + + // Send POST data + http_payload_len = strnlen(http_payload, http_payload_size - 1); + ESP_GOTO_ON_ERROR(esp_http_client_open(client, http_payload_len), cleanup, + TAG, "Failed to open http connection"); + http_len = esp_http_client_write(client, http_payload, http_payload_len); + ESP_GOTO_ON_FALSE(http_len == http_payload_len, ESP_FAIL, close, TAG, + "Failed to write Payload. Returned len = %d.", http_len); + + // Read response + http_len = esp_http_client_fetch_headers(client); + http_status_code = esp_http_client_get_status_code(client); + if ((http_len > 0) && (http_status_code == 200)) { + http_len = esp_http_client_read_response(client, http_payload, + http_payload_size - 1); + http_payload[http_len] = 0; + } else { + http_len = esp_http_client_read_response(client, http_payload, + http_payload_size - 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 : "None"); + ret = http_status_code == 401 ? ESP_ERR_INVALID_STATE : ESP_FAIL; + goto close; + } + + // Parse http response + ESP_LOGD(TAG, "http_response %s", http_payload); + ESP_GOTO_ON_FALSE( + json_parse_start(&jctx, http_payload, http_len) == 0, ESP_FAIL, close, + TAG, "Failed to parse the http response json on json_parse_start"); + if (json_obj_get_array(&jctx, "certificates", &cert_count) == 0 && + cert_count == 1) { + if (json_arr_get_object(&jctx, 0) == 0) { + if (json_obj_get_strlen(&jctx, "user_noc", &noc_pem_formatted_len) == 0 && + json_obj_get_string(&jctx, "user_noc", noc_pem_formatted, + noc_pem_formatted_size) == 0) { + noc_pem_formatted[noc_pem_formatted_len] = 0; + if (csr_type == CSR_TYPE_USER) { + if (json_obj_get_string(&jctx, "matter_user_id", noc_user_id, 17) == + 0) { + ESP_LOGI(TAG, "New NOC user id : 0x%s", noc_user_id); + } + *matter_node_id = strtoull(noc_user_id, NULL, 16); + } + } + json_arr_leave_object(&jctx); + } + json_obj_leave_array(&jctx); + } + json_parse_end(&jctx); + + // De-format the noc_pem + ESP_GOTO_ON_FALSE(noc_pem_formatted_len > 0, ESP_FAIL, close, TAG, + "Failed to get formatted NOC from http response"); + ESP_GOTO_ON_FALSE( + deformat_cert(noc_pem_formatted, noc_pem, noc_pem_buf_size) > 0, ESP_FAIL, + close, TAG, "Failed to de-formatted NOC"); +close: + esp_http_client_close(client); +cleanup: + esp_http_client_cleanup(client); + if (http_payload) { + free(http_payload); + } + if (noc_pem_formatted) { + free(noc_pem_formatted); + } + return ret; +} + +esp_err_t fetch_fabric_ipk(const char *endpoint_url, const char *access_token, + const char *group_id, uint8_t *ipk_buf, + size_t ipk_buf_size) { + ESP_RETURN_ON_FALSE(endpoint_url, ESP_ERR_INVALID_ARG, TAG, + "endpoint_url cannot be NULL"); + ESP_RETURN_ON_FALSE(access_token, ESP_ERR_INVALID_ARG, TAG, + "access_token cannot be NULL"); + ESP_RETURN_ON_FALSE(group_id, ESP_ERR_INVALID_ARG, TAG, + "group_id cannot be NULL"); + ESP_RETURN_ON_FALSE(ipk_buf && ipk_buf_size >= IPK_BYTES_LEN, + ESP_ERR_INVALID_ARG, TAG, "ipk_buf is not enough"); + + esp_err_t ret = ESP_OK; + char url[256]; + snprintf(url, sizeof(url), + "%s/%s/" + "%s?group_id=%s&node_list=false&sub_groups=false&node_details=false&" + "is_matter=true&" + "fabric_details=true", + endpoint_url, HTTP_API_VERSION, "user/node_group", group_id); + esp_http_client_config_t config = { + .url = url, + .transport_type = HTTP_TRANSPORT_OVER_SSL, + .buffer_size = 1024, + .buffer_size_tx = 2048, + .skip_cert_common_name_check = false, + .crt_bundle_attach = esp_crt_bundle_attach, + }; + char *http_payload = NULL; + const size_t http_payload_size = 1536; + char ipk_str[2 * IPK_BYTES_LEN + 1] = {0}; + int group_size = 0, ipk_str_len = 0; + int http_len, http_status_code; + jparse_ctx_t jctx; + bool ipk_fetched = false; + + // Initialize http client + 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, "Content-Type", "application/json"), + cleanup, TAG, "Failed to set http header Content-Type"); + ESP_GOTO_ON_ERROR( + esp_http_client_set_header(client, "Authorization", access_token), + cleanup, TAG, "Failed to set http header Authorization"); + + // HTTP GET + ESP_GOTO_ON_ERROR(esp_http_client_set_method(client, HTTP_METHOD_GET), + cleanup, TAG, "Failed to set http method"); + ESP_GOTO_ON_ERROR(esp_http_client_open(client, 0), cleanup, TAG, + "Failed to open http connection"); + + // Read response + http_payload = (char *)MEM_CALLOC_EXTRAM(http_payload_size, sizeof(char)); + ESP_GOTO_ON_FALSE(http_payload, ESP_ERR_NO_MEM, close, TAG, + "Failed to alloc memory for http_payload"); + http_len = esp_http_client_fetch_headers(client); + http_status_code = esp_http_client_get_status_code(client); + if ((http_len > 0) && (http_status_code == 200)) { + http_len = esp_http_client_read_response(client, http_payload, + http_payload_size - 1); + http_payload[http_len] = 0; + } else { + http_len = esp_http_client_read_response(client, http_payload, + http_payload_size - 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 : "None"); + ret = http_status_code == 401 ? ESP_ERR_INVALID_STATE : ESP_FAIL; + goto close; + } + + // Parse the response payload + ESP_LOGD(TAG, "http response:%s", http_payload); + ESP_GOTO_ON_FALSE( + json_parse_start(&jctx, http_payload, http_len) == 0, ESP_FAIL, close, + TAG, "Failed to parse the http response json on json_parse_start"); + if (json_obj_get_array(&jctx, "groups", &group_size) == 0 && + group_size == 1) { + if (json_arr_get_object(&jctx, 0) == 0) { + if (json_obj_get_object(&jctx, "fabric_details") == 0) { + if (json_obj_get_strlen(&jctx, "ipk", &ipk_str_len) == 0 && + ipk_str_len == IPK_BYTES_LEN * 2 && + json_obj_get_string(&jctx, "ipk", ipk_str, sizeof(ipk_str)) == 0) { + if (convert_string_to_bytes(ipk_str, ipk_buf, IPK_BYTES_LEN)) { + ipk_fetched = true; + } + } + json_obj_leave_object(&jctx); + } + json_arr_leave_object(&jctx); + } + json_obj_leave_array(&jctx); + } + json_parse_end(&jctx); + ret = ipk_fetched ? ESP_OK : ESP_FAIL; +close: + esp_http_client_close(client); +cleanup: + esp_http_client_cleanup(client); + if (http_payload) { + free(http_payload); + } + return ret; +} + +esp_err_t fetch_fabric_rcac_der(const char *endpoint_url, + const char *access_token, + const char *rainmaker_group_id, + unsigned char *rcac_der, size_t *rcac_der_len) { + esp_err_t ret = ESP_OK; + char *rcac_pem = NULL; + const size_t rcac_pem_size = 1024; + rcac_pem = (char *)MEM_CALLOC_EXTRAM(rcac_pem_size, sizeof(char)); + ESP_GOTO_ON_FALSE(rcac_pem, ESP_ERR_NO_MEM, exit, TAG, + "Failed to allocate memory for rcac_pem"); + ESP_GOTO_ON_ERROR(fetch_fabric_rcac_pem(endpoint_url, access_token, + rainmaker_group_id, rcac_pem, + rcac_pem_size), + exit, TAG, "Failed to fetch RCAC pem"); + ESP_GOTO_ON_ERROR(convert_pem_to_der(rcac_pem, rcac_der, rcac_der_len), exit, + TAG, "Failed to convert rcac_pem to rcac_der"); +exit: + if (rcac_pem) { + free(rcac_pem); + } + return ret; +} + +esp_err_t issue_noc_with_csr(const char *endpoint_url, const char *access_token, + csr_type_t csr_type, const uint8_t *csr_der, + const size_t csr_der_len, + const char *rainmaker_group_id, + uint64_t *matter_node_id, unsigned char *noc_der, + size_t *noc_der_len) { + esp_err_t ret = ESP_OK; + char *csr_pem = NULL; + char *csr_pem_formatted = NULL; + char *noc_pem = NULL; + const size_t pem_buffer_size = 1024; + + csr_pem = (char *)MEM_CALLOC_EXTRAM(pem_buffer_size, sizeof(char)); + ESP_GOTO_ON_FALSE(csr_pem, ESP_ERR_NO_MEM, exit, TAG, + "Failed to allocate memory for csr_pem"); + ESP_GOTO_ON_ERROR( + convert_der_to_pem(csr_der, csr_der_len, csr_pem, pem_buffer_size), exit, + TAG, "Failed to convert csr_der to csr_pem"); + csr_pem_formatted = (char *)MEM_CALLOC_EXTRAM(pem_buffer_size, sizeof(char)); + ESP_GOTO_ON_FALSE(csr_pem_formatted, ESP_ERR_NO_MEM, exit, TAG, + "Failed to allocate memory for csr_pem_formatted"); + ESP_GOTO_ON_FALSE(format_csr(csr_pem, csr_pem_formatted, pem_buffer_size) > 0, + ESP_FAIL, exit, TAG, "Failed to format CSR"); + noc_pem = (char *)MEM_CALLOC_EXTRAM(pem_buffer_size, sizeof(char)); + ; + ESP_GOTO_ON_FALSE(noc_pem, ESP_ERR_NO_MEM, exit, TAG, + "Failed to allocate memory for noc_pem"); + ESP_GOTO_ON_ERROR(fetch_device_noc(endpoint_url, access_token, csr_type, + csr_pem_formatted, matter_node_id, + rainmaker_group_id, noc_pem, + pem_buffer_size), + exit, TAG, "Failed to fetch user noc"); + ESP_GOTO_ON_ERROR(convert_pem_to_der(noc_pem, noc_der, noc_der_len), exit, + TAG, "Failed to convert noc_pem to noc_der"); +exit: + if (csr_pem) { + free(csr_pem); + } + if (csr_pem_formatted) { + free(csr_pem_formatted); + } + if (noc_pem) { + free(noc_pem); + } + return ret; +} + +const char *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 NULL; + } + while (*str) { + if (strncmp(str, sub_str, strlen(sub_str)) == 0) { + return str; + } + str++; + } + return NULL; +} + +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 NULL; +} + +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 NULL; + } + jparse_ctx_t jctx; + matter_device_t *device_element = NULL; + 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 NULL; + } + } + 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 NULL; + } + 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) { + const size_t http_response_size = 1024; + char *http_response = + (char *)MEM_CALLOC_EXTRAM(http_response_size, sizeof(char)); + ESP_RETURN_ON_FALSE(http_response, ESP_ERR_NO_MEM, TAG, + "Failed to allocate buffer for http response"); + int buffer_len_to_read = http_response_size - 1; + char *buffer_ptr_to_read = http_response; + 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, "\"node_details\":[{"); + if (!node_details_ptr && is_complete_response_received) { + ESP_LOGE(TAG, "Cannot find node_details from the http response"); + free(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 + 15 + : http_response_size - 40; + buffer_ptr_to_read = consume_buffer(http_response, http_response_size - 1, + cusumed_buffer_size); + } else if (status == READ_NODE_STRUCTURE_ELEMENTS) { + if (http_response[0] == '[' || http_response[0] == ',') { + const char *element_start = http_response + 1; + if (*element_start != '{') { + free(http_response); + return ESP_FAIL; + } + const char *element_end = get_element_end(element_start); + if (!element_end) { + free(http_response); + return ESP_FAIL; + } + matter_device_t *device_entry = + parse_matter_device_element(element_start, element_end); + if (device_entry) { + device_entry->is_metadata_fetched = false; + device_entry->next = *device_list; + *device_list = device_entry; + } + // Consume the parsed node element + buffer_ptr_to_read = + consume_buffer(http_response, http_response_size - 1, + element_end + 1 - http_response); + } else if (http_response[0] == ']') { + // Node elements array ends with ']' + status = READ_END; + } else { + free(http_response); + return ESP_FAIL; + } + } + // Try to fill all the buffer in the next response read. + buffer_len_to_read = + http_response + http_response_size - buffer_ptr_to_read - 1; + } while (status != READ_END); + free(http_response); + return ESP_OK; +} + +static esp_err_t fetch_matter_node_list(const char *endpoint_url, + const char *access_token, + const char *rainmaker_group_id, + matter_device_t **matter_dev_list) { + ESP_RETURN_ON_FALSE(endpoint_url, ESP_ERR_INVALID_ARG, TAG, + "endpoint_url cannot be NULL"); + ESP_RETURN_ON_FALSE(access_token, ESP_ERR_INVALID_ARG, TAG, + "access_token cannot be NULL"); + ESP_RETURN_ON_FALSE(rainmaker_group_id, ESP_ERR_INVALID_ARG, TAG, + "rainmaker_group_id cannot be NULL"); + ESP_RETURN_ON_FALSE(matter_dev_list && *matter_dev_list == NULL, + ESP_ERR_INVALID_ARG, TAG, + "matter_dev_list cannot be NULL and *matter_dev_list " + "should be an empty list"); + + esp_err_t ret = ESP_OK; + char url[200]; + int http_len, http_status_code; + char *http_payload = NULL; + const size_t http_payload_size = 512; + matter_device_t *new_device_list = NULL; + + snprintf(url, sizeof(url), "%s/%s/%s=%s&%s", endpoint_url, HTTP_API_VERSION, + "user/node_group?group_id", rainmaker_group_id, + "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), + cleanup, TAG, "Failed to set http header Authorization"); + // HTTP GET Method + ESP_GOTO_ON_ERROR(esp_http_client_set_method(client, HTTP_METHOD_GET), + cleanup, TAG, "Failed to set http method"); + ESP_GOTO_ON_ERROR(esp_http_client_open(client, 0), cleanup, TAG, + "Failed to open http connection"); + + // Read Response + http_len = esp_http_client_fetch_headers(client); + http_status_code = esp_http_client_get_status_code(client); + http_payload = (char *)MEM_CALLOC_EXTRAM(http_payload_size, sizeof(char)); + ESP_GOTO_ON_FALSE(http_payload, ESP_ERR_NO_MEM, close, TAG, + "Failed to allocate memory for http_payload"); + if ((http_len == 0) || (http_status_code != 200)) { + http_len = esp_http_client_read_response(client, http_payload, + http_payload_size - 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 : "None"); + ret = http_status_code == 401 ? ESP_ERR_INVALID_STATE : ESP_FAIL; + goto close; + } + + // Read the node list from the http response + if (read_node_list(client, &new_device_list) == ESP_OK) { + *matter_dev_list = new_device_list; + } else { + free_matter_device_list(new_device_list); + } +close: + esp_http_client_close(client); +cleanup: + esp_http_client_cleanup(client); + if (http_payload) { + free(http_payload); + } + return ret; +} + +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_ERR_NOT_FOUND; +} + +static esp_err_t get_node_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 = 32; + if (json_obj_get_strlen(jctx, "deviceName", &device_name_len) == 0 && + device_name_len < ESP_MATTER_DEVICE_NAME_MAX_LEN) { + json_obj_get_string(jctx, "deviceName", dev->endpoints[0].device_name, + ESP_MATTER_DEVICE_NAME_MAX_LEN); + } + json_obj_get_bool(jctx, "isRainmaker", &(dev->is_rainmaker_device)); + json_obj_leave_object(jctx); + } + return ESP_OK; +} + +static esp_err_t fetch_matter_node_metadata(const char *endpoint_url, + const char *access_token, + matter_device_t *matter_dev) { + ESP_RETURN_ON_FALSE(endpoint_url, ESP_ERR_INVALID_ARG, TAG, + "endpoint_url cannot be NULL"); + ESP_RETURN_ON_FALSE(access_token, ESP_ERR_INVALID_ARG, TAG, + "access_token cannot be NULL"); + ESP_RETURN_ON_FALSE( + matter_dev && strnlen(matter_dev->rainmaker_node_id, + sizeof(matter_dev->rainmaker_node_id)) > 0, + ESP_ERR_INVALID_ARG, TAG, + "matter_dev cannot be NULL and it should has the rainmaker_node_id info"); + + esp_err_t ret = ESP_OK; + char url[200]; + snprintf(url, sizeof(url), "%s/%s/%s?node_id=%s&%s", endpoint_url, + HTTP_API_VERSION, "user/nodes", matter_dev->rainmaker_node_id, + "node_details=true&is_matter=true"); + int http_len, http_status_code; + int node_count; + char id_str[40] = {0}; + int id_str_len = 0; + jparse_ctx_t jctx; + char *http_payload = NULL; + const size_t http_payload_size = 4096; + + 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 = 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), + cleanup, TAG, "Failed to set http header Authorization"); + // HTTP GET Method + ESP_GOTO_ON_ERROR(esp_http_client_set_method(client, HTTP_METHOD_GET), + cleanup, TAG, "Failed to set http method"); + ESP_GOTO_ON_ERROR(esp_http_client_open(client, 0), cleanup, TAG, + "Failed to open http connection"); + http_payload = (char *)MEM_CALLOC_EXTRAM(http_payload_size, sizeof(char)); + ESP_GOTO_ON_FALSE(http_payload, ESP_ERR_NO_MEM, cleanup, TAG, + "Failed to allocate memory for http_payload"); + + // Read Response + http_len = esp_http_client_fetch_headers(client); + http_status_code = esp_http_client_get_status_code(client); + if ((http_len > 0) && (http_status_code == 200)) { + http_len = esp_http_client_read_response(client, http_payload, + http_payload_size - 1); + http_payload[http_len] = 0; + } else { + http_len = esp_http_client_read_response(client, http_payload, + http_payload_size - 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 : "None"); + ret = http_status_code == 401 ? ESP_ERR_INVALID_STATE : ESP_FAIL; + goto close; + } + ESP_LOGD(TAG, "http response payload: %s", http_payload); + + // Parse the http response + ESP_GOTO_ON_FALSE( + json_parse_start(&jctx, http_payload, http_len) == 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 && + node_count == 1) { + if (json_arr_get_object(&jctx, 0) == 0) { + if (json_obj_get_strlen(&jctx, "id", &id_str_len) == 0 && + id_str_len < sizeof(id_str) && + json_obj_get_string(&jctx, "id", id_str, sizeof(id_str)) == 0) { + id_str[id_str_len] = 0; + if (strncmp(matter_dev->rainmaker_node_id, id_str, id_str_len) != 0) { + ESP_LOGE(TAG, "rainmaker_node_id does not match"); + ret = ESP_FAIL; + } else { + get_node_reachable(&jctx, &(matter_dev->reachable)); + get_node_metadata(&jctx, matter_dev); + matter_dev->is_metadata_fetched = true; + } + } + json_arr_leave_object(&jctx); + } + json_obj_leave_array(&jctx); + } + json_parse_end(&jctx); +close: + esp_http_client_close(client); +cleanup: + esp_http_client_cleanup(client); + if (http_payload) { + free(http_payload); + } + return ret; +} + +esp_err_t fetch_matter_device_list(const char *endpoint_url, + const char *access_token, + const char *rainmaker_group_id, + matter_device_t **matter_dev_list) { + esp_err_t err = ESP_OK; + ESP_RETURN_ON_ERROR(fetch_matter_node_list(endpoint_url, access_token, + rainmaker_group_id, + matter_dev_list), + TAG, "Failed to fetch matter node list"); + if (*matter_dev_list) { + matter_device_t *dev = *matter_dev_list; + while (dev) { + if ((err = fetch_matter_node_metadata(endpoint_url, access_token, dev)) != + ESP_OK) { + ESP_LOGE(TAG, "Failed to fetch metadata for Matter Node 0x%llX", + dev->node_id); + free_matter_device_list(*matter_dev_list); + *matter_dev_list = NULL; + return err; + } + dev = dev->next; + } + } + return ESP_OK; +} diff --git a/examples/matter/common/controller_rest_apis/include/controller_rest_apis.h b/examples/matter/common/controller_rest_apis/include/controller_rest_apis.h new file mode 100644 index 0000000..787a53d --- /dev/null +++ b/examples/matter/common/controller_rest_apis/include/controller_rest_apis.h @@ -0,0 +1,141 @@ +// 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 + +#ifdef __cplusplus +extern "C" +{ +#endif + +typedef enum { + CSR_TYPE_USER, + CSR_TYPE_CONTROLLER, +} csr_type_t; + +/** + * Fetch access_token with refresh token + * + * @param[in] endpoint_url The base endpoint URL + * @param[in] refresh_token The refresh token used to extend an existing session + * @param[out] access_token The access token to be passed in the "Authorization" HTTP header as the authentication token + * @param[in] access_token_buf_len The access_token buffer size + * + * @return ESP_OK on sussess + * @return error in case of failure + */ +esp_err_t fetch_access_token(const char *endpoint_url, const char *refresh_token, char *access_token, + size_t access_token_buf_len); + +/** + * Fetch RainMaker Group ID corresponding to the Matter Fabric ID + * + * @param[in] endpoint_url The base endpoint URL + * @param[in] access_token The access token to be passed in the "Authorization" HTTP header as the authentication token + * @param[in] fabric_id The Matter Fabric ID + * @param[out] group_id The RainMaker Group ID corresponding to the Matter Fabric ID + * @param[in] group_id_buf_len The group_id buffer size + * + * @return ESP_OK on sussess + * @return error in case of failure + */ +esp_err_t fetch_rainmaker_group_id(const char *endpoint_url, const char *access_token, const uint64_t fabric_id, + char *group_id, size_t group_id_buf_len); + +/** + * Fetch Matter Fabric ID corresponding to the RainMaker Group ID + * + * @param[in] endpoint_url The base endpoint URL + * @param[in] access_token The access token to be passed in the "Authorization" HTTP header as the authentication token + * @param[in] group_id The RainMaker ID + * @param[out] fabric_id The Matter Fabric ID corresponding to the RainMaker Group ID + * + * @return ESP_OK on sussess + * @return error in case of failure + */ +esp_err_t fetch_matter_fabric_id(const char *endpoint_url, const char *access_token, const char *group_id, + uint64_t *fabric_id); + +/** + * Fetch the Root CA certificate + * + * @param[in] endpoint_url The base endpoint URL + * @param[in] access_token The access token to be passed in the "Authorization" HTTP header as the authentication token + * @param[in] rainmaker_group_id The Rainmaker Group ID for the Matter Fabric of the new NOC + * @param[out] rcac_der The fetched RCAC file in DER format + * @param[in,out] rcac_der_len The length of rcac_der buffer as input and the length of issued RCAC file in DER format as output + * + * @return ESP_OK on sussess + * @return error in case of failure + */ +esp_err_t fetch_fabric_rcac_der(const char *endpoint_url, const char *access_token, const char *rainmaker_group_id, + unsigned char *rcac_der, size_t *rcac_der_len); + +/** + * Fetch the IPK of Matter Fabric + * + * @param[in] endpoint_url The base endpoint URL + * @param[in] access_token The access token to be passed in the "Authorization" HTTP header as the authentication token + * @param[in] rainmaker_group_id The Rainmaker Group ID for the Matter Fabric of the new NOC + * @param[out] ipk_buf The IPK of the Matter Fabric + * @param[in] ipk_buf_size The size of ipk_buf + * + * @return ESP_OK on sussess + * @return error in case of failure + */ +esp_err_t fetch_fabric_ipk(const char *endpoint_url, const char *access_token, const char *rainmaker_group_id, + uint8_t *ipk_buf, size_t ipk_buf_size); + +/** + * Issue Matter NOC with CSR + * + * @param[in] endpoint_url The base endpoint URL + * @param[in] access_token The access token to be passed in the "Authorization" HTTP header as the authentication token + * @param[in] csr_type The CSR type of this NOC request + * @param[in] csr_der The CSR bytes in DER format to issue the user NOC + * @param[in] csr_der_len The length of the CSR bytes in DER format + * @param[in] rainmaker_group_id The Rainmaker Group ID for the Matter Fabric of the new NOC + * @param[in,out] matter_node_id The Matter Node ID in the subject DN of the new user NOC + * @param[out] noc_der The issued NOC file in DER format + * @param[in,out] noc_der_len The length of noc_der buffer as input and the length of issued NOC file in DER format as output + * + * @return ESP_OK on sussess + * @return error in case of failure + */ +esp_err_t issue_noc_with_csr(const char *endpoint_url, const char *access_token, csr_type_t csr_type, + const uint8_t *csr_der, const size_t csr_der_len, const char *rainmaker_group_id, + uint64_t *matter_node_id, unsigned char *noc_der, size_t *noc_der_len); + +/** + * Fetch Matter device list in Matter Fabric(RainMaker Group) + * + * Note:this function will allocate memory for the newly fetched device list + * + * @param[in] endpoint_url The base endpoint URL + * @param[in] access_token The access token to be passed in the "Authorization" HTTP header as the authentication token + * @param[in] rainmaker_group_id The Rainmaker Group ID + * @param[out] matter_dev_list The Matter device list to be fetched + * + * @return ESP_OK on sussess + * @return error in case of failure + */ +esp_err_t fetch_matter_device_list(const char *endpoint_url, const char *access_token, const char *rainmaker_group_id, + matter_device_t **matter_dev_list); + +#ifdef __cplusplus +} // extern "C" +#endif diff --git a/examples/matter/common/controller_rest_apis/include/matter_device.h b/examples/matter/common/controller_rest_apis/include/matter_device.h new file mode 100644 index 0000000..4b686b3 --- /dev/null +++ b/examples/matter/common/controller_rest_apis/include/matter_device.h @@ -0,0 +1,61 @@ +// 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 + +#ifdef __cplusplus +extern "C" +{ +#endif + +#define ESP_MATTER_DEVICE_MAX_ENDPOINT 8 +#define ESP_MATTER_DEVICE_NAME_MAX_LEN 32 + +typedef struct endpoint_entry { + uint16_t endpoint_id; + uint32_t device_type_id; + char device_name[ESP_MATTER_DEVICE_NAME_MAX_LEN]; +} endpoint_entry_t; + +typedef struct matter_device { + uint64_t node_id; + char rainmaker_node_id[24]; + bool is_metadata_fetched; + 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; + +/** + * Free the allocated memory for matter device entries list + * + * @param[in] dev_list The device list to free + */ +void free_matter_device_list(matter_device_t *dev_list); + +/** + * Print the informations for matter device entries list + * + * @param[in] dev_list The device list to be printed + */ +void print_matter_device_list(matter_device_t *dev_list); + +#ifdef __cplusplus +} // extern "C" +#endif diff --git a/examples/matter/common/controller_rest_apis/matter_device.c b/examples/matter/common/controller_rest_apis/matter_device.c new file mode 100644 index 0000000..be15e99 --- /dev/null +++ b/examples/matter/common/controller_rest_apis/matter_device.c @@ -0,0 +1,54 @@ +// 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 + +#define TAG "MATTER_DEVICE" + +void free_matter_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_matter_device_list(matter_device_t *dev_list) +{ + uint16_t dev_index = 0; + while (dev_list) { + ESP_LOGI(TAG, "device %d : {", dev_index); + ESP_LOGI(TAG, " rainmaker_node_id: %s,", dev_list->rainmaker_node_id); + ESP_LOGI(TAG, " matter_node_id: 0x%llX,", dev_list->node_id); + if (dev_list->is_metadata_fetched) { + ESP_LOGI(TAG, " is_rainmaker_device: %s,", dev_list->is_rainmaker_device ? "true" : "false"); + ESP_LOGI(TAG, " is_online: %s,", dev_list->reachable ? "true" : "false"); + ESP_LOGI(TAG, " endpoints : ["); + for (size_t i = 0; i < dev_list->endpoint_count; ++i) { + ESP_LOGI(TAG, " {"); + ESP_LOGI(TAG, " endpoint_id: %d,", dev_list->endpoints[i].endpoint_id); + ESP_LOGI(TAG, " device_type_id: 0x%lx,", dev_list->endpoints[i].device_type_id); + ESP_LOGI(TAG, " device_name: %s,", dev_list->endpoints[i].device_name); + ESP_LOGI(TAG, " },"); + } + ESP_LOGI(TAG, " ]"); + } + ESP_LOGI(TAG, "}"); + dev_list = dev_list->next; + dev_index++; + } +} diff --git a/examples/matter/matter_controller/CMakeLists.txt b/examples/matter/matter_controller/CMakeLists.txt index 112acf9..ae673d2 100644 --- a/examples/matter/matter_controller/CMakeLists.txt +++ b/examples/matter/matter_controller/CMakeLists.txt @@ -41,6 +41,7 @@ set(EXTRA_COMPONENT_DIRS "${ESP_MATTER_PATH}/device_hal/device" "${ESP_MATTER_PATH}/examples/common" "${RMAKER_PATH}/examples/common/app_insights" + "${RMAKER_PATH}/examples/matter/common" "${RMAKER_PATH}/components/" ${extra_components_dirs_append}) diff --git a/examples/matter/matter_controller/sdkconfig.defaults b/examples/matter/matter_controller/sdkconfig.defaults index fb2b01d..88a8283 100644 --- a/examples/matter/matter_controller/sdkconfig.defaults +++ b/examples/matter/matter_controller/sdkconfig.defaults @@ -59,9 +59,9 @@ CONFIG_ESP_SECURE_CERT_DS_PERIPHERAL=n CONFIG_MBEDTLS_HKDF_C=y # Enable controller -CONFIG_ENABLE_CHIP_CONTROLLER_BUILD=y CONFIG_ESP_MATTER_CONTROLLER_ENABLE=y CONFIG_ESP_MATTER_COMMISSIONER_ENABLE=n +CONFIG_CONTROLLER_CUSTOM_CLUSTER_ENABLE=y # Increase stack size CONFIG_ESP_SYSTEM_EVENT_TASK_STACK_SIZE=4096 diff --git a/examples/matter/matter_controller_with_touchscreen/CMakeLists.txt b/examples/matter/matter_controller_with_touchscreen/CMakeLists.txt index 3a235c5..a34efe7 100644 --- a/examples/matter/matter_controller_with_touchscreen/CMakeLists.txt +++ b/examples/matter/matter_controller_with_touchscreen/CMakeLists.txt @@ -36,6 +36,7 @@ set(EXTRA_COMPONENT_DIRS "${ESP_MATTER_PATH}/device_hal/device" "${ESP_MATTER_PATH}/examples/common" "${RMAKER_PATH}/examples/common/app_insights" + "${RMAKER_PATH}/examples/matter/common" "${RMAKER_PATH}/components" ${extra_components_dirs_append}) diff --git a/examples/matter/matter_controller_with_touchscreen/main/linker.lf b/examples/matter/matter_controller_with_touchscreen/main/linker.lf index 7bd8f95..dfc775b 100644 --- a/examples/matter/matter_controller_with_touchscreen/main/linker.lf +++ b/examples/matter/matter_controller_with_touchscreen/main/linker.lf @@ -14,18 +14,10 @@ entries: else: * (default) -[mapping:wpa_supplicant] -archive: libwpa_supplicant.a -entries: - if ESP_ALLOW_BSS_SEG_EXTERNAL_MEMORY = y: - * (extram_bss) - else: - * (default) - [mapping:esp_matter] archive: libesp_matter.a entries: if ESP_ALLOW_BSS_SEG_EXTERNAL_MEMORY = y: * (extram_bss) else: - * (default) \ No newline at end of file + * (default) diff --git a/examples/matter/matter_controller_with_touchscreen/sdkconfig.defaults b/examples/matter/matter_controller_with_touchscreen/sdkconfig.defaults index 811abc6..c2e18d2 100644 --- a/examples/matter/matter_controller_with_touchscreen/sdkconfig.defaults +++ b/examples/matter/matter_controller_with_touchscreen/sdkconfig.defaults @@ -64,9 +64,9 @@ CONFIG_ESP_MATTER_MEM_ALLOC_MODE_EXTERNAL=y CONFIG_ESP_SECURE_CERT_DS_PERIPHERAL=n # Enable controller -CONFIG_ENABLE_CHIP_CONTROLLER_BUILD=y CONFIG_ESP_MATTER_CONTROLLER_ENABLE=y CONFIG_ESP_MATTER_COMMISSIONER_ENABLE=n +CONFIG_CONTROLLER_CUSTOM_CLUSTER_ENABLE=y # Increase stack size CONFIG_ESP_SYSTEM_EVENT_TASK_STACK_SIZE=4096