diff --git a/examples/.build-rules.yml b/examples/.build-rules.yml index 540b8e323..dd8464352 100644 --- a/examples/.build-rules.yml +++ b/examples/.build-rules.yml @@ -134,3 +134,9 @@ examples/sensors: - if: IDF_TARGET in ["esp32c3"] temporary: true reason: the other targets are not tested yet + +examples/bridge_apps/esp_rainmaker_bridge: + enable: + - if: IDF_TARGET in ["esp32s3"] + temporary: true + reason: the other targets are not tested yet diff --git a/examples/bridge_apps/esp_rainmaker_bridge/CMakeLists.txt b/examples/bridge_apps/esp_rainmaker_bridge/CMakeLists.txt new file mode 100644 index 000000000..983083da6 --- /dev/null +++ b/examples/bridge_apps/esp_rainmaker_bridge/CMakeLists.txt @@ -0,0 +1,33 @@ +# The following lines of boilerplate have to be in your project's +# CMakeLists in this exact order for cmake to work correctly +cmake_minimum_required(VERSION 3.5) + +if(NOT DEFINED ENV{ESP_MATTER_PATH}) + message(FATAL_ERROR "Please set ESP_MATTER_PATH to the path of esp-matter repo") +endif(NOT DEFINED ENV{ESP_MATTER_PATH}) + +set(PROJECT_VER "1.0") +set(PROJECT_VER_NUMBER 1) + +set(ESP_MATTER_PATH $ENV{ESP_MATTER_PATH}) +set(MATTER_SDK_PATH ${ESP_MATTER_PATH}/connectedhomeip/connectedhomeip) + +# This should be done before using the IDF_TARGET variable. +include($ENV{IDF_PATH}/tools/cmake/project.cmake) + +set(EXTRA_COMPONENT_DIRS + "${ESP_MATTER_PATH}/examples/common" + "${MATTER_SDK_PATH}/config/esp32/components" + "${ESP_MATTER_PATH}/components" + ${extra_components_dirs_append} + $ENV{IDF_PATH}/examples/common_components/protocol_examples_common + "./example_components/rainmaker_api" + "./example_components/app_network") + +project(rainmaker_bridge) + +idf_build_set_property(CXX_COMPILE_OPTIONS "-std=gnu++17;-Os;-DCHIP_HAVE_CONFIG_H;-fpermissive;-Wno-overloaded-virtual" APPEND) +idf_build_set_property(C_COMPILE_OPTIONS "-Os" APPEND) +# For RISCV chips, project_include.cmake sets -Wno-format, but does not clear various +# flags that depend on -Wformat +idf_build_set_property(COMPILE_OPTIONS "-Wno-format-nonliteral;-Wno-format-security;-Wno-error=cpp" APPEND) diff --git a/examples/bridge_apps/esp_rainmaker_bridge/README.md b/examples/bridge_apps/esp_rainmaker_bridge/README.md new file mode 100644 index 000000000..ef7c24a06 --- /dev/null +++ b/examples/bridge_apps/esp_rainmaker_bridge/README.md @@ -0,0 +1,115 @@ +# Rainmaker Bridge + +This example demonstrates a Matter-Rainmaker Bridge that bridges Rainmaker devices to Matter fabric. + +The Matter Bridge device is running on ESP32-S3. + +See the [docs](https://docs.espressif.com/projects/esp-matter/en/latest/esp32/developing.html) for more information about building and flashing the firmware. + +💡 Important: `create_bridge_devices` callback can be used to add data model elements (e.g., attributes, commands, etc.) to the bridge endpoint. + +## 1. Additional Environment Setup + +### 1.1 Hardware connection + +This example run on ESP32S3 devkit by default. + +### 1.2 Build and flash the Bridge (ESP32-S3) + +For Standalone DevKit boards: + +``` +cd ${ESP_MATTER_PATH}/examples/bridge_apps/esp_rainmaker_bridge +idf.py set-target esp32s3 +idf.py -p build flash +``` + +## 2. Commissioning Setup + +### 2.1 Use Rainmaker App pairing Bridge + +Use Rainmaker App scan the qrcode print in device log to pairing the bridge + +``` +I (16104) NimBLE: GAP procedure initiated: advertise; +I (16104) NimBLE: disc_mode=2 +I (16114) NimBLE: adv_channel_map=0 own_addr_type=0 adv_filter_policy=0 adv_itvl_min=256 adv_itvl_max=256 +I (16124) NimBLE: + +I (16104) network_prov_mgr: Provisioning started with service name : PROV_54a900 +I (16134) app_wifi: Provisioning started +I (16134) app_network: Scan this QR code from the ESP RainMaker phone app for Provisioning. +I (16144) QRCODE: Encoding below text with ECC LVL 0 & QR Code Version 5 +I (16144) QRCODE: {"ver":"v1","name":"PROV_54a900","pop":"47d4fb71","transport":"ble"} + + █▀▀▀▀▀█ â–ˆ â–„â–„â–ˆ ▀▀ â–„â–ˆ ▀▄ █▀▀▀▀▀█ + â–ˆ ███ â–ˆ ██▄ █▄ ▄▀▀▄ ▀▀▄██ â–ˆ ███ â–ˆ + â–ˆ ▀▀▀ â–ˆ ▄██ ▀▀█▀█▄▀▄ ▀▀▀▀ â–ˆ ▀▀▀ â–ˆ + ▀▀▀▀▀▀▀ â–€ █▄▀ â–€ ▀▄▀▄█▄▀▄█ ▀▀▀▀▀▀▀ + ██ ▄█▀▀▄▄ â–€ â–€ ▀█▄▀▄▀▄▀▄▄▄▄ â–ˆ ▀█▀▀ + ▄▀█▀█▀▀ █▀ â–„â–€ ██▄ ▄██▀██▀▄██▀█ + ▄█▀█▄▀▀▀▄ ▄▀▄▄█▄█▀▀▀▄▀▄▀ ▄▀▄▀ â–„â–„â–€ + ▀█ ▀▄▀▄█▀██▀▀ ▀▄▄█▄ ██▀▄█ â–€ █▀▀▄ + ▀█▀▄█▀▀██▀ ▀▄▄▄█▀▀▀█▀ █▀▄▀▀ â–€ + ▄█▄▀▀▀▀ ▄██ â–„â–€ ▀▀█▄▄ ▄█▀█ ▄█▄█▀▀▄ + ███ █▀▀█▀ ▀▄ ▄█▄▀█ â–„â–ˆ ▀▀▀▀ â–€ ▄█▀ + █▀▀ ▀▄▄█▀ ▀█▄▄ â–„ █▄▀ █▀▀▀▀▄▄ + ▀▀▀▀ ▀▀▀█ â–ˆ ▀▄▄▄▄██▀█▀ ▄█▀▀▀██▄▀ + █▀▀▀▀▀█ ▄▀▀ â–„ █▀██▀ ▄█▀█ â–€ ██▀▀ + â–ˆ ███ â–ˆ ▀▀▄▀█▄█▄█▀█▀ ▀█ ███▀█▀▄▄█ + â–ˆ ▀▀▀ â–ˆ ▄▄███▀ █▀▄ ▄███▀ ▀█▀▄ â–€ + ▀▀▀▀▀▀▀ â–€ ▀▀ ▀▀ ▀▀▀▀▀▀ â–€ + + +I (16344) app_network: If QR code is not visible, copy paste the below URL in a browser. +https://rainmaker.espressif.com/qrcode.html?data={"ver":"v1","name":"PROV_54a900","pop":"47d4fb71","transport":"ble"} +I (16364) app_network: Provisioning will auto stop after 30 minute(s). + +``` + +### 2.2 Use Rainmaker App pairing rainmaker end device + +Follow this [guide](https://github.com/espressif/esp-rainmaker/blob/master/README.md) to setup Rainmaker device + +### 2.3 Use chip-tool pairing bridge through onnetwork method + +Use below command to pairing bridge + +``` +./chip-tool pairing onnetwork 0x1234 20202021 +``` + +### 2.4 Control the bulb with chip-tool + +Now you can control the Rainmaker device using the chip tool. + +``` +./chip-tool onoff toggle 0x1234 0x2 +``` + +## 3. Device Performance + +### 3.1 Memory usage + +The following is the Memory and Flash Usage. + +- `Bootup` == Device just finished booting up. Device is not + commissioned or connected to wifi yet. +- `After Commissioning` == Device is connected to wifi and is also + commissioned and is rebooted. +- `After Adding a Bridged device` == A Rainmaker Color Light is added + on the Bridge. +- device used: ESP32-S3-DevKitC-1 +- tested on: + [d0faa92c](https://github.com/espressif/esp-matter/commit/d0faa92c9336205de21a4b325c956893736c4d64) + (2025-09-29) +- IDF: v5.4.1 [4c2820d3](https://github.com/espressif/esp-idf/tree/v5.4.1) + +| | Bootup | After Rainmaker Commissioning | After Matter Commissioning | After Adding a Bridged device | +|:- |:-: |:-: |:-: |:-: | +|**Free Internal Memory** |162KB |118KB |118KB |106KB | + +**Flash Usage**: Firmware binary size: 1.65MB + +This should give you a good idea about the amount of free memory that is +available for you to run your application's code. diff --git a/examples/bridge_apps/esp_rainmaker_bridge/example_components/app_network/CMakeLists.txt b/examples/bridge_apps/esp_rainmaker_bridge/example_components/app_network/CMakeLists.txt new file mode 100644 index 000000000..544613f56 --- /dev/null +++ b/examples/bridge_apps/esp_rainmaker_bridge/example_components/app_network/CMakeLists.txt @@ -0,0 +1,10 @@ +set(priv_req qrcode nvs_flash esp_event rmaker_common vfs network_provisioning openthread) + +idf_component_register(SRCS "app_wifi_internal.c" "app_network.c" + INCLUDE_DIRS "." + PRIV_INCLUDE_DIRS "private_include" + PRIV_REQUIRES ${priv_req}) + +if(CONFIG_APP_WIFI_SHOW_DEMO_INTRO_TEXT) + target_compile_definitions(${COMPONENT_TARGET} PRIVATE "-D RMAKER_DEMO_PROJECT_NAME=\"${CMAKE_PROJECT_NAME}\"") +endif() diff --git a/examples/bridge_apps/esp_rainmaker_bridge/example_components/app_network/Kconfig.projbuild b/examples/bridge_apps/esp_rainmaker_bridge/example_components/app_network/Kconfig.projbuild new file mode 100644 index 000000000..e0c71a153 --- /dev/null +++ b/examples/bridge_apps/esp_rainmaker_bridge/example_components/app_network/Kconfig.projbuild @@ -0,0 +1,84 @@ +menu "ESP RainMaker App Wi-Fi Provisioning" + + config APP_NETWORK_PROV_SHOW_QR + bool "Show provisioning QR code" + default y + help + Show the QR code for provisioning. + + config APP_NETWORK_PROV_MAX_POP_MISMATCH + int + default 5 + range 0 20 + prompt "Max wrong pop attempts allowed" + help + Set the maximum wrong pop attempts allowed before stopping provisioning. + Set 0 for the feature to be disabled. + This safeguards the device from brute-force attempt by limiting the wrong pop allowed. + + choice APP_NETWORK_PROV_TRANSPORT + bool "Provisioning Transport method" + default APP_NETWORK_PROV_TRANSPORT_BLE + help + Wi-Fi/Network provisioning component offers both, SoftAP and BLE transports. Choose any one. + + config APP_NETWORK_PROV_TRANSPORT_SOFTAP + bool "Soft AP" + depends on !IDF_TARGET_ESP32H2 + config APP_NETWORK_PROV_TRANSPORT_BLE + bool "BLE" + select BT_ENABLED + depends on !IDF_TARGET_ESP32S2 + endchoice + + config APP_NETWORK_PROV_TRANSPORT + int + default 1 if APP_NETWORK_PROV_TRANSPORT_SOFTAP + default 2 if APP_NETWORK_PROV_TRANSPORT_BLE + + config APP_NETWORK_RESET_PROV_ON_FAILURE + bool + default y + prompt "Reset provisioned credentials and state machine after session failure" + help + Enable reseting provisioned credentials and state machine after session failure. + This will restart the provisioning service after retries are exhausted. + + config APP_NETWORK_PROV_MAX_RETRY_CNT + int + default 5 + prompt "Max retries before reseting provisioning state machine" + depends on APP_NETWORK_RESET_PROV_ON_FAILURE + help + Set the Maximum retry to avoid reconnecting to an inexistent network or if credentials + are misconfigured. Provisioned credentials are erased and internal state machine + is reset after this threshold is reached. + + config APP_NETWORK_SHOW_DEMO_INTRO_TEXT + bool "Show intro text for demos" + default n + help + Show some intro text for demos in order to help users understand more about ESP RainMaker. + + config APP_NETWORK_PROV_TIMEOUT_PERIOD + int "Provisioning Timeout" + default 30 + help + Timeout (in minutes) after which the provisioning will auto stop. A reboot will be required + to restart provisioning. It is always recommended to set this to some non zero value, especially + if you are not using PoP. Set to 0 if you do not want provisioning to auto stop. + + config APP_NETWORK_PROV_NAME_PREFIX + string "Provisioning Name Prefix" + default "PROV" + help + Provisioning Name Prefix. + + config APP_WIFI_PROV_COMPAT + bool "Stay compatible with App Wi-Fi component" + depends on ESP_RMAKER_NETWORK_OVER_WIFI + default y + help + Stay compatible with Previous App Wi-Fi component + +endmenu diff --git a/examples/bridge_apps/esp_rainmaker_bridge/example_components/app_network/app_network.c b/examples/bridge_apps/esp_rainmaker_bridge/example_components/app_network/app_network.c new file mode 100644 index 000000000..eba1e71df --- /dev/null +++ b/examples/bridge_apps/esp_rainmaker_bridge/example_components/app_network/app_network.c @@ -0,0 +1,435 @@ +/* + This example code is in the Public Domain (or CC0 licensed, at your option.) + + Unless required by applicable law or agreed to in writing, this + software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + CONDITIONS OF ANY KIND, either express or implied. +*/ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef CONFIG_ESP_RMAKER_NETWORK_OVER_WIFI +#include +#include +#endif /* CONFIG_ESP_RMAKER_NETWORK_OVER_WIFI */ + +#include + +#include +#ifdef CONFIG_APP_NETWORK_PROV_TRANSPORT_BLE +#include +#else /* CONFIG_APP_NETWORK_PROV_TRANSPORT_SOFTAP */ +#include +#endif /* CONFIG_APP_NETWORK_PROV_TRANSPORT_BLE */ + +#ifdef CONFIG_APP_NETWORK_PROV_SHOW_QR +#include +#endif + +#include +#include +#include +#include + +ESP_EVENT_DEFINE_BASE(APP_NETWORK_EVENT); +static const char *TAG = "app_network"; +static const int NETWORK_CONNECTED_EVENT = BIT0; +static EventGroupHandle_t network_event_group; + +#define PROV_QR_VERSION "v1" + +#define PROV_TRANSPORT_SOFTAP "softap" +#define PROV_TRANSPORT_BLE "ble" +#define QRCODE_BASE_URL "https://rainmaker.espressif.com/qrcode.html" + +#define CREDENTIALS_NAMESPACE "rmaker_creds" +#define RANDOM_NVS_KEY "random" + +#define POP_STR_SIZE 9 +static esp_timer_handle_t prov_stop_timer; +/* Timeout period in minutes */ +#define APP_NETWORK_PROV_TIMEOUT_PERIOD CONFIG_APP_NETWORK_PROV_TIMEOUT_PERIOD +/* Autofetch period in micro-seconds */ +static uint64_t prov_timeout_period = (APP_NETWORK_PROV_TIMEOUT_PERIOD * 60 * 1000000LL); + +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 1, 3) +#define APP_PROV_STOP_ON_CREDS_MISMATCH +#elif (CONFIG_APP_NETWORK_PROV_MAX_RETRY_CNT > 0) +#warning "Provisioning window stop on max credentials failures, needs IDF version >= 5.1.3" +#endif + +#ifdef CONFIG_APP_NETWORK_SHOW_DEMO_INTRO_TEXT + +#define ESP_RAINMAKER_GITHUB_EXAMPLES_PATH "https://github.com/espressif/esp-rainmaker/blob/master/examples" +#define ESP_RAINMAKER_INTRO_LINK "https://rainmaker.espressif.com" +#define ESP_RMAKER_PHONE_APP_LINK "http://bit.ly/esp-rmaker" +char esp_rainmaker_ascii_art[] = \ +" ______ _____ _____ _____ _____ _ _ __ __ _ ________ _____\n"\ +" | ____|/ ____| __ \\ | __ \\ /\\ |_ _| \\ | | \\/ | /\\ | |/ / ____| __ \\\n"\ +" | |__ | (___ | |__) | | |__) | / \\ | | | \\| | \\ / | / \\ | ' /| |__ | |__) |\n"\ +" | __| \\___ \\| ___/ | _ / / /\\ \\ | | | . ` | |\\/| | / /\\ \\ | < | __| | _ /\n"\ +" | |____ ____) | | | | \\ \\ / ____ \\ _| |_| |\\ | | | |/ ____ \\| . \\| |____| | \\ \\\n"\ +" |______|_____/|_| |_| \\_\\/_/ \\_\\_____|_| \\_|_| |_/_/ \\_\\_|\\_\\______|_| \\_\\\n"; + +static void intro_print(bool provisioned) +{ + printf("####################################################################################################\n"); + printf("%s\n", esp_rainmaker_ascii_art); + printf("Welcome to ESP RainMaker %s demo application!\n", RMAKER_DEMO_PROJECT_NAME); + if (!provisioned) { + printf("Follow these steps to get started:\n"); + printf("1. Download the ESP RainMaker phone app by visiting this link from your phone's browser:\n\n"); + printf(" %s\n\n", ESP_RMAKER_PHONE_APP_LINK); + printf("2. Sign up and follow the steps on screen to add the device to your Wi-Fi/Thread network.\n"); + printf("3. You are now ready to use the device and control it locally as well as remotely.\n"); + printf(" You can also use the Boot button on the board to control your device.\n"); + } + printf("\nIf you want to reset network credentials, or reset to factory, press and hold the Boot button.\n"); + printf("\nThis application uses ESP RainMaker, which is based on ESP IDF.\n"); + printf("Check out the source code for this application here:\n %s/%s\n", + ESP_RAINMAKER_GITHUB_EXAMPLES_PATH, RMAKER_DEMO_PROJECT_NAME); + printf("\nPlease visit %s for additional information.\n\n", ESP_RAINMAKER_INTRO_LINK); + printf("####################################################################################################\n"); +} + +#else + +static void intro_print(bool provisioned) +{ + /* Do nothing */ +} + +#endif /* !APP_NETWORK_SHOW_DEMO_INTRO_TEXT */ + +#ifdef CONFIG_APP_NETWORK_PROV_SHOW_QR +static esp_err_t qrcode_display(const char *text) +{ +#define MAX_QRCODE_VERSION 5 + esp_qrcode_config_t cfg = ESP_QRCODE_CONFIG_DEFAULT(); + cfg.max_qrcode_version = MAX_QRCODE_VERSION; + return esp_qrcode_generate(&cfg, text); +} +#endif + +static uint8_t *custom_mfg_data = NULL; +static size_t custom_mfg_data_len = 0; + +esp_err_t app_network_set_custom_mfg_data(uint16_t device_type, uint8_t device_subtype) +{ + int8_t mfg_data[] = {MFG_DATA_HEADER, MGF_DATA_APP_ID, MFG_DATA_VERSION, MFG_DATA_CUSTOMER_ID}; + size_t mfg_data_len = sizeof(mfg_data) + 4; // 4 bytes of device type, subtype, and extra-code + custom_mfg_data = (uint8_t *)MEM_ALLOC_EXTRAM(mfg_data_len); + if (custom_mfg_data == NULL) { + ESP_LOGE(TAG, "Failed to allocate memory to custom mfg data"); + return ESP_ERR_NO_MEM; + } + memcpy(custom_mfg_data, mfg_data, sizeof(mfg_data)); + custom_mfg_data[8] = 0xff & (device_type >> 8); + custom_mfg_data[9] = 0xff & device_type; + custom_mfg_data[10] = device_subtype; + custom_mfg_data[11] = 0; + custom_mfg_data_len = mfg_data_len; + ESP_LOG_BUFFER_HEXDUMP("tag", custom_mfg_data, mfg_data_len, 3); + return ESP_OK; +} + +static void app_network_print_qr(const char *name, const char *pop, const char *transport) +{ + if (!name || !transport) { + ESP_LOGW(TAG, "Cannot generate QR code payload. Data missing."); + return; + } + char payload[150]; + if (pop) { + snprintf(payload, sizeof(payload), "{\"ver\":\"%s\",\"name\":\"%s\"" \ + ",\"pop\":\"%s\",\"transport\":\"%s\"}", + PROV_QR_VERSION, name, pop, transport); + } else { + snprintf(payload, sizeof(payload), "{\"ver\":\"%s\",\"name\":\"%s\"" \ + ",\"transport\":\"%s\"}", + PROV_QR_VERSION, name, transport); + } +#ifdef CONFIG_APP_NETWORK_PROV_SHOW_QR + ESP_LOGI(TAG, "Scan this QR code from the ESP RainMaker phone app for Provisioning."); + qrcode_display(payload); +#endif /* CONFIG_APP_NETWORK_PROV_SHOW_QR */ + ESP_LOGI(TAG, "If QR code is not visible, copy paste the below URL in a browser.\n%s?data=%s", QRCODE_BASE_URL, payload); + esp_event_post(APP_NETWORK_EVENT, APP_NETWORK_EVENT_QR_DISPLAY, payload, strlen(payload) + 1, portMAX_DELAY); +} + +/* Free random_bytes after use only if function returns ESP_OK */ +static esp_err_t read_random_bytes_from_nvs(uint8_t **random_bytes, size_t *len) +{ + nvs_handle handle; + esp_err_t err; + *len = 0; + + if ((err = nvs_open_from_partition(CONFIG_ESP_RMAKER_FACTORY_PARTITION_NAME, CREDENTIALS_NAMESPACE, + NVS_READONLY, &handle)) != ESP_OK) { + ESP_LOGD(TAG, "NVS open for %s %s %s failed with error %d", CONFIG_ESP_RMAKER_FACTORY_PARTITION_NAME, CREDENTIALS_NAMESPACE, RANDOM_NVS_KEY, err); + return ESP_FAIL; + } + + if ((err = nvs_get_blob(handle, RANDOM_NVS_KEY, NULL, len)) != ESP_OK) { + ESP_LOGD(TAG, "Error %d. Failed to read key %s.", err, RANDOM_NVS_KEY); + nvs_close(handle); + return ESP_ERR_NOT_FOUND; + } + + *random_bytes = calloc(*len, 1); + if (*random_bytes) { + nvs_get_blob(handle, RANDOM_NVS_KEY, *random_bytes, len); + nvs_close(handle); + return ESP_OK; + } + nvs_close(handle); + return ESP_ERR_NO_MEM; +} + +static char *custom_pop; +esp_err_t app_network_set_custom_pop(const char *pop) +{ + /* NULL PoP is not allowed here. Use POP_TYPE_NONE instead. */ + if (!pop) { + return ESP_ERR_INVALID_ARG; + } + + /* Freeing up the PoP in case it is already allocated */ + if (custom_pop) { + free(custom_pop); + custom_pop = NULL; + } + + custom_pop = strdup(pop); + if (!custom_pop) { + return ESP_ERR_NO_MEM; + } + return ESP_OK; +} + +static esp_err_t get_device_service_name(char *service_name, size_t max) +{ + uint8_t *nvs_random = NULL; + const char *ssid_prefix = CONFIG_APP_NETWORK_PROV_NAME_PREFIX; + size_t nvs_random_size = 0; + if ((read_random_bytes_from_nvs(&nvs_random, &nvs_random_size) != ESP_OK) || nvs_random_size < 3) { + uint8_t mac_addr[6]; + esp_read_mac(mac_addr, ESP_MAC_BASE); + snprintf(service_name, max, "%s_%02x%02x%02x", ssid_prefix, mac_addr[3], mac_addr[4], mac_addr[5]); + } else { + snprintf(service_name, max, "%s_%02x%02x%02x", ssid_prefix, nvs_random[nvs_random_size - 3], + nvs_random[nvs_random_size - 2], nvs_random[nvs_random_size - 1]); + } + if (nvs_random) { + free(nvs_random); + } + return ESP_OK; +} + + +static char *get_device_pop(app_network_pop_type_t pop_type) +{ + if (pop_type == POP_TYPE_NONE) { + return NULL; + } else if (pop_type == POP_TYPE_CUSTOM) { + if (!custom_pop) { + ESP_LOGE(TAG, "Custom PoP not set. Please use app_wifi_set_custom_pop()."); + return NULL; + } + return strdup(custom_pop); + } + char *pop = calloc(1, POP_STR_SIZE); + if (!pop) { + ESP_LOGE(TAG, "Failed to allocate memory for PoP."); + return NULL; + } + + if (pop_type == POP_TYPE_MAC) { + uint8_t mac_addr[6]; + esp_err_t err = esp_read_mac(mac_addr, ESP_MAC_BASE); + if (err == ESP_OK) { + snprintf(pop, POP_STR_SIZE, "%02x%02x%02x%02x", mac_addr[2], mac_addr[3], mac_addr[4], mac_addr[5]); + return pop; + } else { + ESP_LOGE(TAG, "Failed to get MAC address to generate PoP."); + goto pop_err; + } + } else if (pop_type == POP_TYPE_RANDOM) { + uint8_t *nvs_random = NULL; + size_t nvs_random_size = 0; + if ((read_random_bytes_from_nvs(&nvs_random, &nvs_random_size) != ESP_OK) || nvs_random_size < 4) { + ESP_LOGE(TAG, "Failed to read random bytes from NVS to generate PoP."); + if (nvs_random) { + free(nvs_random); + } + goto pop_err; + } else { + snprintf(pop, POP_STR_SIZE, "%02x%02x%02x%02x", nvs_random[0], nvs_random[1], nvs_random[2], nvs_random[3]); + free(nvs_random); + return pop; + } + } +pop_err: + free(pop); + return NULL; +} + +static void network_event_handler(void* arg, esp_event_base_t event_base, int32_t event_id, void* event_data) +{ + +#ifdef APP_PROV_STOP_ON_CREDS_MISMATCH + static int failed_cnt = 0; +#endif +#ifdef APP_PROV_STOP_ON_CREDS_MISMATCH + if (event_base == PROTOCOMM_SECURITY_SESSION_EVENT) { + switch (event_id) { + case PROTOCOMM_SECURITY_SESSION_SETUP_OK: + ESP_LOGI(TAG, "Secured session established!"); + break; + case PROTOCOMM_SECURITY_SESSION_INVALID_SECURITY_PARAMS: + /* fall-through */ + case PROTOCOMM_SECURITY_SESSION_CREDENTIALS_MISMATCH: + ESP_LOGE(TAG, "Received incorrect PoP or invalid security params! event: %d", (int) event_id); + if (CONFIG_APP_NETWORK_PROV_MAX_POP_MISMATCH && + (++failed_cnt >= CONFIG_APP_NETWORK_PROV_MAX_POP_MISMATCH)) { + /* stop provisioning for security reasons */ + network_prov_mgr_stop_provisioning(); + ESP_LOGW(TAG, "Max PoP attempts reached! Provisioning disabled for security reasons. Please reboot device to restart provisioning"); + esp_event_post(APP_NETWORK_EVENT, APP_NETWORK_EVENT_PROV_CRED_MISMATCH, NULL, 0, portMAX_DELAY); + } + break; + default: + break; + } + } +#endif /* APP_PROV_STOP_ON_CREDS_MISMATCH */ +#ifdef CONFIG_ESP_RMAKER_NETWORK_OVER_WIFI + if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) { + ip_event_got_ip_t* event = (ip_event_got_ip_t*) event_data; + ESP_LOGI(TAG, "Connected with IP Address:" IPSTR, IP2STR(&event->ip_info.ip)); + /* Signal main application to continue execution */ + xEventGroupSetBits(network_event_group, NETWORK_CONNECTED_EVENT); + } +#endif /* CONFIG_ESP_RMAKER_NETWORK_OVER_WIFI */ + if (event_base == NETWORK_PROV_EVENT && event_id == NETWORK_PROV_END) { + if (prov_stop_timer) { + esp_timer_stop(prov_stop_timer); + esp_timer_delete(prov_stop_timer); + prov_stop_timer = NULL; + } + network_prov_mgr_deinit(); + } +} + +void app_network_init() +{ + /* Initialize the event loop, if not done already. */ + esp_err_t err = esp_event_loop_create_default(); + /* If the default event loop is already initialized, we get ESP_ERR_INVALID_STATE */ + if (err != ESP_OK) { + if (err == ESP_ERR_INVALID_STATE) { + ESP_LOGW(TAG, "Event loop creation failed with ESP_ERR_INVALID_STATE. Proceeding since it must have been created elsewhere."); + } else { + ESP_LOGE(TAG, "Failed to create default event loop, err = %x", err); + return; + } + } +#ifdef CONFIG_ESP_RMAKER_NETWORK_OVER_WIFI + ESP_ERROR_CHECK(wifi_init()); +#endif + network_event_group = xEventGroupCreate(); +#ifdef APP_PROV_STOP_ON_CREDS_MISMATCH + ESP_ERROR_CHECK(esp_event_handler_register(PROTOCOMM_SECURITY_SESSION_EVENT, ESP_EVENT_ANY_ID, &network_event_handler, NULL)); +#endif +#ifdef CONFIG_ESP_RMAKER_NETWORK_OVER_WIFI + ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT, IP_EVENT_STA_GOT_IP, &network_event_handler, NULL)); +#endif + ESP_ERROR_CHECK(esp_event_handler_register(NETWORK_PROV_EVENT, NETWORK_PROV_END, &network_event_handler, NULL)); +} + +static void app_network_prov_stop(void *priv) +{ + ESP_LOGW(TAG, "Provisioning timed out. Please reboot device to restart provisioning."); + network_prov_mgr_stop_provisioning(); + esp_event_post(APP_NETWORK_EVENT, APP_NETWORK_EVENT_PROV_TIMEOUT, NULL, 0, portMAX_DELAY); +} + +esp_err_t app_network_start_timer(void) +{ + if (prov_timeout_period == 0) { + return ESP_OK; + } + esp_timer_create_args_t prov_stop_timer_conf = { + .callback = app_network_prov_stop, + .arg = NULL, + .dispatch_method = ESP_TIMER_TASK, + .name = "app_wifi_prov_stop_tm" + }; + if (esp_timer_create(&prov_stop_timer_conf, &prov_stop_timer) == ESP_OK) { + esp_timer_start_once(prov_stop_timer, prov_timeout_period); + ESP_LOGI(TAG, "Provisioning will auto stop after %d minute(s).", + APP_NETWORK_PROV_TIMEOUT_PERIOD); + return ESP_OK; + } else { + ESP_LOGE(TAG, "Failed to create Provisioning auto stop timer."); + } + return ESP_FAIL; +} + +esp_err_t app_network_start(app_network_pop_type_t pop_type) +{ + /* Do we want a proof-of-possession (ignored if Security 0 is selected): + * - this should be a string with length > 0 + * - NULL if not used + */ + char *pop = get_device_pop(pop_type); + if ((pop_type != POP_TYPE_NONE) && (pop == NULL)) { + return ESP_ERR_NO_MEM; + } + /* What is the Device Service Name that we want + * This translates to : + * - device name when scheme is network_prov_scheme_ble/wifi_prov_scheme_ble + */ + char service_name[12]; + get_device_service_name(service_name, sizeof(service_name)); + /* What is the service key (Wi-Fi password) + * NULL = Open network + * This is ignored when scheme is network_prov_scheme_ble/wifi_prov_scheme_ble + */ + const char *service_key = NULL; + esp_err_t err = ESP_OK; + bool provisioned = false; +#ifdef CONFIG_ESP_RMAKER_NETWORK_OVER_WIFI + err = wifi_start(pop, service_name, service_key, custom_mfg_data, custom_mfg_data_len, &provisioned); +#endif + if (err != ESP_OK) { + free(pop); + return err; + } + if (!provisioned) { +#ifdef CONFIG_APP_NETWORK_PROV_TRANSPORT_BLE + app_network_print_qr(service_name, pop, PROV_TRANSPORT_BLE); +#else /* CONFIG_APP_NETWORK_PROV_TRANSPORT_SOFTAP */ + app_network_print_qr(service_name, pop, PROV_TRANSPORT_SOFTAP); +#endif /* CONFIG_APP_NETWORK_PROV_TRANSPORT_BLE */ + app_network_start_timer(); + } + free(pop); + intro_print(provisioned); + if (custom_mfg_data) { + free(custom_mfg_data); + custom_mfg_data = NULL; + custom_mfg_data_len = 0; + } + /* Wait for Network connection */ + xEventGroupWaitBits(network_event_group, NETWORK_CONNECTED_EVENT, false, true, portMAX_DELAY); + return err; +} diff --git a/examples/bridge_apps/esp_rainmaker_bridge/example_components/app_network/app_network.h b/examples/bridge_apps/esp_rainmaker_bridge/example_components/app_network/app_network.h new file mode 100644 index 000000000..4fffe76e4 --- /dev/null +++ b/examples/bridge_apps/esp_rainmaker_bridge/example_components/app_network/app_network.h @@ -0,0 +1,122 @@ +/* + This example code is in the Public Domain (or CC0 licensed, at your option.) + + Unless required by applicable law or agreed to in writing, this + software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + CONDITIONS OF ANY KIND, either express or implied. +*/ +#pragma once +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define MFG_DATA_HEADER 0xe5, 0x02 +#define MGF_DATA_APP_ID 'N', 'o', 'v' +#define MFG_DATA_VERSION 'a' +#define MFG_DATA_CUSTOMER_ID 0x00, 0x01 + +#define MGF_DATA_DEVICE_TYPE_LIGHT 0x0005 +#define MGF_DATA_DEVICE_TYPE_SWITCH 0x0080 +#define MGF_DATA_DEVICE_TYPE_MATTER_CONTROLLER 0xFFF1 + +#define MFG_DATA_DEVICE_SUBTYPE_SWITCH 0x01 +#define MFG_DATA_DEVICE_SUBTYPE_LIGHT 0x01 +#define MFG_DATA_DEVICE_SUBTYPE_MATTER_CONTROLLER 0x01 + +#define MFG_DATA_DEVICE_EXTRA_CODE 0x00 + +/** ESP RainMaker Event Base */ +ESP_EVENT_DECLARE_BASE(APP_NETWORK_EVENT); + +/** App Network Events */ +typedef enum { + /** QR code available for display. Associated data is the NULL terminated QR payload. */ + APP_NETWORK_EVENT_QR_DISPLAY = 1, + /** Provisioning timed out */ + APP_NETWORK_EVENT_PROV_TIMEOUT, + /** Provisioning has restarted due to failures (Invalid SSID/Passphrase) */ + APP_NETWORK_EVENT_PROV_RESTART, + /** Provisioning closed due to invalid credentials */ + APP_NETWORK_EVENT_PROV_CRED_MISMATCH, +} app_network_event_t; + +/** Types of Proof of Possession */ +typedef enum { + /** Use MAC address to generate PoP */ + POP_TYPE_MAC, + /** Use random stream generated and stored in fctry partition during claiming process as PoP */ + POP_TYPE_RANDOM, + /** Do not use any PoP. + * Use this option with caution. Consider using `CONFIG_APP_NETWORK_PROV_TIMEOUT_PERIOD` with this. + */ + POP_TYPE_NONE, + /** Use a custom PoP. + * Set a custom PoP using app_network_set_custom_pop() first. + */ + POP_TYPE_CUSTOM +} app_network_pop_type_t; + +/** Initialize Wi-Fi/Thread + * + * This initializes Wi-Fi/Thread stack and the network provisioning manager + */ +void app_network_init(); + +/** Start Wi-Fi/Thread + * + * This will start provisioning if the node is not provisioned and will connect to any network + * if node is provisioned. Function will return successfully only after network is connected + * + * @param[in] pop_type The type for Proof of Possession (PoP) pin + * + * @return ESP_OK on success (Network connected). + * @return error in case of failure. + */ +esp_err_t app_network_start(app_network_pop_type_t pop_type); + +/** Set custom manufacturing data + * + * This can be used to add some custom manufacturing data in BLE advertisements during + * provisioning. This can be used by apps to filter the scanned BLE devices and show + * only the relevant one. Supported by Nova Home app for light and switch + * + * @param[in] device_type Type of the device, like light or switch + * @param[in] device_subtype Sub Type of the device (application specific) + * + * @return ESP_OK on success. + * @return error in case of failure. + */ +esp_err_t app_network_set_custom_mfg_data(uint16_t device_type, uint8_t device_subtype); + +/** Set custom PoP + * + * This can be used to set a custom Proof of Possession (PoP) pin for provisioning. + * Applicable only if POP_TYPE_CUSTOM is used for app_network_start(). + * + * @param[in] pop A NULL terminated PoP string (typically 8 characters alphanumeric) + * + * @return ESP_OK on success. + * @return error in case of failure. + */ +esp_err_t app_network_set_custom_pop(const char *pop); + +#if CONFIG_APP_WIFI_PROV_COMPAT +#define APP_WIFI_EVENT APP_NETWORK_EVENT +typedef app_network_event_t app_wifi_event_t; +#define APP_WIFI_EVENT_QR_DISPLAY APP_NETWORK_EVENT_QR_DISPLAY +#define APP_WIFI_EVENT_PROV_TIMEOUT APP_NETWORK_EVENT_PROV_TIMEOUT +#define APP_WIFI_EVENT_PROV_RESTART APP_NETWORK_EVENT_PROV_RESTART +#define APP_WIFI_EVENT_PROV_CRED_MISMATCH APP_NETWORK_EVENT_PROV_CRED_MISMATCH +typedef app_network_pop_type_t app_wifi_pop_type_t; +#define app_wifi_init() app_network_init() +#define app_wifi_start(pop_type) app_network_start(pop_type) +#define app_wifi_set_custom_mfg_data(device_type, device_subtype) app_network_set_custom_mfg_data(device_type, device_subtype) +#define app_wifi_set_custom_pop(pop) app_network_set_custom_pop(pop) +#endif /* !CONFIG_APP_WIFI_PROV_COMPAT */ + +#ifdef __cplusplus +} +#endif diff --git a/examples/bridge_apps/esp_rainmaker_bridge/example_components/app_network/app_wifi.h b/examples/bridge_apps/esp_rainmaker_bridge/example_components/app_network/app_wifi.h new file mode 100644 index 000000000..1bc81092d --- /dev/null +++ b/examples/bridge_apps/esp_rainmaker_bridge/example_components/app_network/app_wifi.h @@ -0,0 +1,32 @@ +/* + This example code is in the Public Domain (or CC0 licensed, at your option.) + + Unless required by applicable law or agreed to in writing, this + software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + CONDITIONS OF ANY KIND, either express or implied. +*/ +#pragma once +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#if CONFIG_APP_WIFI_PROV_COMPAT +#define APP_WIFI_EVENT APP_NETWORK_EVENT +typedef app_network_event_t app_wifi_event_t; +#define APP_WIFI_EVENT_QR_DISPLAY APP_NETWORK_EVENT_QR_DISPLAY +#define APP_WIFI_EVENT_PROV_TIMEOUT APP_NETWORK_EVENT_PROV_TIMEOUT +#define APP_WIFI_EVENT_PROV_RESTART APP_NETWORK_EVENT_PROV_RESTART +#define APP_WIFI_EVENT_PROV_CRED_MISMATCH APP_NETWORK_EVENT_PROV_CRED_MISMATCH +typedef app_network_pop_type_t app_wifi_pop_type_t; +#define app_wifi_init() app_network_init() +#define app_wifi_start(pop_type) app_network_start(pop_type) +#define app_wifi_set_custom_mfg_data(device_type, device_subtype) app_network_set_custom_mfg_data(device_type, device_subtype) +#define app_wifi_set_custom_pop(pop) app_network_set_custom_pop(pop) +#endif /* !CONFIG_APP_WIFI_PROV_COMPAT */ + +#ifdef __cplusplus +} +#endif diff --git a/examples/bridge_apps/esp_rainmaker_bridge/example_components/app_network/app_wifi_internal.c b/examples/bridge_apps/esp_rainmaker_bridge/example_components/app_network/app_wifi_internal.c new file mode 100644 index 000000000..27984122d --- /dev/null +++ b/examples/bridge_apps/esp_rainmaker_bridge/example_components/app_network/app_wifi_internal.c @@ -0,0 +1,209 @@ +/* + This example code is in the Public Domain (or CC0 licensed, at your option.) + + Unless required by applicable law or agreed to in writing, this + software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + CONDITIONS OF ANY KIND, either express or implied. +*/ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#ifdef CONFIG_APP_NETWORK_PROV_TRANSPORT_BLE +#include +#else /* CONFIG_APP_NETWORK_PROV_TRANSPORT_SOFTAP */ +#include +#endif /* CONFIG_APP_NETWORK_PROV_TRANSPORT_BLE */ + +#include +#include + +#define APP_PROV_STOP_ON_CREDS_MISMATCH + +#ifdef CONFIG_ESP_RMAKER_NETWORK_OVER_WIFI +static const char* TAG = "app_wifi"; +/* Event handler for catching system events */ +static void event_handler(void* arg, esp_event_base_t event_base, + int32_t event_id, void* event_data) +{ +#ifdef CONFIG_APP_NETWORK_RESET_PROV_ON_FAILURE + static int retries = 0; +#endif + + if (event_base == NETWORK_PROV_EVENT) { + switch (event_id) { + case NETWORK_PROV_START: + ESP_LOGI(TAG, "Provisioning started"); + break; + case NETWORK_PROV_WIFI_CRED_RECV: { + wifi_sta_config_t *wifi_sta_cfg = (wifi_sta_config_t *)event_data; + ESP_LOGI(TAG, "Received Wi-Fi credentials" + "\n\tSSID : %s\n\tPassword : %s", + (const char *) wifi_sta_cfg->ssid, + (const char *) wifi_sta_cfg->password); + break; + } + case NETWORK_PROV_WIFI_CRED_FAIL: { + network_prov_wifi_sta_fail_reason_t *reason = (network_prov_wifi_sta_fail_reason_t *)event_data; + ESP_LOGE(TAG, "Provisioning failed!\n\tReason : %s" + "\n\tPlease reset to factory and retry provisioning", + (*reason == NETWORK_PROV_WIFI_STA_AUTH_ERROR) ? + "Wi-Fi station authentication failed" : "Wi-Fi access-point not found"); +#ifdef CONFIG_APP_NETWORK_RESET_PROV_ON_FAILURE + retries++; + if (retries >= CONFIG_APP_NETWORK_PROV_MAX_RETRY_CNT) { + ESP_LOGI(TAG, "Failed to connect with provisioned AP, resetting provisioned credentials"); + network_prov_mgr_reset_wifi_sm_state_on_failure(); + esp_event_post(APP_NETWORK_EVENT, APP_NETWORK_EVENT_PROV_RESTART, NULL, 0, portMAX_DELAY); + retries = 0; + } +#endif // CONFIG_APP_NETWORK_RESET_PROV_ON_FAILURE + break; + } + case NETWORK_PROV_WIFI_CRED_SUCCESS: + ESP_LOGI(TAG, "Provisioning successful"); +#ifdef CONFIG_APP_NETWORK_RESET_PROV_ON_FAILURE + retries = 0; +#endif + break; + default: + break; + } + } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) { + esp_wifi_connect(); + } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) { + ESP_LOGI(TAG, "Disconnected. Connecting to the AP again..."); + esp_wifi_connect(); + } +} + +static void wifi_init_sta() +{ + ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA)); + ESP_ERROR_CHECK(esp_wifi_start()); +} +#endif // CONFIG_ESP_RMAKER_NETWORK_OVER_WIFI + +esp_err_t wifi_init(void) +{ +#ifdef CONFIG_ESP_RMAKER_NETWORK_OVER_WIFI + /* Initialize TCP/IP */ + esp_netif_init(); + /* Register our event handler for Wi-Fi, IP and Provisioning related events */ + ESP_ERROR_CHECK(esp_event_handler_register(NETWORK_PROV_EVENT, ESP_EVENT_ANY_ID, &event_handler, NULL)); + ESP_ERROR_CHECK(esp_event_handler_register(WIFI_EVENT, ESP_EVENT_ANY_ID, &event_handler, NULL)); + + /* Initialize Wi-Fi including netif with default config */ + esp_netif_create_default_wifi_sta(); + wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); + ESP_ERROR_CHECK(esp_wifi_init(&cfg)); + return ESP_OK; +#else + return ESP_ERR_NOT_SUPPORTED; +#endif /* CONFIG_ESP_RMAKER_NETWORK_OVER_WIFI */ +} + +esp_err_t wifi_start(const char *pop, const char *service_name, const char *service_key, uint8_t *mfg_data, + size_t mfg_data_len, bool *provisioned) +{ +#ifdef CONFIG_ESP_RMAKER_NETWORK_OVER_WIFI + /* Configuration for the provisioning manager */ + network_prov_mgr_config_t config = { + /* What is the Provisioning Scheme that we want ? + * network_prov_scheme_softap or network_prov_scheme_ble */ +#ifdef CONFIG_APP_NETWORK_PROV_TRANSPORT_BLE + .scheme = network_prov_scheme_ble, +#else /* CONFIG_APP_NETWORK_PROV_TRANSPORT_SOFTAP */ + .scheme = network_prov_scheme_softap, +#endif /* CONFIG_APP_NETWORK_PROV_TRANSPORT_BLE */ + + /* Any default scheme specific event handler that you would + * like to choose. Since our example application requires + * neither BT nor BLE, we can choose to release the associated + * memory once provisioning is complete, or not needed + * (in case when device is already provisioned). Choosing + * appropriate scheme specific event handler allows the manager + * to take care of this automatically. This can be set to + * NETWORK_PROV_EVENT_HANDLER_NONE when using network_prov_scheme_softap*/ +#ifdef CONFIG_APP_NETWORK_PROV_TRANSPORT_BLE + .scheme_event_handler = NETWORK_PROV_SCHEME_BLE_EVENT_HANDLER_FREE_BTDM +#else /* CONFIG_APP_NETWORK_PROV_TRANSPORT_SOFTAP */ + .scheme_event_handler = NETWORK_PROV_EVENT_HANDLER_NONE, +#endif /* CONFIG_APP_NETWORK_PROV_TRANSPORT_BLE */ + }; + + /* Initialize provisioning manager with the + * configuration parameters set above */ + ESP_ERROR_CHECK(network_prov_mgr_init(config)); + /* Let's find out if the device is provisioned */ + network_prov_mgr_is_wifi_provisioned(provisioned); + /* If device is not yet provisioned start provisioning service */ + if (!(*provisioned)) { + ESP_LOGI(TAG, "Starting provisioning"); +#if CONFIG_ESP_WIFI_SOFTAP_SUPPORT + esp_netif_create_default_wifi_ap(); +#endif + /* What is the security level that we want (0 or 1): + * - NETWORK_PROV_SECURITY_0/WIFI_PROV_SECURITY_0 is simply plain text communication. + * - NETWORK_PROV_SECURITY_1/WIFI_PROV_SECURITY_1 is secure communication which consists of secure handshake + * using X25519 key exchange and proof of possession (pop) and AES-CTR + * for encryption/decryption of messages. + */ + network_prov_security_t security = NETWORK_PROV_SECURITY_1; + +#ifdef CONFIG_APP_NETWORK_PROV_TRANSPORT_BLE + /* This step is only useful when scheme is wifi_prov_scheme_ble. This will + * set a custom 128 bit UUID which will be included in the BLE advertisement + * and will correspond to the primary GATT service that provides provisioning + * endpoints as GATT characteristics. Each GATT characteristic will be + * formed using the primary service UUID as base, with different auto assigned + * 12th and 13th bytes (assume counting starts from 0th byte). The client side + * applications must identify the endpoints by reading the User Characteristic + * Description descriptor (0x2901) for each characteristic, which contains the + * endpoint name of the characteristic */ + uint8_t custom_service_uuid[] = { + /* This is a random uuid. This can be modified if you want to change the BLE uuid. */ + /* 12th and 13th bit will be replaced by internal bits. */ + 0xb4, 0xdf, 0x5a, 0x1c, 0x3f, 0x6b, 0xf4, 0xbf, + 0xea, 0x4a, 0x82, 0x03, 0x04, 0x90, 0x1a, 0x02, + }; + esp_err_t err = network_prov_scheme_ble_set_service_uuid(custom_service_uuid); + if (err != ESP_OK) { + ESP_LOGE(TAG, "wifi_prov_scheme_ble_set_service_uuid failed %d", err); + return err; + } + if (mfg_data) { + err = network_prov_scheme_ble_set_mfg_data(mfg_data, mfg_data_len); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to set mfg data, err=0x%x", err); + return err; + } + } +#endif /* CONFIG_APP_NETWORK_PROV_TRANSPORT_BLE */ + + /* Start provisioning service */ + ESP_ERROR_CHECK(network_prov_mgr_start_provisioning(security, pop, service_name, service_key)); + } else { + ESP_LOGI(TAG, "Already provisioned, starting Wi-Fi STA"); + /* We don't need the manager as device is already provisioned, + * so let's release it's resources */ + network_prov_mgr_deinit(); + + /* Start Wi-Fi station */ + wifi_init_sta(); + } + return ESP_OK; +#else + return ESP_ERR_NOT_SUPPORTED; +#endif /* CONFIG_ESP_RMAKER_NETWORK_OVER_WIFI */ +} diff --git a/examples/bridge_apps/esp_rainmaker_bridge/example_components/app_network/private_include/app_wifi_internal.h b/examples/bridge_apps/esp_rainmaker_bridge/example_components/app_network/private_include/app_wifi_internal.h new file mode 100644 index 000000000..e888a9353 --- /dev/null +++ b/examples/bridge_apps/esp_rainmaker_bridge/example_components/app_network/private_include/app_wifi_internal.h @@ -0,0 +1,50 @@ +/* + This example code is in the Public Domain (or CC0 licensed, at your option.) + + Unless required by applicable law or agreed to in writing, this + software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + CONDITIONS OF ANY KIND, either express or implied. +*/ +#pragma once +#include +#include +#include +#include "app_network.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** Initialize Wi-Fi + * + * This initializes Wi-Fi and the network/wifi provisioning manager + * + * @return ESP_OK on success. + * @return error in case of failure. + */ +esp_err_t wifi_init(); + +/** Start Wi-Fi + * + * This will start provisioning if the node is not provisioned and will connect to Wi-Fi + * if node is provisioned. Function will return successfully only after Wi-Fi is connect + * + * @param[in] pop The Proof of Possession (PoP) pin + * @param[in] service_name The service name of network/wifi provisioning. This translates to + * - Wi-Fi SSID when scheme is network_prov_scheme_softap/wifi_prov_scheme_softap + * - device name when scheme is network_prov_scheme_ble/wifi_prov_scheme_ble + * @param[in] service_key The service key of network/wifi provisioning. This translates to + * - Wi-Fi password when scheme is network_prov_scheme_softap/wifi_prov_scheme_softap (NULL = Open network) + * @param[in] mfg_data The manufacture specific data of network/wifi provisioning. + * @param[in] mfg_data The manufacture specific data length of network/wifi provisioning. + * @param[out] provisioned Whether the device is provisioned. + * + * @return ESP_OK on success (Wi-Fi connected). + * @return error in case of failure. + */ +esp_err_t wifi_start(const char *pop, const char *service_name, const char *service_key, uint8_t *mfg_data, + size_t mfg_data_len, bool *provisioned); + +#ifdef __cplusplus +} +#endif diff --git a/examples/bridge_apps/esp_rainmaker_bridge/example_components/app_network/sdkconfig.rename b/examples/bridge_apps/esp_rainmaker_bridge/example_components/app_network/sdkconfig.rename new file mode 100644 index 000000000..02b70d91c --- /dev/null +++ b/examples/bridge_apps/esp_rainmaker_bridge/example_components/app_network/sdkconfig.rename @@ -0,0 +1,13 @@ +# sdkconfig replacement configurations for deprecated options formatted as +# CONFIG_DEPRECATED_OPTION CONFIG_NEW_OPTION + + +CONFIG_APP_WIFI_PROV_SHOW_QR CONFIG_APP_NETWORK_PROV_SHOW_QR +CONFIG_APP_WIFI_PROV_MAX_POP_MISMATCH CONFIG_APP_NETWORK_PROV_MAX_POP_MISMATCH +CONFIG_APP_WIFI_PROV_TRANSPORT_SOFTAP CONFIG_APP_NETWORK_PROV_TRANSPORT_SOFTAP +CONFIG_APP_WIFI_PROV_TRANSPORT_BLE CONFIG_APP_NETWORK_PROV_TRANSPORT_BLE +CONFIG_APP_WIFI_PROV_TRANSPORT CONFIG_APP_NETWORK_PROV_TRANSPORT +CONFIG_APP_WIFI_RESET_PROV_ON_FAILURE CONFIG_APP_NETWORK_RESET_PROV_ON_FAILURE +CONFIG_APP_WIFI_SHOW_DEMO_INTRO_TEXT CONFIG_APP_NETWORK_SHOW_DEMO_INTRO_TEXT +CONFIG_APP_WIFI_PROV_TIMEOUT_PERIOD CONFIG_APP_NETWORK_PROV_TIMEOUT_PERIOD +CONFIG_APP_WIFI_PROV_NAME_PREFIX CONFIG_APP_NETWORK_PROV_NAME_PREFIX diff --git a/examples/bridge_apps/esp_rainmaker_bridge/example_components/rainmaker_api/CMakeLists.txt b/examples/bridge_apps/esp_rainmaker_bridge/example_components/rainmaker_api/CMakeLists.txt new file mode 100755 index 000000000..fc7418458 --- /dev/null +++ b/examples/bridge_apps/esp_rainmaker_bridge/example_components/rainmaker_api/CMakeLists.txt @@ -0,0 +1,5 @@ +set(requires esp_http_client json mbedtls esp_wifi wifi_provisioning nvs_flash protobuf-c esp_app_format pthread) + +idf_component_register(SRCS "rainmaker_api.cpp" + INCLUDE_DIRS "." + REQUIRES ${requires}) diff --git a/examples/bridge_apps/esp_rainmaker_bridge/example_components/rainmaker_api/rainmaker_api.cpp b/examples/bridge_apps/esp_rainmaker_bridge/example_components/rainmaker_api/rainmaker_api.cpp new file mode 100755 index 000000000..306ebef94 --- /dev/null +++ b/examples/bridge_apps/esp_rainmaker_bridge/example_components/rainmaker_api/rainmaker_api.cpp @@ -0,0 +1,1299 @@ +/* + * SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include +#include +#include +#include +#include "esp_crt_bundle.h" +#include "rainmaker_api.h" + +static const char *TAG = "RM_API"; + +static const char* rainmaker_login_url = "/v1/login2"; +static const char* rainmaker_user_info_url = "/v1/user2"; +static const char* rainmaker_nodes_url = "/v1/user/nodes"; +static const char* rainmaker_nodes_params_url = "/v1/user/nodes/params"; +static const char* rainmaker_nodes_config_url = "/v1/user/nodes/config"; +static const char* rainmaker_group_url = "/v1/user/node_group"; +static const char* rainmaker_node_mapping_url = "/v1/user/nodes/mapping"; +static const char* rainmaker_node_mapping_payload = "{\"secret_key\":\"%s\",\"node_id\":\"%s\",\"operation\":\"%s\"}"; +static const char* rainmaker_node_mapping_payload_remove = "{\"node_id\":\"%s\",\"operation\":\"remove\"}"; + +#define RAINMAKER_URL_LEN 256 + +/* RAII wrapper for HTTP client */ +class HttpClientWrapper +{ +public: + explicit HttpClientWrapper(const esp_http_client_config_t& config) + : client_(esp_http_client_init(&config)) {} + + ~HttpClientWrapper() { + if (client_) { + esp_http_client_cleanup(client_); + } + } + + esp_http_client_handle_t get() const { return client_; } + bool is_valid() const { return client_ != nullptr; } + + /* Non-copyable */ + HttpClientWrapper(const HttpClientWrapper&) = delete; + HttpClientWrapper& operator=(const HttpClientWrapper&) = delete; + +private: + esp_http_client_handle_t client_; +}; + +/* RAII wrapper for JSON objects */ +class JsonWrapper +{ +public: + explicit JsonWrapper(cJSON* json) : json_(json) {} + ~JsonWrapper() { if (json_) cJSON_Delete(json_); } + + cJSON* get() const { return json_; } + cJSON* release() { + cJSON* temp = json_; + json_ = nullptr; + return temp; + } + + /* Non-copyable */ + JsonWrapper(const JsonWrapper&) = delete; + JsonWrapper& operator=(const JsonWrapper&) = delete; + +private: + cJSON* json_; +}; + +/* RAII wrapper for malloc'd memory */ +class MallocWrapper +{ +public: + explicit MallocWrapper(void* ptr) : ptr_(ptr) {} + ~MallocWrapper() { if (ptr_) free(ptr_); } + + void* get() const { return ptr_; } + void* release() { + void* temp = ptr_; + ptr_ = nullptr; + return temp; + } + + /* Non-copyable */ + MallocWrapper(const MallocWrapper&) = delete; + MallocWrapper& operator=(const MallocWrapper&) = delete; + +private: + void* ptr_; +}; + +/* Helper function to make HTTP request */ +esp_err_t RainmakerApi::MakeHttpRequest(esp_http_client_handle_t client, const char* post_data) +{ + if (!client) { + return ESP_ERR_INVALID_ARG; + } + + esp_err_t err = esp_http_client_open(client, post_data ? strlen(post_data) : 0); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to open HTTP connection: %s", esp_err_to_name(err)); + return err; + } + + if (post_data) { + ESP_LOGI(TAG, "Sending data: %s", post_data); + int wlen = esp_http_client_write(client, post_data, strlen(post_data)); + if (wlen < 0) { + ESP_LOGE(TAG, "Write failed"); + return ESP_FAIL; + } + } + + int content_length = esp_http_client_fetch_headers(client); + if (content_length < 0) { + ESP_LOGE(TAG, "Failed to fetch headers"); + return ESP_FAIL; + } + + ESP_LOGI(TAG, "HTTP request sent successfully"); + return ESP_OK; +} + +/* Helper function to read HTTP response */ +esp_err_t RainmakerApi::ReadHttpResponse(esp_http_client_handle_t client, char** response_data) +{ + int content_length = esp_http_client_get_content_length(client); + if (content_length <= 0) { + ESP_LOGE(TAG, "Invalid content length: %d", content_length); + return ESP_FAIL; + } + + *response_data = static_cast(malloc(content_length + 1)); + if (!*response_data) { + ESP_LOGE(TAG, "Failed to allocate memory for response"); + return ESP_ERR_NO_MEM; + } + + int read_len = esp_http_client_read(client, *response_data, content_length); + if (read_len <= 0) { + ESP_LOGE(TAG, "Failed to read response"); + free(*response_data); + *response_data = nullptr; + return ESP_FAIL; + } + + (*response_data)[read_len] = '\0'; + return ESP_OK; +} + +/* Helper function to handle HTTP response and check for authentication errors */ +esp_err_t RainmakerApi::HandleHttpResponse(esp_http_client_handle_t client, char** response_data, + std::function retry_func) +{ + int status_code = esp_http_client_get_status_code(client); + esp_err_t err = ReadHttpResponse(client, response_data); + + if (err != ESP_OK) { + return err; + } + + if (!*response_data) { + return ESP_FAIL; + } + + ESP_LOGD(TAG, "Status code: %d, response_data: %s", status_code, *response_data); + + /* Handle unauthorized error */ + if (status_code == 401 && strstr(*response_data, "Unauthorized")) { + ESP_LOGE(TAG, "Access token expired, attempting re-login"); + free(*response_data); + *response_data = nullptr; + + if (retry_func) { + return retry_func(); + } + return ESP_ERR_INVALID_STATE; + } + + return (status_code == 200 || status_code == 207) ? ESP_OK : ESP_FAIL; +} + +/* Helper function to create JSON payload with refresh token */ +static char* CreateRefreshTokenPayload(const std::string& refresh_token) +{ + JsonWrapper root(cJSON_CreateObject()); + if (!root.get()) { + ESP_LOGE(TAG, "Failed to create JSON object"); + return nullptr; + } + + cJSON_AddStringToObject(root.get(), "refreshtoken", refresh_token.c_str()); + return cJSON_PrintUnformatted(root.get()); +} + +/* Helper function to parse login response */ +static esp_err_t ParseLoginResponse(const char* response_data, std::string& access_token) +{ + JsonWrapper response(cJSON_Parse(response_data)); + if (!response.get()) { + ESP_LOGE(TAG, "Failed to parse response JSON"); + return ESP_FAIL; + } + + cJSON *status = cJSON_GetObjectItem(response.get(), "status"); + if (!status || !status->valuestring || strcmp(status->valuestring, "success") != 0) { + cJSON *description = cJSON_GetObjectItem(response.get(), "description"); + ESP_LOGE(TAG, "Login failed: %s", + description && description->valuestring ? description->valuestring : "Unknown error"); + return ESP_FAIL; + } + + cJSON *access_token_json = cJSON_GetObjectItem(response.get(), "accesstoken"); + if (!access_token_json || !access_token_json->valuestring) { + ESP_LOGE(TAG, "No access token in response"); + return ESP_FAIL; + } + + access_token = access_token_json->valuestring; + ESP_LOGI(TAG, "Access token saved successfully"); + return ESP_OK; +} + +/* Helper function to set common HTTP headers */ +static void SetCommonHeaders(esp_http_client_handle_t client, const std::string& access_token, + bool is_json_content = false) +{ + if (!access_token.empty()) { + esp_http_client_set_header(client, "Authorization", access_token.c_str()); + } + esp_http_client_set_header(client, "accept", "application/json"); + if (is_json_content) { + esp_http_client_set_header(client, "Content-Type", "application/json"); + } +} + +RainmakerApi::RainmakerApi() : access_token_(""), refresh_token_(""), base_url_(""), user_id_("") {} + +RainmakerApi::~RainmakerApi() +{ + access_token_.clear(); + refresh_token_.clear(); + base_url_.clear(); + user_id_.clear(); +} + +esp_err_t RainmakerApi::Login(void) +{ + if (refresh_token_.empty()) { + ESP_LOGE(TAG, "Refresh token not available"); + return ESP_ERR_INVALID_STATE; + } + + char url[RAINMAKER_URL_LEN]; + snprintf(url, sizeof(url), "%s%s", base_url_.c_str(), rainmaker_login_url); + + esp_http_client_config_t config = { + .url = url, + .method = HTTP_METHOD_POST, + .crt_bundle_attach = esp_crt_bundle_attach, + }; + + HttpClientWrapper client(config); + if (!client.is_valid()) { + ESP_LOGE(TAG, "Failed to initialize HTTP client"); + return ESP_FAIL; + } + + /* Create JSON payload */ + MallocWrapper post_data(CreateRefreshTokenPayload(refresh_token_)); + if (!post_data.get()) { + ESP_LOGE(TAG, "Failed to create post data"); + return ESP_ERR_NO_MEM; + } + + /* Set headers */ + SetCommonHeaders(client.get(), "", true); + esp_http_client_set_post_field(client.get(), static_cast(post_data.get()), + strlen(static_cast(post_data.get()))); + + esp_err_t err = MakeHttpRequest(client.get(), static_cast(post_data.get())); + if (err != ESP_OK) { + return err; + } + + int status_code = esp_http_client_get_status_code(client.get()); + ESP_LOGI(TAG, "HTTP POST Status = %d", status_code); + + if (status_code != 200) { + ESP_LOGE(TAG, "HTTP POST request failed with status code: %d", status_code); + return ESP_FAIL; + } + + char* response_data = nullptr; + err = ReadHttpResponse(client.get(), &response_data); + if (err != ESP_OK) { + return err; + } + + MallocWrapper response_wrapper(response_data); + ESP_LOGD(TAG, "Response data: %s", response_data); + + return ParseLoginResponse(response_data, access_token_); +} + +esp_err_t RainmakerApi::GetUserInfo(void) +{ + if (access_token_.empty()) { + ESP_LOGE(TAG, "Access token not available, need login first"); + if (Login() == ESP_OK) { + return GetUserInfo(); + } + return ESP_FAIL; + } + + char url[RAINMAKER_URL_LEN]; + snprintf(url, sizeof(url), "%s%s", base_url_.c_str(), rainmaker_user_info_url); + + esp_http_client_config_t config = { + .url = url, + .method = HTTP_METHOD_GET, + .buffer_size_tx = 2048, + .crt_bundle_attach = esp_crt_bundle_attach, + }; + + HttpClientWrapper client(config); + if (!client.is_valid()) { + ESP_LOGE(TAG, "Failed to initialize HTTP client"); + return ESP_FAIL; + } + + SetCommonHeaders(client.get(), access_token_); + + esp_err_t err = MakeHttpRequest(client.get()); + if (err != ESP_OK) { + return ESP_FAIL; + } + + char* response_data = nullptr; + auto retry_func = [this]() -> esp_err_t { + if (Login() == ESP_OK) { + return GetUserInfo(); + } + return ESP_FAIL; + }; + + err = HandleHttpResponse(client.get(), &response_data, retry_func); + if (err == ESP_OK) { + JsonWrapper response(cJSON_Parse(response_data)); + if (response.get()) { + cJSON *user_id = cJSON_GetObjectItem(response.get(), "user_id"); + if (user_id && user_id->valuestring) { + user_id_ = user_id->valuestring; + } + } + } + if (response_data) { + free(response_data); + } + return ESP_OK; +} + +esp_err_t RainmakerApi::GetNodesRecursive(const char* start_id) +{ + if (access_token_.empty()) { + ESP_LOGE(TAG, "Access token not available, need login first"); + if (Login() == ESP_OK) { + return GetNodesRecursive(start_id); + } + return ESP_ERR_INVALID_STATE; + } + + char url[RAINMAKER_URL_LEN]; + const char* base_params = "?node_details=true&status=true&config=false¶ms=true&show_tags=false&is_matter=false"; + + if (start_id) { + snprintf(url, sizeof(url), "%s%s%s&start_id=%s", + base_url_.c_str(), rainmaker_nodes_url, base_params, start_id); + } else { + snprintf(url, sizeof(url), "%s%s%s", + base_url_.c_str(), rainmaker_nodes_url, base_params); + } + + esp_http_client_config_t config = { + .url = url, + .method = HTTP_METHOD_GET, + .buffer_size_tx = 2048, + .crt_bundle_attach = esp_crt_bundle_attach, + }; + + HttpClientWrapper client(config); + if (!client.is_valid()) { + ESP_LOGE(TAG, "Failed to initialize HTTP client"); + return ESP_FAIL; + } + + SetCommonHeaders(client.get(), access_token_); + + esp_err_t err = MakeHttpRequest(client.get()); + if (err != ESP_OK) { + return err; + } + + char* response_data = nullptr; + auto retry_func = [this, start_id]() -> esp_err_t { + if (Login() == ESP_OK) { + return GetNodesRecursive(start_id); + } + return ESP_ERR_INVALID_STATE; + }; + + err = HandleHttpResponse(client.get(), &response_data, retry_func); + if (err != ESP_OK) { + return err; + } + + MallocWrapper response_wrapper(response_data); + ESP_LOGD(TAG, "Nodes response: %s", response_data); + + /* Check if there are more devices to fetch */ + JsonWrapper response_json(cJSON_Parse(response_data)); + if (response_json.get()) { + cJSON *next_id = cJSON_GetObjectItem(response_json.get(), "next_id"); + if (next_id && next_id->valuestring) { + ESP_LOGD(TAG, "Found next_id: %s, fetching next batch", next_id->valuestring); + /* Recursively fetch next batch of devices */ + return GetNodesRecursive(next_id->valuestring); + } + } + + return ESP_OK; +} + +esp_err_t RainmakerApi::RefreshNodes(void) +{ + return GetNodesRecursive(nullptr); +} + +char* RainmakerApi::GetNodeList(void) +{ + if (access_token_.empty()) { + ESP_LOGE(TAG, "Access token not available, need login first"); + if (Login() == ESP_OK) { + return GetNodeList(); + } + return nullptr; + } + + char url[RAINMAKER_URL_LEN]; + const char* base_params = "?node_details=false&status=false&config=false¶ms=false&show_tags=false&is_matter=false"; + + snprintf(url, sizeof(url), "%s%s%s", + base_url_.c_str(), rainmaker_nodes_url, base_params); + + esp_http_client_config_t config = { + .url = url, + .method = HTTP_METHOD_GET, + .buffer_size_tx = 2048, + .crt_bundle_attach = esp_crt_bundle_attach, + }; + + HttpClientWrapper client(config); + if (!client.is_valid()) { + ESP_LOGE(TAG, "Failed to initialize HTTP client"); + return nullptr; + } + + SetCommonHeaders(client.get(), access_token_); + + esp_err_t err = MakeHttpRequest(client.get()); + if (err != ESP_OK) { + return nullptr; + } + + char* response_data = nullptr; + auto retry_func = [this]() -> esp_err_t { + if (Login() == ESP_OK) { + return GetNodeList() ? ESP_OK : ESP_FAIL; + } + return ESP_FAIL; + }; + + err = HandleHttpResponse(client.get(), &response_data, retry_func); + if (err != ESP_OK && response_data) { + free(response_data); + return nullptr; + } + + return response_data; +} + +char* RainmakerApi::GetNodeConfig(const char* node_id) +{ + if (access_token_.empty()) { + ESP_LOGE(TAG, "Access token not available, need login first"); + if (Login() == ESP_OK) { + return GetNodeConfig(node_id); + } + return nullptr; + } + + char url[RAINMAKER_URL_LEN]; + snprintf(url, sizeof(url), "%s%s?node_id=%s", base_url_.c_str(), rainmaker_nodes_config_url, node_id); + + esp_http_client_config_t config = { + .url = url, + .method = HTTP_METHOD_GET, + .buffer_size_tx = 2048, + .crt_bundle_attach = esp_crt_bundle_attach, + }; + + HttpClientWrapper client(config); + if (!client.is_valid()) { + ESP_LOGE(TAG, "Failed to initialize HTTP client"); + return nullptr; + } + + SetCommonHeaders(client.get(), access_token_); + + esp_err_t err = MakeHttpRequest(client.get()); + if (err != ESP_OK) { + return nullptr; + } + + char* response_data = nullptr; + auto retry_func = [this, node_id]() -> char* { + if (Login() == ESP_OK) { + return GetNodeConfig(node_id); + } + return nullptr; + }; + + err = HandleHttpResponse(client.get(), &response_data, [&retry_func]() -> esp_err_t { + char* result = retry_func(); + return result ? ESP_OK : ESP_ERR_INVALID_STATE; + }); + + if (err != ESP_OK && response_data) { + free(response_data); + return nullptr; + } + + return response_data; +} + +esp_err_t RainmakerApi::SetNodeParams(const char* payload) +{ + if (access_token_.empty()) { + ESP_LOGE(TAG, "Access token not available, need login first"); + if (Login() == ESP_OK) { + return SetNodeParams(payload); + } + return ESP_ERR_INVALID_STATE; + } + + char url[RAINMAKER_URL_LEN]; + snprintf(url, sizeof(url), "%s%s", base_url_.c_str(), rainmaker_nodes_params_url); + + esp_http_client_config_t config = { + .url = url, + .method = HTTP_METHOD_PUT, + .buffer_size_tx = 2048, + .crt_bundle_attach = esp_crt_bundle_attach, + }; + + HttpClientWrapper client(config); + if (!client.is_valid()) { + ESP_LOGE(TAG, "Failed to initialize HTTP client"); + return ESP_FAIL; + } + + SetCommonHeaders(client.get(), access_token_, true); + esp_http_client_set_post_field(client.get(), payload, strlen(payload)); + + esp_err_t err = MakeHttpRequest(client.get(), payload); + if (err != ESP_OK) { + return err; + } + + char* response_data = nullptr; + auto retry_func = [this, payload]() -> esp_err_t { + if (Login() == ESP_OK) { + return SetNodeParams(payload); + } + return ESP_ERR_INVALID_STATE; + }; + + err = HandleHttpResponse(client.get(), &response_data, retry_func); + if (response_data) { + free(response_data); + } + return err; +} + +char* RainmakerApi::GetNodeParams(const char* node_id) +{ + if (access_token_.empty()) { + ESP_LOGE(TAG, "Access token not available, need login first"); + if (Login() == ESP_OK) { + return GetNodeParams(node_id); + } + return nullptr; + } + + char url[RAINMAKER_URL_LEN]; + snprintf(url, sizeof(url), "%s%s?node_id=%s", base_url_.c_str(), rainmaker_nodes_params_url, node_id); + + esp_http_client_config_t config = { + .url = url, + .method = HTTP_METHOD_GET, + .buffer_size_tx = 2048, + .crt_bundle_attach = esp_crt_bundle_attach, + }; + + HttpClientWrapper client(config); + if (!client.is_valid()) { + ESP_LOGE(TAG, "Failed to initialize HTTP client"); + return nullptr; + } + + SetCommonHeaders(client.get(), access_token_); + + esp_err_t err = MakeHttpRequest(client.get()); + if (err != ESP_OK) { + return nullptr; + } + + char* response_data = nullptr; + auto retry_func = [this, node_id]() -> char* { + if (Login() == ESP_OK) { + return GetNodeParams(node_id); + } + return nullptr; + }; + + err = HandleHttpResponse(client.get(), &response_data, [&retry_func]() -> esp_err_t { + char* result = retry_func(); + return result ? ESP_OK : ESP_ERR_INVALID_STATE; + }); + + if (err != ESP_OK && response_data) { + free(response_data); + return nullptr; + } + + return response_data; +} + +esp_err_t RainmakerApi::SetRefreshToken(const char* refresh_token) +{ + if (!refresh_token) { + ESP_LOGE(TAG, "Refresh token is null"); + return ESP_ERR_INVALID_ARG; + } + + refresh_token_ = refresh_token; + access_token_.clear(); + user_id_.clear(); + return ESP_OK; +} + +esp_err_t RainmakerApi::SetBaseUrl(const char* base_url) +{ + if (!base_url) { + ESP_LOGE(TAG, "Base URL is null"); + return ESP_ERR_INVALID_ARG; + } + base_url_ = base_url; + return ESP_OK; +} + +esp_err_t RainmakerApi::DeleteRefreshToken(void) +{ + refresh_token_.clear(); + access_token_.clear(); + user_id_.clear(); + return ESP_OK; +} + +char* RainmakerApi::GetGroup(void) +{ + if (access_token_.empty()) { + ESP_LOGE(TAG, "Access token not available, need login first"); + if (Login() == ESP_OK) { + return GetGroup(); + } + return nullptr; + } + + char url[RAINMAKER_URL_LEN]; + snprintf(url, sizeof(url), "%s%s?node_list=true", base_url_.c_str(), rainmaker_group_url); + + esp_http_client_config_t config = { + .url = url, + .method = HTTP_METHOD_GET, + .buffer_size_tx = 2048, + .crt_bundle_attach = esp_crt_bundle_attach, + }; + + HttpClientWrapper client(config); + if (!client.is_valid()) { + ESP_LOGE(TAG, "Failed to initialize HTTP client"); + return nullptr; + } + + SetCommonHeaders(client.get(), access_token_); + + esp_err_t err = MakeHttpRequest(client.get()); + if (err != ESP_OK) { + return nullptr; + } + + char* response_data = nullptr; + auto retry_func = [this]() -> char* { + if (Login() == ESP_OK) { + return GetGroup(); + } + return nullptr; + }; + + err = HandleHttpResponse(client.get(), &response_data, [&retry_func]() -> esp_err_t { + char* result = retry_func(); + return result ? ESP_OK : ESP_ERR_INVALID_STATE; + }); + + if (err != ESP_OK && response_data) { + free(response_data); + return nullptr; + } + + return response_data; +} + +esp_err_t RainmakerApi::CreateGroup(const char* group_name) +{ + if (access_token_.empty()) { + ESP_LOGE(TAG, "Access token not available, need login first"); + if (Login() == ESP_OK) { + return CreateGroup(group_name); + } + return ESP_ERR_INVALID_STATE; + } + + char url[RAINMAKER_URL_LEN]; + snprintf(url, sizeof(url), "%s%s", base_url_.c_str(), rainmaker_group_url); + + esp_http_client_config_t config = { + .url = url, + .method = HTTP_METHOD_POST, + .buffer_size_tx = 2048, + .crt_bundle_attach = esp_crt_bundle_attach, + }; + + HttpClientWrapper client(config); + if (!client.is_valid()) { + ESP_LOGE(TAG, "Failed to initialize HTTP client"); + return ESP_FAIL; + } + + /* Create JSON payload */ + JsonWrapper root(cJSON_CreateObject()); + if (!root.get()) { + ESP_LOGE(TAG, "Failed to create JSON object"); + return ESP_ERR_NO_MEM; + } + + cJSON_AddStringToObject(root.get(), "group_name", group_name); + MallocWrapper post_data(cJSON_PrintUnformatted(root.get())); + if (!post_data.get()) { + ESP_LOGE(TAG, "Failed to create post data"); + return ESP_ERR_NO_MEM; + } + + /* Set headers */ + SetCommonHeaders(client.get(), access_token_, true); + esp_http_client_set_post_field(client.get(), static_cast(post_data.get()), + strlen(static_cast(post_data.get()))); + + esp_err_t err = MakeHttpRequest(client.get(), static_cast(post_data.get())); + if (err != ESP_OK) { + return err; + } + + char* response_data = nullptr; + auto retry_func = [this, group_name]() -> esp_err_t { + if (Login() == ESP_OK) { + return CreateGroup(group_name); + } + return ESP_ERR_INVALID_STATE; + }; + + err = HandleHttpResponse(client.get(), &response_data, retry_func); + if (response_data) { + free(response_data); + } + return err; +} + +esp_err_t RainmakerApi::DeleteGroup(const char* group_id) +{ + if (access_token_.empty()) { + ESP_LOGE(TAG, "Access token not available, need login first"); + if (Login() == ESP_OK) { + return DeleteGroup(group_id); + } + return ESP_ERR_INVALID_STATE; + } + + char url[RAINMAKER_URL_LEN]; + snprintf(url, sizeof(url), "%s%s?group_id=%s", base_url_.c_str(), rainmaker_group_url, group_id); + + esp_http_client_config_t config = { + .url = url, + .method = HTTP_METHOD_DELETE, + .buffer_size_tx = 2048, + .crt_bundle_attach = esp_crt_bundle_attach, + }; + + HttpClientWrapper client(config); + if (!client.is_valid()) { + ESP_LOGE(TAG, "Failed to initialize HTTP client"); + return ESP_FAIL; + } + + /* Set headers */ + SetCommonHeaders(client.get(), access_token_, true); + + esp_err_t err = MakeHttpRequest(client.get()); + if (err != ESP_OK) { + return err; + } + + char* response_data = nullptr; + auto retry_func = [this, group_id]() -> esp_err_t { + if (Login() == ESP_OK) { + return DeleteGroup(group_id); + } + return ESP_ERR_INVALID_STATE; + }; + + err = HandleHttpResponse(client.get(), &response_data, retry_func); + if (response_data) { + free(response_data); + } + return err; +} + +esp_err_t RainmakerApi::OperateNodeToGroup(const char* node_id, const char* group_id, + esp_rainmaker_api_group_operation_type_t operation_type) +{ + if (access_token_.empty()) { + ESP_LOGE(TAG, "Access token not available, need login first"); + if (Login() == ESP_OK) { + return OperateNodeToGroup(node_id, group_id, operation_type); + } + return ESP_ERR_INVALID_STATE; + } + + char url[RAINMAKER_URL_LEN]; + snprintf(url, sizeof(url), "%s%s?group_id=%s", base_url_.c_str(), rainmaker_group_url, group_id); + + esp_http_client_config_t config = { + .url = url, + .method = HTTP_METHOD_PUT, + .buffer_size_tx = 2048, + .crt_bundle_attach = esp_crt_bundle_attach, + }; + + HttpClientWrapper client(config); + if (!client.is_valid()) { + ESP_LOGE(TAG, "Failed to initialize HTTP client"); + return ESP_FAIL; + } + + /* Create JSON payload */ + JsonWrapper root(cJSON_CreateObject()); + if (!root.get()) { + ESP_LOGE(TAG, "Failed to create JSON object"); + return ESP_ERR_NO_MEM; + } + + const char* operation = (operation_type == ESP_RAINMAKER_API_ADD_NODE_TO_GROUP) ? "add" : "remove"; + cJSON_AddStringToObject(root.get(), "operation", operation); + + cJSON *nodes_array = cJSON_CreateArray(); + if (!nodes_array) { + ESP_LOGE(TAG, "Failed to create nodes array"); + return ESP_ERR_NO_MEM; + } + + cJSON_AddItemToArray(nodes_array, cJSON_CreateString(node_id)); + cJSON_AddItemToObject(root.get(), "nodes", nodes_array); + + MallocWrapper post_data(cJSON_PrintUnformatted(root.get())); + if (!post_data.get()) { + ESP_LOGE(TAG, "Failed to create post data"); + return ESP_ERR_NO_MEM; + } + + /* Set headers */ + SetCommonHeaders(client.get(), access_token_, true); + esp_http_client_set_post_field(client.get(), static_cast(post_data.get()), + strlen(static_cast(post_data.get()))); + + esp_err_t err = MakeHttpRequest(client.get(), static_cast(post_data.get())); + if (err != ESP_OK) { + return err; + } + + char* response_data = nullptr; + auto retry_func = [this, node_id, group_id, operation_type]() -> esp_err_t { + if (Login() == ESP_OK) { + return OperateNodeToGroup(node_id, group_id, operation_type); + } + return ESP_ERR_INVALID_STATE; + }; + + err = HandleHttpResponse(client.get(), &response_data, retry_func); + if (response_data) { + free(response_data); + } + return err; +} + +static esp_err_t ParseNodeMappingResponse(char *response_data, char *request_id) +{ + esp_err_t err = ESP_OK; + JsonWrapper response(cJSON_Parse(response_data)); + if (response.get()) { + /* Parse the response to extract status and request_id */ + cJSON *status_json = cJSON_GetObjectItem(response.get(), "status"); + if (status_json && status_json->valuestring) { + if (strcmp(status_json->valuestring, "success") == 0) { + cJSON *request_id_json = cJSON_GetObjectItem(response.get(), "request_id"); + if (request_id_json && request_id_json->valuestring && request_id) { + strncpy(request_id, request_id_json->valuestring, strlen(request_id_json->valuestring)); + request_id[strlen(request_id_json->valuestring)] = '\0'; /* Ensure null termination */ + ESP_LOGI(TAG, "Request ID extracted: %s", request_id); + } + } else { + ESP_LOGE(TAG, "Failed to set node mapping, status: %s", status_json->valuestring); + err = ESP_FAIL; + } + } + } else { + ESP_LOGE(TAG, "Failed to parse node mapping response"); + err = ESP_FAIL; + } + return err; +} + +esp_err_t RainmakerApi::SetNodeMapping(const char* user_id, const char* secret_key, const char* node_id, + esp_rainmaker_api_node_mapping_operation_type_t operation_type, char *request_id) +{ + if (access_token_.empty()) { + ESP_LOGE(TAG, "Access token not available, need login first"); + if (Login() == ESP_OK) { + return SetNodeMapping(user_id, secret_key, node_id, operation_type, request_id); + } + return ESP_ERR_INVALID_STATE; + } + + char url[RAINMAKER_URL_LEN]; + snprintf(url, sizeof(url), "%s%s", base_url_.c_str(), rainmaker_node_mapping_url); + + esp_http_client_config_t config = { + .url = url, + .method = HTTP_METHOD_PUT, + .buffer_size_tx = 2048, + .crt_bundle_attach = esp_crt_bundle_attach, + }; + + HttpClientWrapper client(config); + if (!client.is_valid()) { + ESP_LOGE(TAG, "Failed to initialize HTTP client"); + return ESP_FAIL; + } + + const char* operation = (operation_type == ESP_RAINMAKER_API_ADD_NODE_MAPPING) ? "add" : "remove"; + char payload[256] = {0}; + if (operation_type == ESP_RAINMAKER_API_ADD_NODE_MAPPING) { + snprintf(payload, sizeof(payload), rainmaker_node_mapping_payload, secret_key, node_id, operation); + } else if (operation_type == ESP_RAINMAKER_API_REMOVE_NODE_MAPPING) { + snprintf(payload, sizeof(payload), rainmaker_node_mapping_payload_remove, node_id); + } else { + ESP_LOGE(TAG, "Invalid operation type"); + return ESP_FAIL; + } + + SetCommonHeaders(client.get(), access_token_, true); + esp_http_client_set_post_field(client.get(), payload, strlen(payload)); + + esp_err_t err = MakeHttpRequest(client.get(), payload); + if (err != ESP_OK) { + return err; + } + + char* response_data = nullptr; + auto retry_func = [this, user_id, secret_key, node_id, operation_type, request_id]() -> esp_err_t { + if (Login() == ESP_OK) { + return SetNodeMapping(user_id, secret_key, node_id, operation_type, request_id); + } + return ESP_ERR_INVALID_STATE; + }; + + err = HandleHttpResponse(client.get(), &response_data, retry_func); + if (response_data) { + if (err == ESP_OK) { + err = ParseNodeMappingResponse(response_data, request_id); + } + free(response_data); + } + return err; +} + +static esp_rainmaker_api_node_mapping_status_type_t ParseNodeMappingStatusResponse(char *response_data) +{ + esp_rainmaker_api_node_mapping_status_type_t status = ESP_RAINMAKER_API_NODE_MAPPING_STATUS_INTERNAL_ERROR; + JsonWrapper response(cJSON_Parse(response_data)); + if (response.get()) { + cJSON *status_json = cJSON_GetObjectItem(response.get(), "request_status"); + if (status_json && status_json->valuestring) { + if (strcmp(status_json->valuestring, "requested") == 0) { + status = ESP_RAINMAKER_API_NODE_MAPPING_STATUS_REQUESTED; + } else if (strcmp(status_json->valuestring, "confirmed") == 0) { + status = ESP_RAINMAKER_API_NODE_MAPPING_STATUS_CONFIRMED; + } else if (strcmp(status_json->valuestring, "timeout") == 0) { + status = ESP_RAINMAKER_API_NODE_MAPPING_STATUS_TIMEOUT; + } else if (strcmp(status_json->valuestring, "discarded") == 0) { + status = ESP_RAINMAKER_API_NODE_MAPPING_STATUS_DISCARDED; + } else { + ESP_LOGE(TAG, "Unknown node mapping status: %s", status_json->valuestring); + status = ESP_RAINMAKER_API_NODE_MAPPING_STATUS_INTERNAL_ERROR; + } + } + } else { + ESP_LOGE(TAG, "Failed to parse node mapping status response"); + status = ESP_RAINMAKER_API_NODE_MAPPING_STATUS_INTERNAL_ERROR; + } + return status; +} + +esp_rainmaker_api_node_mapping_status_type_t RainmakerApi::GetNodeMappingStatus(const char *request_id) +{ + if (access_token_.empty()) { + ESP_LOGE(TAG, "Access token not available, need login first"); + if (Login() == ESP_OK) { + return GetNodeMappingStatus(request_id); + } + return ESP_RAINMAKER_API_NODE_MAPPING_STATUS_INTERNAL_ERROR; + } + + char url[RAINMAKER_URL_LEN]; + snprintf(url, sizeof(url), "%s%s?request_id=%s", base_url_.c_str(), rainmaker_node_mapping_url, request_id); + + esp_http_client_config_t config = { + .url = url, + .method = HTTP_METHOD_GET, + .buffer_size_tx = 2048, + .crt_bundle_attach = esp_crt_bundle_attach, + }; + + HttpClientWrapper client(config); + if (!client.is_valid()) { + ESP_LOGE(TAG, "Failed to initialize HTTP client"); + return ESP_RAINMAKER_API_NODE_MAPPING_STATUS_INTERNAL_ERROR; + } + + SetCommonHeaders(client.get(), access_token_); + + esp_err_t err = MakeHttpRequest(client.get()); + if (err != ESP_OK) { + return ESP_RAINMAKER_API_NODE_MAPPING_STATUS_INTERNAL_ERROR; + } + + char* response_data = nullptr; + auto retry_func = [this, request_id]() -> esp_rainmaker_api_node_mapping_status_type_t { + if (Login() == ESP_OK) { + return GetNodeMappingStatus(request_id); + } + return ESP_RAINMAKER_API_NODE_MAPPING_STATUS_INTERNAL_ERROR; + }; + + esp_rainmaker_api_node_mapping_status_type_t status = ESP_RAINMAKER_API_NODE_MAPPING_STATUS_INTERNAL_ERROR; + err = HandleHttpResponse(client.get(), &response_data, retry_func); + if (err == ESP_OK) { + status = ParseNodeMappingStatusResponse(response_data); + } + if (response_data) { + free(response_data); + } + return status; +} + +const std::string& RainmakerApi::GetUserId() const +{ + return user_id_; +} + +esp_err_t RainmakerApi::GetNodeConnectionStatus(const char *node_id, bool *connection_status) +{ + if (access_token_.empty()) { + ESP_LOGE(TAG, "Access token not available, need login first"); + if (Login() == ESP_OK) { + return GetNodeConnectionStatus(node_id, connection_status); + } + return ESP_ERR_INVALID_STATE; + } + + char url[RAINMAKER_URL_LEN] = {0}; + snprintf(url, sizeof(url), "%s%s?node_id=%s&node_details=false&status=true&config=false¶ms=false&show_tags=false&is_matter=false", + base_url_.c_str(), rainmaker_nodes_url, node_id); + + esp_http_client_config_t config = { + .url = url, + .method = HTTP_METHOD_GET, + .buffer_size_tx = 2048, + .crt_bundle_attach = esp_crt_bundle_attach, + }; + + HttpClientWrapper client(config); + if (!client.is_valid()) { + ESP_LOGE(TAG, "Failed to initialize HTTP client"); + return ESP_ERR_INVALID_STATE; + } + + SetCommonHeaders(client.get(), access_token_); + + esp_err_t err = MakeHttpRequest(client.get()); + if (err != ESP_OK) { + return ESP_ERR_INVALID_STATE; + } + + char* response_data = nullptr; + auto retry_func = [this, node_id, connection_status]() -> esp_err_t { + if (Login() == ESP_OK) { + return GetNodeConnectionStatus(node_id, connection_status); + } + return ESP_ERR_INVALID_STATE; + }; + + err = HandleHttpResponse(client.get(), &response_data, retry_func); + if (err != ESP_OK) { + return ESP_ERR_INVALID_STATE; + } + /* Parse response data, extract connected field */ + if (!response_data) { + ESP_LOGE(TAG, "No response data received"); + return ESP_ERR_INVALID_RESPONSE; + } + + JsonWrapper root(cJSON_Parse(response_data)); + if (!root.get()) { + ESP_LOGE(TAG, "Failed to parse response JSON"); + free(response_data); + return ESP_ERR_INVALID_RESPONSE; + } + + cJSON *node_details = cJSON_GetObjectItem(root.get(), "node_details"); + if (!node_details || !cJSON_IsArray(node_details)) { + ESP_LOGE(TAG, "No node_details object in response"); + free(response_data); + return ESP_ERR_INVALID_RESPONSE; + } + + cJSON *node = cJSON_GetArrayItem(node_details, 0); + if (!node) { + ESP_LOGE(TAG, "No node object in node_details array"); + free(response_data); + return ESP_ERR_INVALID_RESPONSE; + } + + cJSON *status = cJSON_GetObjectItem(node, "status"); + if (!status) { + ESP_LOGE(TAG, "No status object in node"); + free(response_data); + return ESP_ERR_INVALID_RESPONSE; + } + + cJSON *connectivity = cJSON_GetObjectItem(status, "connectivity"); + if (!connectivity) { + ESP_LOGE(TAG, "No connectivity object in status"); + free(response_data); + return ESP_ERR_INVALID_RESPONSE; + } + + cJSON *connected = cJSON_GetObjectItem(connectivity, "connected"); + if (!connected || !cJSON_IsBool(connected)) { + ESP_LOGE(TAG, "No connected field in connectivity"); + free(response_data); + return ESP_ERR_INVALID_RESPONSE; + } + + if (connection_status) { + *connection_status = cJSON_IsTrue(connected); + } + + free(response_data); + + return ESP_OK; +} + +esp_err_t esp_rainmaker_api_login(void) +{ + return RainmakerApi::GetInstance().Login(); +} + +esp_err_t esp_rainmaker_api_refresh_nodes(void) +{ + return ESP_OK; +} + +char* esp_rainmaker_api_get_nodes_list(void) +{ + return RainmakerApi::GetInstance().GetNodeList(); +} + +char* esp_rainmaker_api_get_node_config(const char* node_id) +{ + return RainmakerApi::GetInstance().GetNodeConfig(node_id); +} + +esp_err_t esp_rainmaker_api_set_node_params(const char* payload) +{ + return RainmakerApi::GetInstance().SetNodeParams(payload); +} + +char* esp_rainmaker_api_get_node_params(const char* node_id) +{ + return RainmakerApi::GetInstance().GetNodeParams(node_id); +} + +esp_err_t esp_rainmaker_api_set_refresh_token(const char* refresh_token) +{ + return RainmakerApi::GetInstance().SetRefreshToken(refresh_token); +} + +esp_err_t esp_rainmaker_api_set_base_url(const char* base_url) +{ + return RainmakerApi::GetInstance().SetBaseUrl(base_url); +} + +esp_err_t esp_rainmaker_api_delete_refresh_token(void) +{ + return RainmakerApi::GetInstance().DeleteRefreshToken(); +} + +char* esp_rainmaker_api_get_group(void) +{ + return RainmakerApi::GetInstance().GetGroup(); +} + +esp_err_t esp_rainmaker_api_create_group(const char* group_name) +{ + return RainmakerApi::GetInstance().CreateGroup(group_name); +} + +esp_err_t esp_rainmaker_api_delete_group(const char* group_id) +{ + return RainmakerApi::GetInstance().DeleteGroup(group_id); +} + +esp_err_t esp_rainmaker_api_operate_node_to_group(const char* node_id, const char* group_id, + esp_rainmaker_api_group_operation_type_t operation_type) +{ + return RainmakerApi::GetInstance().OperateNodeToGroup(node_id, group_id, operation_type); +} + +esp_err_t esp_rainmaker_api_set_node_mapping(const char* user_id, const char* secret_key, const char* node_id, + esp_rainmaker_api_node_mapping_operation_type_t operation_type, char *request_id) +{ + return RainmakerApi::GetInstance().SetNodeMapping(user_id, secret_key, node_id, operation_type, request_id); +} + +esp_rainmaker_api_node_mapping_status_type_t esp_rainmaker_api_get_node_mapping_status(const char *request_id) +{ + return RainmakerApi::GetInstance().GetNodeMappingStatus(request_id); +} + +esp_err_t esp_rainmaker_api_get_node_connection_status(const char *node_id, bool *connection_status) +{ + return RainmakerApi::GetInstance().GetNodeConnectionStatus(node_id, connection_status); +} + +char* esp_rainmaker_api_get_user_id(void) +{ + const std::string& user_id = RainmakerApi::GetInstance().GetUserId(); + if (user_id.empty()) { + return nullptr; + } + return strdup(user_id.c_str()); +} diff --git a/examples/bridge_apps/esp_rainmaker_bridge/example_components/rainmaker_api/rainmaker_api.h b/examples/bridge_apps/esp_rainmaker_bridge/example_components/rainmaker_api/rainmaker_api.h new file mode 100755 index 000000000..0e025d48f --- /dev/null +++ b/examples/bridge_apps/esp_rainmaker_bridge/example_components/rainmaker_api/rainmaker_api.h @@ -0,0 +1,394 @@ +/* + * SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include +#include +#include +#include + +#ifdef __cplusplus +#include +#include +#include +#include +#include +#include + +extern "C" { +#endif + +/** + * @brief Group operation types for node management + */ +typedef enum { + ESP_RAINMAKER_API_ADD_NODE_TO_GROUP = 0, /* Add node to group */ + ESP_RAINMAKER_API_REMOVE_NODE_FROM_GROUP, /* Remove node from group */ +} esp_rainmaker_api_group_operation_type_t; + +/** + * @brief Node mapping operation types + */ +typedef enum { + ESP_RAINMAKER_API_ADD_NODE_MAPPING = 0, /* Add node mapping */ + ESP_RAINMAKER_API_REMOVE_NODE_MAPPING, /* Remove node mapping */ +} esp_rainmaker_api_node_mapping_operation_type_t; + +/** + * @brief Node mapping status types + */ +typedef enum { + ESP_RAINMAKER_API_NODE_MAPPING_STATUS_REQUESTED = 0, /* Node mapping status requested */ + ESP_RAINMAKER_API_NODE_MAPPING_STATUS_CONFIRMED, /* Node mapping status confirmed */ + ESP_RAINMAKER_API_NODE_MAPPING_STATUS_TIMEOUT, /* Node mapping status timeout */ + ESP_RAINMAKER_API_NODE_MAPPING_STATUS_DISCARDED, /* Node mapping status discarded */ + ESP_RAINMAKER_API_NODE_MAPPING_STATUS_INTERNAL_ERROR, /* Node mapping status internal error */ +} esp_rainmaker_api_node_mapping_status_type_t; + +/* Login to Rainmaker cloud using refresh token + * This function attempts to login to the Rainmaker cloud using the stored refresh token. + * If successful, it will store the access token for subsequent API calls. + * Returns ESP_OK on success, error code otherwise + */ +esp_err_t esp_rainmaker_api_login(void); + +/* Get user ID + * This function retrieves the user ID associated with the Rainmaker account. + * Returns user ID string + */ +char *esp_rainmaker_api_get_user_id(void); + +/* Get all nodes associated with the account + * This function retrieves all nodes (devices) associated with the Rainmaker account. + * It will automatically handle pagination and create corresponding device objects. + * Returns ESP_OK on success, error code otherwise + */ +esp_err_t esp_rainmaker_api_refresh_nodes(void); + +/* Get nodes list associated with the account + * This function retrieves all nodes (devices) associated with the Rainmaker account. + * The caller is responsible for freeing the returned string. + * Returns JSON string with nodes list (caller must free), nullptr on error + */ +char* esp_rainmaker_api_get_nodes_list(void); + +/* Get node config + * This function retrieves the config of a node (device) in the Rainmaker cloud. + * The caller is responsible for freeing the returned string. + * @param node_id Node ID + * Returns JSON string with node config (caller must free), nullptr on error + */ +char* esp_rainmaker_api_get_node_config(const char* node_id); + +/* Set node parameters + * This function updates the parameters of a node (device) in the Rainmaker cloud. + * The parameters should be provided as a JSON string. + * @param payload JSON string containing parameter updates + * Returns ESP_OK on success, error code otherwise + */ +esp_err_t esp_rainmaker_api_set_node_params(const char* payload); + +/* Get node parameters + * This function retrieves the current parameters of a specific node (device). + * The caller is responsible for freeing the returned string. + * @param node_id ID of the node to query + * Returns JSON string with node parameters (caller must free), nullptr on error + */ +char* esp_rainmaker_api_get_node_params(const char* node_id); + +/* Set refresh token for authentication + * This function stores the refresh token that will be used for authentication. + * The refresh token is used to obtain access tokens for API calls. + * @param refresh_token The refresh token string + * Returns ESP_OK on success, error code otherwise + */ +esp_err_t esp_rainmaker_api_set_refresh_token(const char* refresh_token); + +/* Set base URL for Rainmaker API + * This function sets the base URL for the Rainmaker API. + * @param base_url The base URL string + * Returns ESP_OK on success, error code otherwise + */ +esp_err_t esp_rainmaker_api_set_base_url(const char* base_url); + +/* Delete stored refresh token + * This function clears the stored refresh token and access token. + * This should be called when logging out or when the tokens are no longer valid. + * Returns ESP_OK on success, error code otherwise + */ +esp_err_t esp_rainmaker_api_delete_refresh_token(void); + +/* Get all groups + * This function retrieves all groups associated with the Rainmaker account. + * The caller is responsible for freeing the returned string. + * Returns JSON string with groups (caller must free), nullptr on error + */ +char* esp_rainmaker_api_get_group(void); + +/* Create a new group + * This function creates a new group in the Rainmaker cloud. + * @param group_name Name of the group to create + * Returns ESP_OK on success, error code otherwise + */ +esp_err_t esp_rainmaker_api_create_group(const char* group_name); + +/* Delete a group + * This function deletes an existing group from the Rainmaker cloud. + * @param group_id ID of the group to delete + * Returns ESP_OK on success, error code otherwise + */ +esp_err_t esp_rainmaker_api_delete_group(const char* group_id); + +/* Add or remove node from group + * This function adds or removes a node (device) from a group. + * The operation type determines whether to add or remove the node. + * @param node_id ID of the node to operate on + * @param group_id ID of the target group + * @param operation_type Operation type (add or remove) + * Returns ESP_OK on success, error code otherwise + */ +esp_err_t esp_rainmaker_api_operate_node_to_group(const char* node_id, const char* group_id, + esp_rainmaker_api_group_operation_type_t operation_type); + +/* Set node mapping + * This function sets the node mapping for a user. + * @param user_id User ID + * @param secret_key Secret key + * @param node_id Node ID + * @param operation_type Operation type (add or remove) + * @param request_id Request ID to store the request ID + * Returns ESP_OK on success, error code otherwise + */ +esp_err_t esp_rainmaker_api_set_node_mapping(const char* user_id, const char* secret_key, const char* node_id, + esp_rainmaker_api_node_mapping_operation_type_t operation_type, char *request_id); + +/* Get node mapping status + * This function retrieves the status of a node mapping request. + * @param request_id Request ID + * Returns node mapping status + */ +esp_rainmaker_api_node_mapping_status_type_t esp_rainmaker_api_get_node_mapping_status(const char *request_id); + +/* Get node connection status + * This function retrieves the connection status of a node. + * @param node_id Node ID + * @param connection_status Pointer to store connection status + * Returns ESP_OK on success, error code otherwise + */ +esp_err_t esp_rainmaker_api_get_node_connection_status(const char *node_id, bool *connection_status); + +#ifdef __cplusplus +} + +/** + * @brief Rainmaker API client class (Singleton) + * + * This class provides a unified interface for interacting with ESP Rainmaker cloud services. + * It handles authentication, node management, and group operations. + */ +class RainmakerApi +{ +public: + /** + * @brief Get singleton instance + * @return Reference to the singleton instance + */ + static RainmakerApi& GetInstance() { + static RainmakerApi instance; + return instance; + } + + /* Disable copy constructor and assignment operator */ + RainmakerApi(const RainmakerApi&) = delete; + RainmakerApi& operator=(const RainmakerApi&) = delete; + + /** + * @brief Login to Rainmaker cloud using refresh token + * @return ESP_OK on success, error code otherwise + */ + esp_err_t Login(void); + + /** + * @brief Get user info + * @return ESP_OK on success, error code otherwise + */ + esp_err_t GetUserInfo(void); + + /** + * @brief Get all nodes associated with the account + * @return ESP_OK on success, error code otherwise + */ + esp_err_t RefreshNodes(void); + + /** + * @brief Get nodes list associated with the account + * @return JSON string with node list (caller must free), nullptr on error + */ + char* GetNodeList(void); + + /** + * @brief Get node config + * @param node_id Node ID + * @return JSON string with node config (caller must free), nullptr on error + */ + char* GetNodeConfig(const char* node_id); + + /** + * @brief Set node parameters + * @param payload JSON payload containing parameter updates + * @return ESP_OK on success, error code otherwise + */ + esp_err_t SetNodeParams(const char* payload); + + /** + * @brief Get node parameters + * @param node_id Node ID to query + * @return JSON string with node parameters (caller must free), nullptr on error + */ + char* GetNodeParams(const char* node_id); + + /** + * @brief Set refresh token for authentication + * @param refresh_token The refresh token string + * @return ESP_OK on success, error code otherwise + */ + esp_err_t SetRefreshToken(const char* refresh_token); + + /** + * @brief Set base URL for Rainmaker API + * @param base_url The base URL string + * @return ESP_OK on success, error code otherwise + */ + esp_err_t SetBaseUrl(const char* base_url); + + /** + * @brief Delete stored refresh token + * @return ESP_OK on success, error code otherwise + */ + esp_err_t DeleteRefreshToken(void); + + /** + * @brief Get all groups + * @return JSON string with groups (caller must free), nullptr on error + */ + char* GetGroup(void); + + /** + * @brief Create a new group + * @param group_name Name of the group to create + * @return ESP_OK on success, error code otherwise + */ + esp_err_t CreateGroup(const char* group_name); + + /** + * @brief Delete a group + * @param group_id ID of the group to delete + * @return ESP_OK on success, error code otherwise + */ + esp_err_t DeleteGroup(const char* group_id); + + /** + * @brief Add or remove node from group + * @param node_id ID of the node + * @param group_id ID of the group + * @param operation_type Operation type (add or remove) + * @return ESP_OK on success, error code otherwise + */ + esp_err_t OperateNodeToGroup(const char* node_id, const char* group_id, + esp_rainmaker_api_group_operation_type_t operation_type); + + /** + * @brief Set node mapping + * @param user_id User ID + * @param secret_key Secret key + * @param node_id Node ID + * @param operation_type Operation type (add or remove) + * @param request_id Request ID to store the request ID + * @return ESP_OK on success, error code otherwise + */ + esp_err_t SetNodeMapping(const char* user_id, const char* secret_key, const char* node_id, + esp_rainmaker_api_node_mapping_operation_type_t operation_type, char *request_id); + + /** + * @brief Get node mapping status + * @param request_id Request ID + * @return Node mapping status + */ + esp_rainmaker_api_node_mapping_status_type_t GetNodeMappingStatus(const char *request_id); + + /** + * @brief Get node connection status + * @param node_id Node ID + * @param connection_status Pointer to store connection status + * @return ESP_OK on success, error code otherwise + */ + esp_err_t GetNodeConnectionStatus(const char *node_id, bool *connection_status); + + /** + * @brief Get user id string (for C API) + */ + const std::string& GetUserId() const; + +private: + /** + * @brief Private constructor for singleton pattern + */ + RainmakerApi(); + + /** + * @brief Private destructor + */ + ~RainmakerApi(); + + std::string access_token_; /* Current access token */ + std::string refresh_token_; /* Stored refresh token */ + std::string base_url_; /* Base URL for Rainmaker API */ + std::string user_id_; /* User ID */ + + /** + * @brief Recursively get nodes with pagination + * @param start_id Starting node ID for pagination + * @return ESP_OK on success, error code otherwise + */ + esp_err_t GetNodesRecursive(const char* start_id); + + /** + * @brief Cleanup HTTP client resources + * @param client HTTP client handle + * @param post_data Optional POST data to free + */ + static void CleanupHttpClient(void* client, char* post_data = nullptr); + + /** + * @brief Make HTTP request + * @param client HTTP client handle + * @param post_data Optional POST data + * @return ESP_OK on success, error code otherwise + */ + static esp_err_t MakeHttpRequest(esp_http_client_handle_t client, const char* post_data = nullptr); + + /** + * @brief Handle HTTP response and check for authentication errors + * @param client HTTP client handle + * @param response_data Pointer to store response data + * @param retry_func Optional retry function to call on authentication error + * @return ESP_OK on success, error code otherwise + */ + esp_err_t HandleHttpResponse(esp_http_client_handle_t client, char** response_data, + std::function retry_func); + + /** + * @brief Read HTTP response data + * @param client HTTP client handle + * @param response_data Pointer to store response data (caller must free) + * @return ESP_OK on success, error code otherwise + */ + esp_err_t ReadHttpResponse(esp_http_client_handle_t client, char** response_data); +}; + +#endif /* __cplusplus */ + diff --git a/examples/bridge_apps/esp_rainmaker_bridge/main/CMakeLists.txt b/examples/bridge_apps/esp_rainmaker_bridge/main/CMakeLists.txt new file mode 100644 index 000000000..43626a795 --- /dev/null +++ b/examples/bridge_apps/esp_rainmaker_bridge/main/CMakeLists.txt @@ -0,0 +1,5 @@ +idf_component_register(SRC_DIRS "." + PRIV_INCLUDE_DIRS "." "${ESP_MATTER_PATH}/examples/common/utils") + +set_property(TARGET ${COMPONENT_LIB} PROPERTY CXX_STANDARD 17) +target_compile_options(${COMPONENT_LIB} PRIVATE "-DCHIP_HAVE_CONFIG_H") diff --git a/examples/bridge_apps/esp_rainmaker_bridge/main/Kconfig b/examples/bridge_apps/esp_rainmaker_bridge/main/Kconfig new file mode 100644 index 000000000..162923438 --- /dev/null +++ b/examples/bridge_apps/esp_rainmaker_bridge/main/Kconfig @@ -0,0 +1,53 @@ +menu "Rainmaker Bridge" + + config RAINMAKER_PARAMS_GET_PERIOD_MS + int + default 10000 + range 1000 60000 + help + Set the period to get rainmaker devices params in rainmaker bridge. + +endmenu + +menu "Thread BR Example" + depends on OPENTHREAD_BORDER_ROUTER + + choice THREAD_BR_BOARD_TYPE + prompt "Thread border router board type" + default ESP_THREAD_BR_BOARD_DEV_KIT + help + The board running the border router. + + config ESP_THREAD_BR_BOARD_DEV_KIT + bool "ESP Thread border router dev kit" + help + Integrated border router dev kit + + config M5STACK_THREAD_BR_BOARD + bool "M5Stack Thread border router board" + help + M5Stack CoreS3 with Module Gateway H2 + endchoice + + menu "Board Configuration" + config PIN_TO_RCP_RESET + int "Pin to RCP reset" + default "7" + + config PIN_TO_RCP_BOOT + int "Pin to RCP boot" + default "18" if M5STACK_THREAD_BR_BOARD + default "8" + + config PIN_TO_RCP_TX + int "Pin to RCP TX" + default "10" if M5STACK_THREAD_BR_BOARD + default "17" + + config PIN_TO_RCP_RX + int "Pin to RCP RX" + default "17" if M5STACK_THREAD_BR_BOARD + default "18" + endmenu + +endmenu diff --git a/examples/bridge_apps/esp_rainmaker_bridge/main/app_main.cpp b/examples/bridge_apps/esp_rainmaker_bridge/main/app_main.cpp new file mode 100644 index 000000000..0b472aeb7 --- /dev/null +++ b/examples/bridge_apps/esp_rainmaker_bridge/main/app_main.cpp @@ -0,0 +1,154 @@ +/* + This example code is in the Public Domain (or CC0 licensed, at your option.) + + Unless required by applicable law or agreed to in writing, this + software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + CONDITIONS OF ANY KIND, either express or implied. +*/ + +#include +#include +#include + +#include +#include +#include + +#include +#include +#include + +static const char *TAG = "app_main"; + +using namespace esp_matter; +using namespace esp_matter::attribute; +using namespace esp_matter::endpoint; + +uint16_t aggregator_endpoint_id = chip::kInvalidEndpointId; + +static void app_event_cb(const ChipDeviceEvent *event, intptr_t arg) +{ + switch (event->Type) { + case chip::DeviceLayer::DeviceEventType::kInterfaceIpAddressChanged: + ESP_LOGI(TAG, "Interface IP Address Changed"); + break; + + case chip::DeviceLayer::DeviceEventType::kCommissioningComplete: + ESP_LOGI(TAG, "Commissioning complete"); + break; + + case chip::DeviceLayer::DeviceEventType::kFailSafeTimerExpired: + ESP_LOGI(TAG, "Commissioning failed, fail safe timer expired"); + break; + + case chip::DeviceLayer::DeviceEventType::kCommissioningSessionStarted: + ESP_LOGI(TAG, "Commissioning session started"); + break; + + case chip::DeviceLayer::DeviceEventType::kCommissioningSessionStopped: + ESP_LOGI(TAG, "Commissioning session stopped"); + break; + + case chip::DeviceLayer::DeviceEventType::kCommissioningWindowOpened: + ESP_LOGI(TAG, "Commissioning window opened"); + break; + + case chip::DeviceLayer::DeviceEventType::kCommissioningWindowClosed: + ESP_LOGI(TAG, "Commissioning window closed"); + break; + + default: + break; + } +} + +// This callback is called for every attribute update. The callback implementation shall +// handle the desired attributes and return an appropriate error code. If the attribute +// is not of your interest, please do not return an error code and strictly return ESP_OK. +static esp_err_t app_attribute_update_cb(callback_type_t type, uint16_t endpoint_id, uint32_t cluster_id, + uint32_t attribute_id, esp_matter_attr_val_t *val, void *priv_data) +{ + esp_err_t err = ESP_OK; + + if (type == PRE_UPDATE) { + err = rainmaker_bridge_attribute_update(endpoint_id, cluster_id, attribute_id, val); + } + return err; +} + +// This callback is invoked after the creation or resumption of a bridge endpoint. +// It can be used to add data model elements (e.g., attributes, commands, etc.) to the bridge endpoint. +esp_err_t create_bridge_devices(esp_matter::endpoint_t *ep, uint32_t device_type_id, void *priv_data) +{ + esp_err_t err = ESP_OK; + + switch (device_type_id) { + case ESP_MATTER_ON_OFF_LIGHT_DEVICE_TYPE_ID: { + on_off_light::config_t on_off_light_conf; + err = on_off_light::add(ep, &on_off_light_conf); + break; + } + case ESP_MATTER_DIMMABLE_LIGHT_DEVICE_TYPE_ID: { + dimmable_light::config_t dimmable_light_conf; + err = dimmable_light::add(ep, &dimmable_light_conf); + break; + } + case ESP_MATTER_COLOR_TEMPERATURE_LIGHT_DEVICE_TYPE_ID: { + color_temperature_light::config_t color_temperature_light_conf; + err = color_temperature_light::add(ep, &color_temperature_light_conf); + break; + } + case ESP_MATTER_EXTENDED_COLOR_LIGHT_DEVICE_TYPE_ID: { + extended_color_light::config_t extended_color_light_conf; + err = extended_color_light::add(ep, &extended_color_light_conf); + cluster_t *color_cluster = cluster::get(ep, chip::app::Clusters::ColorControl::Id); + cluster::color_control::feature::hue_saturation::config_t hs_config; + cluster::color_control::feature::hue_saturation::add(color_cluster, &hs_config); + break; + } + case ESP_MATTER_ON_OFF_LIGHT_SWITCH_DEVICE_TYPE_ID: { + on_off_light_switch::config_t switch_config; + err = on_off_light_switch::add(ep, &switch_config); + break; + } + default: { + ESP_LOGE(TAG, "Unsupported bridged matter device type"); + return ESP_ERR_INVALID_ARG; + } + } + return err; +} + +extern "C" void app_main() +{ + esp_err_t err = ESP_OK; + + /* Initialize the ESP NVS layer */ + nvs_flash_init(); + + /* Create a Matter node and add the mandatory Root Node device type on endpoint 0 */ + node::config_t node_config; + node_t *node = node::create(&node_config, app_attribute_update_cb, NULL); + ABORT_APP_ON_FAILURE(node != nullptr, ESP_LOGE(TAG, "Failed to create Matter node")); + + aggregator::config_t aggregator_config; + endpoint_t *aggregator = endpoint::aggregator::create(node, &aggregator_config, ENDPOINT_FLAG_NONE, NULL); + ABORT_APP_ON_FAILURE(aggregator != nullptr, ESP_LOGE(TAG, "Failed to create aggregator endpoint")); + + aggregator_endpoint_id = endpoint::get_id(aggregator); + +#if CONFIG_ENABLE_CHIP_SHELL + esp_matter::console::diagnostics_register_commands(); + esp_matter::console::wifi_register_commands(); + esp_matter::console::factoryreset_register_commands(); + esp_matter::console::init(); +#endif + rainmaker_init(); + + /* Matter start */ + err = esp_matter::start(app_event_cb); + ABORT_APP_ON_FAILURE(err == ESP_OK, ESP_LOGE(TAG, "Failed to start Matter, err:%d", err)); + + err = app_bridge_initialize(node, create_bridge_devices); + ABORT_APP_ON_FAILURE(err == ESP_OK, ESP_LOGE(TAG, "Failed to resume the bridged endpoints: %d", err)); +} diff --git a/examples/bridge_apps/esp_rainmaker_bridge/main/idf_component.yml b/examples/bridge_apps/esp_rainmaker_bridge/main/idf_component.yml new file mode 100644 index 000000000..c0e68bef8 --- /dev/null +++ b/examples/bridge_apps/esp_rainmaker_bridge/main/idf_component.yml @@ -0,0 +1,7 @@ +dependencies: + espressif/esp_rainmaker: + version: "^1.4.2" + espressif/qrcode: + version: "*" + espressif/button: + version: "^4" diff --git a/examples/bridge_apps/esp_rainmaker_bridge/main/rainmaker_bridge.cpp b/examples/bridge_apps/esp_rainmaker_bridge/main/rainmaker_bridge.cpp new file mode 100644 index 000000000..ca00be671 --- /dev/null +++ b/examples/bridge_apps/esp_rainmaker_bridge/main/rainmaker_bridge.cpp @@ -0,0 +1,518 @@ +/* + This example code is in the Public Domain (or CC0 licensed, at your option.) + + Unless required by applicable law or agreed to in writing, this + software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + CONDITIONS OF ANY KIND, either express or implied. +*/ + +#include +#include +#include +#include +#include +#include +#include +#include "esp_rmaker_standard_params.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static const char *TAG = "rainmaker_bridge"; + +using namespace chip::app::Clusters; +using namespace esp_matter; +using namespace esp_matter::cluster; + +extern uint16_t aggregator_endpoint_id; + +static esp_err_t attribute_update(uint16_t endpoint_id, uint32_t cluster_id, uint32_t attribute_id, int value) +{ + esp_err_t err = ESP_OK; + attribute_t *attribute = attribute::get(endpoint_id, cluster_id, attribute_id); + esp_matter_attr_val_t val = esp_matter_invalid(NULL); + attribute::get_val(attribute, &val); + + if (cluster_id == OnOff::Id) { + if (attribute_id == OnOff::Attributes::OnOff::Id) { + if (val.val.b != (bool)value) { + val.val.b = (bool)value; + } else { + return err; + } + } + } else if (cluster_id == LevelControl::Id) { + if (attribute_id == LevelControl::Attributes::CurrentLevel::Id) { + if (val.val.u8 != REMAP_TO_RANGE(value, RMAKER_LEVEL_MAX_VALUE, MATTER_LEVEL_MAX_VALUE)) { + val.val.u8 = REMAP_TO_RANGE(value, RMAKER_LEVEL_MAX_VALUE, MATTER_LEVEL_MAX_VALUE); + } else { + return err; + } + } + } else if (cluster_id == ColorControl::Id) { + if (attribute_id == ColorControl::Attributes::CurrentHue::Id) { + if (val.val.u8 != REMAP_TO_RANGE(value, RMAKER_HUE_MAX_VALUE, MATTER_HUE_MAX_VALUE)) { + val.val.u8 = REMAP_TO_RANGE(value, RMAKER_HUE_MAX_VALUE, MATTER_HUE_MAX_VALUE); + } else { + return err; + } + } else if (attribute_id == ColorControl::Attributes::CurrentSaturation::Id) { + if (val.val.u8 != REMAP_TO_RANGE(value, RMAKER_SATURATION_MAX_VALUE, MATTER_SATURATION_MAX_VALUE)) { + val.val.u8 = REMAP_TO_RANGE(value, RMAKER_SATURATION_MAX_VALUE, MATTER_SATURATION_MAX_VALUE); + } else { + return err; + } + } else if (attribute_id == ColorControl::Attributes::ColorTemperatureMireds::Id) { + if (val.val.u16 != REMAP_TO_RANGE_INVERSE(value, STANDARD_TEMPERATURE_FACTOR)) { + val.val.u16 = REMAP_TO_RANGE_INVERSE(value, STANDARD_TEMPERATURE_FACTOR); + } else { + return err; + } + } + } else if (cluster_id == WindowCovering::Id) { + /* windowcovering was not supported yet */ + return err; + } + + attribute::report(endpoint_id, cluster_id, attribute_id, &val); + + return err; +} + +static esp_err_t get_attribute_value_from_rainmaker_device(uint16_t endpoint_id, const char *node_id, const char *node_name) +{ + int attribute_value; + jparse_ctx_t jctx; + bool power; + + char* receive_buffer = esp_rainmaker_api_get_node_params(node_id); + + if (receive_buffer == NULL) { + return ESP_FAIL; + } + + if (json_parse_start(&jctx, receive_buffer, strlen(receive_buffer)) != 0) { + free(receive_buffer); + return ESP_FAIL; + } + + if (node_name[0] != 0) { + if (json_obj_get_object(&jctx, node_name) == 0) { + if (json_obj_get_int(&jctx, "Brightness", &attribute_value) == 0) { + attribute_update(endpoint_id, LevelControl::Id, LevelControl::Attributes::CurrentLevel::Id, attribute_value); + } + + if (json_obj_get_int(&jctx, "Hue", &attribute_value) == 0) { + attribute_update(endpoint_id, ColorControl::Id, ColorControl::Attributes::CurrentHue::Id, attribute_value); + } + + if (json_obj_get_bool(&jctx, "Power", &power) == 0) { + attribute_value = power; + attribute_update(endpoint_id, OnOff::Id, OnOff::Attributes::OnOff::Id, attribute_value); + } + + if (json_obj_get_int(&jctx, "Saturation", &attribute_value) == 0) { + attribute_update(endpoint_id, ColorControl::Id, ColorControl::Attributes::CurrentSaturation::Id, attribute_value); + } + + if (json_obj_get_int(&jctx, "CCT", &attribute_value) == 0) { + attribute_update(endpoint_id, ColorControl::Id, ColorControl::Attributes::ColorTemperatureMireds::Id, attribute_value); + } + } else { + ESP_LOGE(TAG, "No light param found in json "); + } + json_obj_leave_object(&jctx); + } else { + /* Todo: add other device types later */ + } + + json_parse_end(&jctx); + + free(receive_buffer); + + return ESP_OK; +} + +static uint32_t matter_get_device_type_from_rainmaker_device(const char *input_buf, size_t buf_length, const char *node_name) +{ + jparse_ctx_t jctx; + int param_count; + int device_count; + char type[32]; + char param_type[32]; + uint32_t matter_device_type_id = 0xFFFF; + + if (json_parse_start(&jctx, input_buf, buf_length) != 0) { + return 0xFFFF; + } + + if (json_obj_get_array(&jctx, "devices", &device_count) == 0) { + if (json_arr_get_object(&jctx, 0) == 0) { + if (json_obj_get_string(&jctx, "type", type, sizeof(type)) == 0) { + /* get device type */ + if ((strcmp(type, ESP_RMAKER_DEVICE_LIGHTBULB) == 0) || (strcmp(type, ESP_RMAKER_DEVICE_LIGHT) == 0)) { + matter_device_type_id = ESP_MATTER_ON_OFF_LIGHT_DEVICE_TYPE_ID; + if (json_obj_get_array(&jctx, "params", ¶m_count) == 0) { + for (int i = 0; i < param_count; i++) { + if (json_arr_get_object(&jctx, i) == 0) { + if (json_obj_get_string(&jctx, "type", param_type, sizeof(param_type)) == 0) { + /* get param type */ + if (((strcmp(param_type, ESP_RMAKER_PARAM_HUE) == 0) || (strcmp(param_type, ESP_RMAKER_PARAM_SATURATION) == 0)) && + (matter_device_type_id < ESP_MATTER_EXTENDED_COLOR_LIGHT_DEVICE_TYPE_ID)) { + matter_device_type_id = ESP_MATTER_EXTENDED_COLOR_LIGHT_DEVICE_TYPE_ID; + } else if ((strcmp(param_type, ESP_RMAKER_PARAM_CCT) == 0) && + (matter_device_type_id < ESP_MATTER_COLOR_TEMPERATURE_LIGHT_DEVICE_TYPE_ID)) { + matter_device_type_id = ESP_MATTER_COLOR_TEMPERATURE_LIGHT_DEVICE_TYPE_ID; + } else if (((strcmp(param_type, ESP_RMAKER_PARAM_BRIGHTNESS) == 0) || (strcmp(param_type, ESP_RMAKER_PARAM_INTENSITY) == 0)) && + (matter_device_type_id < ESP_MATTER_DIMMABLE_LIGHT_DEVICE_TYPE_ID)) { + matter_device_type_id = ESP_MATTER_DIMMABLE_LIGHT_DEVICE_TYPE_ID; + } + } + json_arr_leave_object(&jctx); + } + } + json_obj_leave_array(&jctx); + } else { + ESP_LOGE(TAG, "No parameters found in json "); + } + } else { + /* Todo: add other device types */ + ESP_LOGW(TAG, "unsupported device type\n"); + } + } else { + ESP_LOGE(TAG, "No type found in json "); + } + + if (json_obj_get_string(&jctx, "name", node_name, 32) != 0) { + ESP_LOGE(TAG, "Get node name failed"); + } + + json_obj_leave_object(&jctx); + } else { + ESP_LOGE(TAG, "No devices found in array "); + } + json_obj_leave_array(&jctx); + } else { + ESP_LOGE(TAG, "No devices found in json "); + } + + json_parse_end(&jctx); + + return matter_device_type_id; +} + +static esp_err_t rainmaker_bridge_match_device(const char *node_id, const char *node_name, uint32_t device_type_id) +{ + node_t *node = node::get(); + + if (app_bridge_get_matter_endpointid_by_rainmaker_node_id(node_id) == chip::kInvalidEndpointId) { + app_bridged_device_t *bridged_device = app_bridge_create_bridged_device(node, aggregator_endpoint_id, device_type_id, + ESP_MATTER_BRIDGED_DEVICE_TYPE_RAINMAKER, + app_bridge_rainmaker_address(node_id, node_name), NULL); + ESP_RETURN_ON_FALSE(bridged_device, ESP_FAIL, TAG, "Failed to create bridged device (rainmaker device)"); + } else { + ESP_LOGI(TAG, "Bridged node for %s rainmaker device has been created", node_id); + } + + return ESP_OK; +} + +static esp_err_t rainmaker_bridge_get_param_from_device(const char* node_id, uint16_t endpoint_id) +{ + node_t *node = node::get(); + endpoint_t *dev_endpoint = endpoint::get(node, endpoint_id); + uint8_t device_type_count = endpoint::get_device_type_count(dev_endpoint); + uint32_t dev_type_id; + uint8_t dev_type_ver; + + for (uint8_t i = 0; i < device_type_count; ++i) { + if ((ESP_OK == endpoint::get_device_type_at_index(dev_endpoint, i, dev_type_id, dev_type_ver))) { + ESP_LOGI(TAG, "Endpoint Id: %d--Node Id: %s Device Type: %ld\n", endpoint_id, node_id, dev_type_id); + if (dev_type_id != endpoint::bridged_node::get_device_type_id()) { + break; + } + } + } + + switch(dev_type_id) { + case ESP_MATTER_EXTENDED_COLOR_LIGHT_DEVICE_TYPE_ID: + case ESP_MATTER_COLOR_TEMPERATURE_LIGHT_DEVICE_TYPE_ID: + case ESP_MATTER_DIMMABLE_LIGHT_DEVICE_TYPE_ID: + case ESP_MATTER_ON_OFF_LIGHT_DEVICE_TYPE_ID: + { + get_attribute_value_from_rainmaker_device(endpoint_id ,node_id, app_bridge_get_rainmaker_node_name_by_matter_endpointid(endpoint_id)); + } + break; + /* Todo: add other device types */ + default: + break; + } + return ESP_OK; +} + +static esp_err_t rainmaker_bridge_update_online_state(const char* node_id, uint16_t endpoint_id) +{ + esp_err_t err = ESP_OK; + bool connection_status = false; + attribute_t *attribute = attribute::get(endpoint_id, BridgedDeviceBasicInformation::Id, BridgedDeviceBasicInformation::Attributes::Reachable::Id); + esp_matter_attr_val_t val = esp_matter_invalid(NULL); + attribute::get_val(attribute, &val); + + err = esp_rainmaker_api_get_node_connection_status(node_id, &connection_status); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to get node %s connection status", node_id); + } else { + ESP_LOGI(TAG, "node %s connection status: %d", node_id, connection_status); + if (val.val.b != connection_status) { + val.val.b = connection_status; + attribute::update(endpoint_id, BridgedDeviceBasicInformation::Id, BridgedDeviceBasicInformation::Attributes::Reachable::Id, &val); + } + } + return err; +} + +static esp_err_t rainmaker_bridge_add_new_device(const char *node_id) +{ + uint32_t matter_device_type = 0; + char node_name[32] = {0}; + + if (node_id != NULL) { + char* receive_buffer = esp_rainmaker_api_get_node_config(node_id); + if (receive_buffer == NULL) { + ESP_LOGE(TAG, "Get Node %s config failed\n", node_id); + return ESP_FAIL; + } + matter_device_type = matter_get_device_type_from_rainmaker_device(receive_buffer, strlen(receive_buffer), node_name); + if ((matter_device_type != 0xFFFF) && (node_name[0] != 0)) { + rainmaker_bridge_match_device(node_id, node_name, matter_device_type); + } else { + ESP_LOGW(TAG, "Node %s device type 0x%lx is invalid\n", node_id, matter_device_type); + free(receive_buffer); + return ESP_ERR_NOT_FOUND; + } + free(receive_buffer); + } + return ESP_OK; +} + +static esp_err_t rainmaker_bridge_delete_device(uint16_t endpoint_id) +{ + const char* node_id = app_bridge_get_rainmaker_node_id_by_matter_endpointid(endpoint_id); + + if (node_id == NULL) { + ESP_LOGI(TAG, "Can't find rainmaker device from ep: %d", endpoint_id); + } + + app_bridged_device_t *bridged_device = app_bridge_get_device_by_rainmaker_node_id(node_id); + + if (bridged_device) { + app_bridge_remove_device(bridged_device); + ESP_LOGI(TAG, "Bridged rainmaker device removed: %s", node_id); + } else { + ESP_LOGI(TAG, "Bridged rainmaker device not found: %s", node_id); + } + return ESP_OK; +} + +static void matter_check_and_remove_not_exist_device() +{ + uint16_t matter_endpoint_id_array[MAX_BRIDGED_DEVICE_COUNT]; + esp_matter_bridge::get_bridged_endpoint_ids(matter_endpoint_id_array); + + for (int i = 0; i < MAX_BRIDGED_DEVICE_COUNT; i++) { + if (matter_endpoint_id_array[i] != 0xFFFF) { + const char *node_id = app_bridge_get_rainmaker_node_id_by_matter_endpointid(matter_endpoint_id_array[i]); + if (node_id != NULL) { + char *buffer = esp_rainmaker_api_get_node_params(node_id); + if (buffer == NULL) { + ESP_LOGI(TAG, "Remove not exist Rainmaker device Node: %s Endpoint: %d\n", node_id, matter_endpoint_id_array[i]); + rainmaker_bridge_delete_device(matter_endpoint_id_array[i]); + } else { + free(buffer); + } + } + } + } +} + +static esp_err_t rainmaker_sync_nodes(char *out_buf, size_t out_buf_len) +{ + jparse_ctx_t jctx; + int total_count; + char node[32]; + static uint8_t check_count = 0; + if (json_parse_start(&jctx, out_buf, out_buf_len) != 0) { + return ESP_FAIL; + } + + if (json_obj_get_array(&jctx, "nodes", &total_count) == 0) { + for (int i = 0; i < total_count; i++) { + if (json_arr_get_string(&jctx, i, node, sizeof(node)) == 0) { + uint16_t endpoint_id = app_bridge_get_matter_endpointid_by_rainmaker_node_id(node); + if (endpoint_id == chip::kInvalidEndpointId) { + if (rainmaker_bridge_add_new_device(node) == ESP_OK) { + endpoint_id = app_bridge_get_matter_endpointid_by_rainmaker_node_id(node); + } + } + + ESP_LOGI(TAG, "Exist node: %s--endpoint id: %d\n", node, endpoint_id); + if (endpoint_id != chip::kInvalidEndpointId) { + rainmaker_bridge_get_param_from_device(node, endpoint_id); + rainmaker_bridge_update_online_state(node, endpoint_id); + } + } + } + json_obj_leave_array(&jctx); + } else { + ESP_LOGE(TAG, "No node found in json "); + json_obj_leave_array(&jctx); + } + + json_parse_end(&jctx); + + check_count++; + if (check_count >= 5) { + matter_check_and_remove_not_exist_device(); + check_count = 0; + } + return ESP_OK; +} + +esp_err_t rainmaker_bridge_attribute_update(uint16_t endpoint_id, uint32_t cluster_id, uint32_t attribute_id, esp_matter_attr_val_t *val) +{ + char param_buffer[128] = {0}; + const char* node_id = app_bridge_get_rainmaker_node_id_by_matter_endpointid(endpoint_id); + const char* node_name = app_bridge_get_rainmaker_node_name_by_matter_endpointid(endpoint_id); + if (node_id == NULL) { + return ESP_OK; + } + + esp_err_t err = ESP_OK; + if (cluster_id == OnOff::Id) { + if (attribute_id == OnOff::Attributes::OnOff::Id) { + snprintf(param_buffer, sizeof(param_buffer), "[{\"node_id\":\"%s\",\"payload\":{\"%s\":{\"Power\":%s}}}]", node_id, node_name, val->val.b ? "true" : "false"); + } + } else if (cluster_id == LevelControl::Id) { + if (attribute_id == LevelControl::Attributes::CurrentLevel::Id) { + attribute_t *attribute = attribute::get(endpoint_id, OnOff::Id, OnOff::Attributes::OnOff::Id); + esp_matter_attr_val_t val_onoff = esp_matter_invalid(NULL); + attribute::get_val(attribute, &val_onoff); + if (val_onoff.val.b == false) { + return ESP_OK; + } + snprintf(param_buffer, sizeof(param_buffer), "[{\"node_id\":\"%s\",\"payload\":{\"%s\":{\"Brightness\":%d}}}]", node_id, node_name, + REMAP_TO_RANGE(val->val.u8, MATTER_LEVEL_MAX_VALUE, RMAKER_LEVEL_MAX_VALUE)); + } + } else if (cluster_id == ColorControl::Id) { + if (attribute_id == ColorControl::Attributes::CurrentHue::Id) { + snprintf(param_buffer, sizeof(param_buffer), "[{\"node_id\":\"%s\",\"payload\":{\"%s\":{\"Hue\":%d}}}]", node_id, node_name, + REMAP_TO_RANGE(val->val.u8, MATTER_HUE_MAX_VALUE, RMAKER_HUE_MAX_VALUE)); + } else if (attribute_id == ColorControl::Attributes::CurrentSaturation::Id) { + snprintf(param_buffer, sizeof(param_buffer), "[{\"node_id\":\"%s\",\"payload\":{\"%s\":{\"Saturation\":%d}}}]", node_id, node_name, + REMAP_TO_RANGE(val->val.u8, MATTER_SATURATION_MAX_VALUE, RMAKER_SATURATION_MAX_VALUE)); + } else if (attribute_id == ColorControl::Attributes::ColorTemperatureMireds::Id) { + snprintf(param_buffer, sizeof(param_buffer), "[{\"node_id\":\"%s\",\"payload\":{\"%s\":{\"CCT\":%d}}}]", node_id, node_name, + REMAP_TO_RANGE_INVERSE(val->val.u16, STANDARD_TEMPERATURE_FACTOR)); + } + } + + if (param_buffer[0] == 0) { + return ESP_OK; + } + + esp_rainmaker_api_set_node_params(param_buffer); + return err; +} + +static void rainmaker_bridge_task(void *pvParameters) +{ + while(true) { + vTaskDelay(pdMS_TO_TICKS(CONFIG_RAINMAKER_PARAMS_GET_PERIOD_MS)); + char* nodes_buffer = esp_rainmaker_api_get_nodes_list(); + if (nodes_buffer == NULL) { + continue; + } + rainmaker_sync_nodes(nodes_buffer, strlen(nodes_buffer)); + free(nodes_buffer); + } +} + +static esp_err_t write_cb(const esp_rmaker_device_t *device, const esp_rmaker_param_t *param, + const esp_rmaker_param_val_t val, void *priv_data, esp_rmaker_write_ctx_t *ctx) +{ + if (strcmp(esp_rmaker_param_get_type(param), ESP_RMAKER_PARAM_BASE_URL) == 0) { + if (val.type != RMAKER_VAL_TYPE_STRING || !val.val.s) { + return ESP_ERR_INVALID_ARG; + } + ESP_LOGI(TAG, "Set base url: %s\n", val.val.s); + ESP_RETURN_ON_ERROR(esp_rainmaker_api_set_base_url(val.val.s), TAG, "Failed to set base_url"); + } else if (strcmp(esp_rmaker_param_get_type(param), ESP_RMAKER_PARAM_USER_TOKEN) == 0) { + if (val.type != RMAKER_VAL_TYPE_STRING || !val.val.s) { + return ESP_ERR_INVALID_ARG; + } + ESP_LOGI(TAG, "Set user token: %s\n", val.val.s); + ESP_RETURN_ON_ERROR(esp_rainmaker_api_set_refresh_token(val.val.s), TAG, "Failed to set user_token"); + } + + return ESP_OK; +} + +void rainmaker_init() +{ + /* Network Init */ + app_network_init(); + + /* Initialize the ESP RainMaker Agent. + * Note that this should be called after app_network_init() but before app_network_start() + * */ + esp_rmaker_config_t rainmaker_cfg = { + .enable_time_sync = false, + }; + esp_rmaker_node_t *node = esp_rmaker_node_init(&rainmaker_cfg, "ESP RainMaker Device", "RainmakerController"); + if (!node) { + ESP_LOGE(TAG, "Could not initialise node. Aborting!!!"); + vTaskDelay(5000/portTICK_PERIOD_MS); + abort(); + } + + esp_rmaker_device_t *device = esp_rmaker_device_create("Rainmaker Controller", ESP_RMAKER_DEVICE_CONTROLLER, NULL); + + esp_rmaker_device_add_param(device, esp_rmaker_name_param_create(ESP_RMAKER_DEF_NAME_PARAM, "RainmakerController")); + + esp_rmaker_node_add_device(node, device); + + esp_rmaker_node_add_device(node, rainmaker_controller_service_create("RainmakerCTL", write_cb, NULL, NULL)); + + esp_rmaker_device_t *thread_br_device = esp_rmaker_device_create("ThreadBR", ESP_RMAKER_DEVICE_THREAD_BR, NULL); + esp_rmaker_device_add_param(thread_br_device, + esp_rmaker_name_param_create(ESP_RMAKER_DEF_NAME_PARAM, "ESP-ThreadBR")); + esp_rmaker_node_add_device(node, thread_br_device); + + esp_openthread_platform_config_t thread_cfg = { + .radio_config = ESP_OPENTHREAD_DEFAULT_RADIO_CONFIG(), + .host_config = ESP_OPENTHREAD_DEFAULT_HOST_CONFIG(), + .port_config = ESP_OPENTHREAD_DEFAULT_PORT_CONFIG() + }; + +#ifdef CONFIG_AUTO_UPDATE_RCP + esp_rcp_update_config_t rcp_update_cfg = ESP_OPENTHREAD_RCP_UPDATE_CONFIG(); + esp_rmaker_thread_br_enable(&thread_cfg, &rcp_update_cfg); +#else + esp_rmaker_thread_br_enable(&thread_cfg, NULL); +#endif + + esp_rmaker_start(); + + app_network_start(POP_TYPE_RANDOM); + + /* create task to get node and params from rainmaker side */ + xTaskCreate(rainmaker_bridge_task, "rainmaker_main", 10240, xTaskGetCurrentTaskHandle(), 5, NULL); +} + diff --git a/examples/bridge_apps/esp_rainmaker_bridge/main/rainmaker_bridge.h b/examples/bridge_apps/esp_rainmaker_bridge/main/rainmaker_bridge.h new file mode 100644 index 000000000..7c701597a --- /dev/null +++ b/examples/bridge_apps/esp_rainmaker_bridge/main/rainmaker_bridge.h @@ -0,0 +1,29 @@ +/* + This example code is in the Public Domain (or CC0 licensed, at your option.) + + Unless required by applicable law or agreed to in writing, this + software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + CONDITIONS OF ANY KIND, either express or implied. +*/ + +#pragma once + +#include +#include +#include + +#define MATTER_HUE_MAX_VALUE 254 +#define MATTER_SATURATION_MAX_VALUE 254 +#define MATTER_LEVEL_MAX_VALUE 254 + +#define STANDARD_TEMPERATURE_FACTOR 1000000 + +#define RMAKER_HUE_MAX_VALUE 360 +#define RMAKER_SATURATION_MAX_VALUE 100 +#define RMAKER_LEVEL_MAX_VALUE 100 + +esp_err_t rainmaker_bridge_attribute_update(uint16_t endpoint_id, uint32_t cluster_id, + uint32_t attribute_id, esp_matter_attr_val_t *val); + +/* Init Rainmaker */ +void rainmaker_init(); \ No newline at end of file diff --git a/examples/bridge_apps/esp_rainmaker_bridge/main/rainmaker_controller_std.cpp b/examples/bridge_apps/esp_rainmaker_bridge/main/rainmaker_controller_std.cpp new file mode 100644 index 000000000..ba05e157b --- /dev/null +++ b/examples/bridge_apps/esp_rainmaker_bridge/main/rainmaker_controller_std.cpp @@ -0,0 +1,36 @@ +/* + This example code is in the Public Domain (or CC0 licensed, at your option.) + + Unless required by applicable law or agreed to in writing, this + software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + CONDITIONS OF ANY KIND, either express or implied. +*/ + +#include +#include + +esp_rmaker_param_t *rainmaker_controller_base_url_param_create(const char *param_name) +{ + esp_rmaker_param_t *param = esp_rmaker_param_create(param_name, ESP_RMAKER_PARAM_BASE_URL, + esp_rmaker_str(""), PROP_FLAG_READ | PROP_FLAG_WRITE | PROP_FLAG_PERSIST); + return param; +} + +esp_rmaker_param_t *rainmaker_controller_user_token_param_create(const char *param_name) +{ + esp_rmaker_param_t *param = esp_rmaker_param_create(param_name, ESP_RMAKER_PARAM_USER_TOKEN, + esp_rmaker_str(""), PROP_FLAG_WRITE | PROP_FLAG_PERSIST); + return param; +} + +esp_rmaker_device_t *rainmaker_controller_service_create(const char *serv_name, esp_rmaker_device_write_cb_t write_cb, + esp_rmaker_device_read_cb_t read_cb, void *priv_data) +{ + esp_rmaker_device_t *service = esp_rmaker_service_create(serv_name, ESP_RMAKER_SERVICE_CONTROLLER, priv_data); + if (service) { + esp_rmaker_device_add_cb(service, write_cb, read_cb); + esp_rmaker_device_add_param(service, rainmaker_controller_base_url_param_create(ESP_RMAKER_DEF_BASE_URL_NAME)); + esp_rmaker_device_add_param(service, rainmaker_controller_user_token_param_create(ESP_RMAKER_DEF_USER_TOKEN_NAME)); + } + return service; +} diff --git a/examples/bridge_apps/esp_rainmaker_bridge/main/rainmaker_controller_std.h b/examples/bridge_apps/esp_rainmaker_bridge/main/rainmaker_controller_std.h new file mode 100644 index 000000000..d00274b5d --- /dev/null +++ b/examples/bridge_apps/esp_rainmaker_bridge/main/rainmaker_controller_std.h @@ -0,0 +1,30 @@ +/* + This example code is in the Public Domain (or CC0 licensed, at your option.) + + Unless required by applicable law or agreed to in writing, this + software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + CONDITIONS OF ANY KIND, either express or implied. +*/ + +#pragma once + +#include +#include + +// Rainmaker Controller device +#define ESP_RMAKER_DEVICE_CONTROLLER "esp.device.controller" + +// Rainmaker Controller service +#define ESP_RMAKER_SERVICE_CONTROLLER "esp.service.rmaker-user-auth" + +// Rainmaker Controller parameters +#define ESP_RMAKER_DEF_BASE_URL_NAME "BaseURL" +#define ESP_RMAKER_PARAM_BASE_URL "esp.param.base-url" +#define ESP_RMAKER_DEF_USER_TOKEN_NAME "UserToken" +#define ESP_RMAKER_PARAM_USER_TOKEN "esp.param.user-token" + +esp_rmaker_param_t *rainmaker_controller_base_url_param_create(const char *param_name); +esp_rmaker_param_t *rainmaker_controller_user_token_param_create(const char *param_name); + +esp_rmaker_device_t *rainmaker_controller_service_create(const char *serv_name, esp_rmaker_device_write_cb_t write_cb, + esp_rmaker_device_read_cb_t read_cb, void *priv_data); diff --git a/examples/bridge_apps/esp_rainmaker_bridge/partitions.csv b/examples/bridge_apps/esp_rainmaker_bridge/partitions.csv new file mode 100644 index 000000000..18501a277 --- /dev/null +++ b/examples/bridge_apps/esp_rainmaker_bridge/partitions.csv @@ -0,0 +1,10 @@ +# Name, Type, SubType, Offset, Size, Flags +# Note: Firmware partition offset needs to be 64K aligned, initial 36K (9 sectors) are reserved for bootloader and partition table +esp_secure_cert, 0x3F, , 0xD000, 0x2000, encrypted +nvs, data, nvs, 0x10000, 0xF000, +nvs_keys, data, nvs_keys, 0x1F000, 0x1000, encrypted +ota_0, app, ota_0, 0x20000, 0x250000, +ota_1, app, ota_1, 0x270000, 0x250000, +fctry, data, nvs, 0x4C0000, 0x6000, +phy_init, data, phy, 0x4C6000, 0x1000, +otadata, data, ota, 0x4C7000, 0x2000 diff --git a/examples/bridge_apps/esp_rainmaker_bridge/sdkconfig.defaults b/examples/bridge_apps/esp_rainmaker_bridge/sdkconfig.defaults new file mode 100644 index 000000000..56f5686e6 --- /dev/null +++ b/examples/bridge_apps/esp_rainmaker_bridge/sdkconfig.defaults @@ -0,0 +1,47 @@ +CONFIG_ESPTOOLPY_FLASHSIZE_8MB=y + +# Enable BT +CONFIG_BT_ENABLED=y +CONFIG_BT_NIMBLE_ENABLED=y +CONFIG_BT_NIMBLE_TASK_STACK_SIZE=5120 +CONFIG_BT_NIMBLE_ACL_BUF_SIZE=255 + +# Enable FreeRTOS legacy API +CONFIG_FREERTOS_ENABLE_BACKWARD_COMPATIBILITY=y + +# Enable lwip ipv6 autoconfig +CONFIG_LWIP_IPV6_AUTOCONFIG=y + +# Use a custom partition table +CONFIG_PARTITION_TABLE_CUSTOM=y +CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv" + +# Enable chip shell +CONFIG_ENABLE_CHIP_SHELL=y + +# Enable lwIP route hooks +CONFIG_LWIP_HOOK_IP6_ROUTE_DEFAULT=y +CONFIG_LWIP_HOOK_ND6_GET_GW_DEFAULT=y + +# Watchdog +CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU0=n +CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU1=n + +# Enable HKDF in mbedtls +CONFIG_MBEDTLS_HKDF_C=y + +# Increase LwIP IPv6 address number to 6 (MAX_FABRIC + 1) +# unique local addresses for fabrics(MAX_FABRIC), a link local address(1) +CONFIG_LWIP_IPV6_NUM_ADDRESSES=6 + +# Increase matter console stack size +CONFIG_ESP_MATTER_CONSOLE_TASK_STACK=3072 + +# mbedtls +CONFIG_MBEDTLS_DYNAMIC_BUFFER=y +CONFIG_MBEDTLS_DYNAMIC_FREE_PEER_CERT=y +CONFIG_MBEDTLS_DYNAMIC_FREE_CONFIG_DATA=y +CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_DEFAULT_CMN=y + +# Disable BLE for matter +CONFIG_ENABLE_CHIPOBLE=n diff --git a/examples/bridge_apps/esp_rainmaker_bridge/sdkconfig.defaults.esp32s3 b/examples/bridge_apps/esp_rainmaker_bridge/sdkconfig.defaults.esp32s3 new file mode 100644 index 000000000..91e2138f6 --- /dev/null +++ b/examples/bridge_apps/esp_rainmaker_bridge/sdkconfig.defaults.esp32s3 @@ -0,0 +1,7 @@ +CONFIG_IDF_TARGET="esp32s3" + +# System event stack size +CONFIG_ESP_SYSTEM_EVENT_TASK_STACK_SIZE=3072 + +# Increase timer stack size +CONFIG_FREERTOS_TIMER_TASK_STACK_DEPTH=4096 \ No newline at end of file diff --git a/examples/common/app_bridge/app_bridged_device.cpp b/examples/common/app_bridge/app_bridged_device.cpp index 083e11330..31fce71cf 100644 --- a/examples/common/app_bridge/app_bridged_device.cpp +++ b/examples/common/app_bridge/app_bridged_device.cpp @@ -165,6 +165,17 @@ app_bridged_device_address_t app_bridge_espnow_address(uint8_t espnow_macaddr[6] return bridged_address; } +app_bridged_device_address_t app_bridge_rainmaker_address(const char* rainmaker_node_id, const char* rainmaker_node_name) +{ + app_bridged_device_address_t bridged_address = { + .rainmaker_node_id = {0}, + .rainmaker_node_name = {0}, + }; + memcpy(bridged_address.rainmaker_node_id, rainmaker_node_id, 32); + memcpy(bridged_address.rainmaker_node_name, rainmaker_node_name, 32); + return bridged_address; +} + /** Bridged Device APIs */ app_bridged_device_t *app_bridge_create_bridged_device(node_t *node, uint16_t parent_endpoint_id, uint32_t matter_device_type_id, @@ -404,4 +415,57 @@ uint8_t *app_bridge_get_espnow_macaddr_by_matter_endpointid(uint16_t matter_endp } return NULL; } + +/** Rainmaker Device APIs */ +app_bridged_device_t *app_bridge_get_device_by_rainmaker_node_id(char rainmaker_node_id[32]) +{ + app_bridged_device_t *current_dev = g_bridged_device_list; + while (current_dev) { + if ((current_dev->dev_type == ESP_MATTER_BRIDGED_DEVICE_TYPE_RAINMAKER) && current_dev->dev && + !memcmp(current_dev->dev_addr.rainmaker_node_id, rainmaker_node_id, 32)) { + return current_dev; + } + current_dev = current_dev->next; + } + return NULL; +} + +uint16_t app_bridge_get_matter_endpointid_by_rainmaker_node_id(char rainmaker_node_id[32]) +{ + app_bridged_device_t *current_dev = g_bridged_device_list; + while (current_dev) { + if ((current_dev->dev_type == ESP_MATTER_BRIDGED_DEVICE_TYPE_RAINMAKER) && current_dev->dev && + !memcmp(current_dev->dev_addr.rainmaker_node_id, rainmaker_node_id, strlen(rainmaker_node_id))) { + return esp_matter::endpoint::get_id(current_dev->dev->endpoint); + } + current_dev = current_dev->next; + } + return chip::kInvalidEndpointId; +} + +char* app_bridge_get_rainmaker_node_id_by_matter_endpointid(uint16_t matter_endpointid) +{ + app_bridged_device_t *current_dev = g_bridged_device_list; + while (current_dev) { + if ((current_dev->dev_type == ESP_MATTER_BRIDGED_DEVICE_TYPE_RAINMAKER) && current_dev->dev && + (esp_matter::endpoint::get_id(current_dev->dev->endpoint) == matter_endpointid)) { + return current_dev->dev_addr.rainmaker_node_id; + } + current_dev = current_dev->next; + } + return NULL; +} + +char* app_bridge_get_rainmaker_node_name_by_matter_endpointid(uint16_t matter_endpointid) +{ + app_bridged_device_t *current_dev = g_bridged_device_list; + while (current_dev) { + if ((current_dev->dev_type == ESP_MATTER_BRIDGED_DEVICE_TYPE_RAINMAKER) && current_dev->dev && + (esp_matter::endpoint::get_id(current_dev->dev->endpoint) == matter_endpointid)) { + return current_dev->dev_addr.rainmaker_node_name; + } + current_dev = current_dev->next; + } + return NULL; +} #endif diff --git a/examples/common/app_bridge/app_bridged_device.h b/examples/common/app_bridge/app_bridged_device.h index 8f6f45e16..28cd2a072 100644 --- a/examples/common/app_bridge/app_bridged_device.h +++ b/examples/common/app_bridge/app_bridged_device.h @@ -26,6 +26,8 @@ typedef enum { ESP_MATTER_BRIDGED_DEVICE_TYPE_BLEMESH, /** ESP-NOW */ ESP_MATTER_BRIDGED_DEVICE_TYPE_ESPNOW, + /** Rainmaker */ + ESP_MATTER_BRIDGED_DEVICE_TYPE_RAINMAKER, } app_bridged_device_type_t; /* Bridged Device Address */ @@ -43,6 +45,11 @@ typedef union { struct { uint8_t espnow_macaddr[6]; }; + /** Rainmaker */ + struct { + char rainmaker_node_id[32]; + char rainmaker_node_name[32]; + }; } app_bridged_device_address_t; /* Bridged Device */ @@ -66,6 +73,8 @@ app_bridged_device_address_t app_bridge_blemesh_address(uint16_t blemesh_addr); app_bridged_device_address_t app_bridge_espnow_address(uint8_t espnow_macaddr[6], uint16_t espnow_initiator_attr); +app_bridged_device_address_t app_bridge_rainmaker_address(const char* rainmaker_node_id, const char* rainmaker_node_name); + /** Bridged Device APIs */ app_bridged_device_t *app_bridge_create_bridged_device(node_t *node, uint16_t parent_endpoint_id, uint32_t matter_device_type_id, @@ -97,3 +106,12 @@ app_bridged_device_t *app_bridge_get_device_by_espnow_macaddr(uint8_t espnow_mac uint16_t app_bridge_get_matter_endpointid_by_espnow_macaddr(uint8_t espnow_macaddr[6]); uint8_t* app_bridge_get_espnow_macaddr_by_matter_endpointid(uint16_t matter_endpointid); + +/** Rainmaker Device APIs */ +app_bridged_device_t *app_bridge_get_device_by_rainmaker_node_id(char rainmaker_node_id[32]); + +uint16_t app_bridge_get_matter_endpointid_by_rainmaker_node_id(char rainmaker_node_id[32]); + +char* app_bridge_get_rainmaker_node_id_by_matter_endpointid(uint16_t matter_endpointid); + +char* app_bridge_get_rainmaker_node_name_by_matter_endpointid(uint16_t matter_endpointid);