diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c2b9ba..9278389 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +## 24-Jul-2024 (Command Response Framework and Components update) +- Added support for command response framework + - To demonstrate this, a simple reboot command has been added. Admins can trigger this command from the Insights dashboard (under Node's Settings tab) to reboot the device. + - This feature is currently supported only for nodes claimed from the RainMaker CLI and when MQTT transport is enabled. +- Updated components: + - Updated the rmaker_common submodule. + - Bumped up the component versions for `esp_diagnostics` and `esp_insights`. + ## 23-Feb-2024 (Added support for new metadata structure 2.0) - This change has been introduced for better management of metrics and variables hierarchy diff --git a/FEATURES.md b/FEATURES.md index 0804997..88fa9d4 100644 --- a/FEATURES.md +++ b/FEATURES.md @@ -8,6 +8,7 @@ Below are some of the features offered by ESP Insights: - [Transport Sharing](#transport-sharing) - [Optimising Device-Cloud Communication](#optimising-device-cloud-communication) - [Group Analytics](#group-analytics) +- [Command Response](#command-response) ### Core Dump In case of a firmware crash, the Insights agent captures the core dump information into the flash memory and reports it to the ESP Insights cloud in the subsequent boot-up. This allows you to look at all the crash logs that the devices may be generating in the field. @@ -123,7 +124,7 @@ esp_diag_wifi_metrics_dump(); ``` #### Custom Metrics -It is fairly simple to register your own metrics as well. This can be done as: +It is fairly simple to register your own metrics as well. This can be done as: ``` /* Register a metrics to track room temperature */ @@ -180,7 +181,7 @@ As you may notice, every variable has some metadata associated with it. Some exp ### Transport Sharing The Insights agent supports sending data to the cloud using HTTPS or MQTT (TLS) transport. -Creating a separate TLS session on the device could add to the memory consumption on the device. +Creating a separate TLS session on the device could add to the memory consumption on the device. To avoid this, the Insights agent shares the transport (MQTT) with your cloud agent. Currently the RainMaker cloud agent is supported. This ensures that we reuse the socket/TLS connection without adding a connection memory overhead on your device. @@ -222,3 +223,7 @@ You can change the interval to hour or aggregate to week or a month interval. ![Group Analytics 5](docs/_static/group_analytics_5.png) - List of top nodes having the most number of events and can be drilled down to category level + +### Command Response +ESP Insights leverages RainMaker's command response feature to enable the device to be controllable from the Insights dashboard. +- When enabled, this feature lists options under the Node's Settings tab. These interactive options can be used to control the device. For example, an admin can remotely reboot the device, enable or disable diagnostics collection, or granularly choose to enable or disable features such as Metrics and Variables. diff --git a/components/esp_diagnostics/idf_component.yml b/components/esp_diagnostics/idf_component.yml index cdde797..cf0ee84 100644 --- a/components/esp_diagnostics/idf_component.yml +++ b/components/esp_diagnostics/idf_component.yml @@ -1,4 +1,4 @@ -version: "1.1.0" +version: "1.2.0" description: Diagnostics component used in ESP Insights, which is a remote diagnostics solution to monitor the health of ESP devices in the field. url: https://github.com/espressif/esp-insights/tree/main/components/esp_diagnostics repository: https://github.com/espressif/esp-insights.git diff --git a/components/esp_diagnostics/include/esp_diagnostics.h b/components/esp_diagnostics/include/esp_diagnostics.h index a033143..e23b5b5 100644 --- a/components/esp_diagnostics/include/esp_diagnostics.h +++ b/components/esp_diagnostics/include/esp_diagnostics.h @@ -104,6 +104,8 @@ typedef enum { ESP_DIAG_DATA_TYPE_STR, /*!< Data type string */ ESP_DIAG_DATA_TYPE_IPv4, /*!< Data type IPv4 address */ ESP_DIAG_DATA_TYPE_MAC, /*!< Data type MAC address */ + ESP_DIAG_DATA_TYPE_NULL, /*!< No type */ + ESP_DIAG_DATA_TYPE_MAX, /*!< Max type */ } esp_diag_data_type_t; /** diff --git a/components/esp_insights/CMakeLists.txt b/components/esp_insights/CMakeLists.txt index 41cf2b0..8f0f38a 100644 --- a/components/esp_insights/CMakeLists.txt +++ b/components/esp_insights/CMakeLists.txt @@ -3,6 +3,7 @@ set(srcs "src/esp_insights.c" "src/esp_insights_transport.c" "src/esp_insights_client_data.c" "src/esp_insights_encoder.c" + "src/esp_insights_cmd_resp.c" "src/esp_insights_cbor_decoder.c" "src/esp_insights_cbor_encoder.c") diff --git a/components/esp_insights/Kconfig b/components/esp_insights/Kconfig index 5345155..8f748cb 100644 --- a/components/esp_insights/Kconfig +++ b/components/esp_insights/Kconfig @@ -41,6 +41,16 @@ menu "ESP Insights" endchoice + config ESP_INSIGHTS_CMD_RESP_ENABLED + depends on (ESP_INSIGHTS_ENABLED && ESP_INSIGHTS_TRANSPORT_MQTT) + bool "Enable command response module" + default n + help + Enabling this option adds control of certain insights options remotely. + When enabled, the available configurations, should be shown on the dashboard + and controllable from there. + Please note, the feature is only available with RainMaker MQTT nodes for now. + config ESP_INSIGHTS_TRANSPORT_HTTPS_HOST depends on ESP_INSIGHTS_TRANSPORT_HTTPS string "Insights https host" diff --git a/components/esp_insights/idf_component.yml b/components/esp_insights/idf_component.yml index 64360f1..759cbcb 100644 --- a/components/esp_insights/idf_component.yml +++ b/components/esp_insights/idf_component.yml @@ -1,4 +1,4 @@ -version: "1.1.0" +version: "1.2.0" description: Firmware agent for ESP Insights, which is a remote diagnostics solution to monitor the health of ESP devices in the field. url: https://github.com/espressif/esp-insights/tree/main/components/esp_insights repository: https://github.com/espressif/esp-insights.git @@ -13,7 +13,7 @@ dependencies: version: "~1.0" override_path: '../esp_diag_data_store/' espressif/esp_diagnostics: - version: "~1.1" + version: ">=1.2.0" override_path: '../esp_diagnostics/' espressif/cbor: version: "~0.6" diff --git a/components/esp_insights/include/esp_insights.h b/components/esp_insights/include/esp_insights.h index 140b398..af834e6 100644 --- a/components/esp_insights/include/esp_insights.h +++ b/components/esp_insights/include/esp_insights.h @@ -5,6 +5,7 @@ */ #pragma once + #include #include #include @@ -200,6 +201,53 @@ void esp_insights_disable(void); * @return Pointer to a NULL terminated Node ID string. */ const char *esp_insights_get_node_id(void); + +/** + * @brief Check if insights reporting is enabled + * + * @return true reporting is on + * @return false reporting is off + */ +bool esp_insights_is_reporting_enabled(void); + +/** + * @brief Turn on the Insights reporting + * + * @return esp_err_t ESP_OK on success, apt error otherwise + */ +esp_err_t esp_insights_reporting_enable(); + +/** + * @brief Turn off the Insights repoting + * + * @return esp_err_t ESP_OK on success, apt error otherwise + * @note meta message if changed and the boot message will still be + * sent as this information is critical for Insights working with the + * cloud. You may disable insight completely using esp_insights_disable + */ +esp_err_t esp_insights_reporting_disable(); + +/** + * @brief Encode and parse the command directly using esp-insight's parser + * + * This tests only if the parser is working as expected. + */ +esp_err_t esp_insights_test_cmd_handler(); + +/** + * @brief Enable esp-insights command-response module + * + * This API registers esp-insights command parser which when data is received, + * parses it to filter out insights specific data, modifies configs accordingly, + * and prepares and gives response data to the module + * + * The \ref esp_insights_init takes care of initializing command response and + * enabling the same. In cases where, only esp_insights_enable is called, e.g., + * ESP Rainmaker's app_insights module, user needs to call this API, before or + * after \ref esp_insights_enable + */ +esp_err_t esp_insights_cmd_resp_enable(void); + #ifdef __cplusplus } #endif diff --git a/components/esp_insights/src/esp_insights.c b/components/esp_insights/src/esp_insights.c index ba0bff6..44168cd 100644 --- a/components/esp_insights/src/esp_insights.c +++ b/components/esp_insights/src/esp_insights.c @@ -1,18 +1,20 @@ /* - * SPDX-FileCopyrightText: 2021-2022 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2021-2024 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ +#include +#include +#include + #include #include #include #include -#include -#include + #include #include -#include #include #include #include @@ -29,6 +31,10 @@ #include "esp_insights_encoder.h" #include "esp_insights_cbor_decoder.h" +#ifdef CONFIG_ESP_INSIGHTS_CMD_RESP_ENABLED +#define INSIGHTS_CMD_RESP 1 +#endif + #include #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0) #include @@ -60,14 +66,19 @@ #define SEND_INSIGHTS_META (CONFIG_DIAG_ENABLE_METRICS || CONFIG_DIAG_ENABLE_VARIABLES) +/* TAG for reporting generic miscellaneous insights. Different from ESP_LOGx tag */ #define TAG_DIAG "diag" #define KEY_LOG_WR_FAIL "log_wr_fail" #define DIAG_DATA_STORE_CRC_KEY "rtc_buf_sha" -#define INSIGHTS_NVS_NAMESPACE "storage" +#define INSIGHTS_NVS_NAMESPACE "storage" ESP_EVENT_DEFINE_BASE(INSIGHTS_EVENT); +#ifdef CONFIG_ESP_INSIGHTS_ENABLED + +static const char *TAG = "esp_insights"; /* tag for ESP_LOGx */ + typedef struct esp_insights_entry { esp_rmaker_work_fn_t work_fn; TimerHandle_t timer; @@ -86,6 +97,10 @@ typedef struct { char app_sha256[APP_ELF_SHA256_LEN]; bool data_sent; #if SEND_INSIGHTS_META +#if INSIGHTS_CMD_RESP + bool conf_meta_msg_pending; + uint32_t conf_meta_msg_id; +#endif bool meta_msg_pending; uint32_t meta_msg_id; uint32_t meta_crc; @@ -95,16 +110,18 @@ typedef struct { TimerHandle_t data_send_timer; /* timer to reset data_send_inprogress flag on timeout */ char *node_id; int boot_msg_id; /* To track whether first message is sent or not, -1:failed, 0:success, >0:inprogress */ +#if INSIGHTS_CMD_RESP + int conf_msg_id; +#endif bool init_done; /* insights init done */ bool enabled; /* insights enable is done */ } esp_insights_data_t; -#ifdef CONFIG_ESP_INSIGHTS_ENABLED - -static const char *TAG = "esp_insights"; static esp_insights_data_t s_insights_data; static esp_insights_entry_t *s_periodic_insights_entry; +extern esp_err_t esp_insights_cmd_resp_init(void); + static void esp_insights_first_call(void *priv_data) { if (!priv_data) { @@ -125,6 +142,14 @@ static bool is_insights_active(void) return wifi_connected && s_insights_data.enabled; } +/* Returns true if wifi is connected, false otherwise */ +static bool is_wifi_connected(void) +{ + wifi_ap_record_t ap_info; + bool wifi_connected = esp_wifi_sta_get_ap_info(&ap_info) == ESP_OK; + return wifi_connected; +} + /* This executes in the context of timer task. * * There is a dynamic logic to decide the next instance when the insights @@ -171,16 +196,17 @@ static esp_err_t esp_insights_unregister_periodic_handler(void) { if (s_periodic_insights_entry) { if (s_periodic_insights_entry->timer) { + ESP_LOGI(TAG, "Stopping the periodic timer"); + if (xTimerIsTimerActive(s_periodic_insights_entry->timer) == pdTRUE) { + xTimerStop(s_periodic_insights_entry->timer, portMAX_DELAY); + } ESP_LOGI(TAG, "Deleting the periodic timer"); - if (xTimerDelete(s_periodic_insights_entry->timer, 10) != pdPASS) { - ESP_LOGE(TAG, "Failed to delete the periodic timer"); - } + xTimerDelete(s_periodic_insights_entry->timer, portMAX_DELAY); + s_periodic_insights_entry->timer = NULL; } - if (s_periodic_insights_entry) { - free(s_periodic_insights_entry); - s_periodic_insights_entry = NULL; - } + free(s_periodic_insights_entry); + s_periodic_insights_entry = NULL; } return ESP_OK; @@ -235,6 +261,11 @@ static void data_send_timeout_cb(TimerHandle_t handle) if (s_insights_data.boot_msg_id > 0) { s_insights_data.boot_msg_id = -1; } +#if INSIGHTS_CMD_RESP + if (s_insights_data.conf_msg_id > 0) { + s_insights_data.conf_msg_id = -1; + } +#endif xSemaphoreGive(s_insights_data.data_lock); } @@ -272,6 +303,11 @@ static void insights_event_handler(void* arg, esp_event_base_t event_base, #endif // CONFIG_ESP_INSIGHTS_COREDUMP_ENABLE s_insights_data.boot_msg_id = 0; } +#if INSIGHTS_CMD_RESP + else if (s_insights_data.conf_msg_id > 0 && s_insights_data.conf_msg_id == data->msg_id) { + s_insights_data.conf_msg_id = 0; + } +#endif xSemaphoreGive(s_insights_data.data_lock); } break; @@ -284,6 +320,11 @@ static void insights_event_handler(void* arg, esp_event_base_t event_base, if (s_insights_data.boot_msg_id > 0 && data->msg_id == s_insights_data.boot_msg_id) { s_insights_data.boot_msg_id = -1; } +#if INSIGHTS_CMD_RESP + else if (s_insights_data.conf_msg_id > 0 && data->msg_id == s_insights_data.conf_msg_id) { + s_insights_data.conf_msg_id = -1; + } +#endif xSemaphoreGive(s_insights_data.data_lock); break; default: @@ -345,10 +386,19 @@ static void send_boottime_data(void) } } +#if INSIGHTS_CMD_RESP +static void send_insights_conf_meta(void); +static void send_insights_config(void) +{ + send_insights_conf_meta(); +} +#endif + #if SEND_INSIGHTS_META /* Returns true if ESP Insights metadata CRC is changed */ static bool insights_meta_changed(void) { + return true; uint32_t nvs_crc; uint32_t meta_crc = esp_diag_meta_crc_get(); esp_err_t err = esp_insights_meta_nvs_crc_get(&nvs_crc); @@ -393,6 +443,35 @@ static void send_insights_meta(void) } #endif /* SEND_INSIGHTS_META */ +#if INSIGHTS_CMD_RESP +static void send_insights_conf_meta(void) +{ + uint16_t len = 0; + + memset(s_insights_data.scratch_buf, 0, INSIGHTS_DATA_MAX_SIZE); + len = esp_insights_encode_conf_meta(s_insights_data.scratch_buf, INSIGHTS_DATA_MAX_SIZE, s_insights_data.app_sha256); + if (len == 0) { +#if INSIGHTS_DEBUG_ENABLED + ESP_LOGI(TAG, "No conf metadata to send"); +#endif + return; + } +#if INSIGHTS_DEBUG_ENABLED + ESP_LOGI(TAG, "Insights conf meta data length %d", len); + insights_dbg_dump(s_insights_data.scratch_buf, len); +#endif + int msg_id = esp_insights_transport_data_send(s_insights_data.scratch_buf, len); + if (msg_id > 0) { + xSemaphoreTake(s_insights_data.data_lock, portMAX_DELAY); + s_insights_data.conf_meta_msg_pending = true; + s_insights_data.conf_meta_msg_id = msg_id; + xSemaphoreGive(s_insights_data.data_lock); + } else if (msg_id == 0) { /* sent successfully */ + s_insights_data.conf_meta_msg_pending = false; + } +} +#endif + /* Consider 100 bytes are published and received on cloud but RMAKER_MQTT_EVENT_PUBLISHED * event is not received for 100 bytes. In a mean time 50 bytes are added to the buffer. * When the next time timer expires then old 100 bytes plus new 50 bytes will be published @@ -474,6 +553,13 @@ static void send_insights_data(void) xSemaphoreGive(s_insights_data.data_lock); } +#if INSIGHTS_CMD_RESP +static void __insights_report_config_update(void *priv_data) +{ + send_insights_config(); +} +#endif + static void insights_periodic_handler(void *priv_data) { xSemaphoreTake(s_insights_data.data_lock, portMAX_DELAY); @@ -492,17 +578,34 @@ static void insights_periodic_handler(void *priv_data) #if SEND_INSIGHTS_META if (insights_meta_changed()) { send_insights_meta(); +#if INSIGHTS_CMD_RESP + send_insights_conf_meta(); +#endif } #endif /* SEND_INSIGHTS_META */ + +#if INSIGHTS_CMD_RESP + xSemaphoreTake(s_insights_data.data_lock, portMAX_DELAY); + if (s_insights_data.conf_msg_id == -1) { + xSemaphoreGive(s_insights_data.data_lock); + send_insights_config(); + } else { + xSemaphoreGive(s_insights_data.data_lock); + } +#endif + xSemaphoreTake(s_insights_data.data_lock, portMAX_DELAY); if (s_insights_data.boot_msg_id == -1) { + xSemaphoreGive(s_insights_data.data_lock); send_boottime_data(); + } else { + xSemaphoreGive(s_insights_data.data_lock); } send_insights_data(); } esp_err_t esp_insights_send_data(void) { - if (is_insights_active() == true) { + if (is_wifi_connected() == true) { ESP_LOGI(TAG, "Sending data to cloud"); return esp_rmaker_work_queue_add_task(insights_periodic_handler, NULL); } @@ -510,6 +613,23 @@ esp_err_t esp_insights_send_data(void) return ESP_FAIL; } +#if INSIGHTS_CMD_RESP +void esp_insights_report_config_update(void) +{ + s_insights_data.conf_msg_id = -1; + if (is_wifi_connected() == true) { + /* if wifi is connected, immediately send the report */ + /* if not, this will be reported from periodic handler */ + esp_rmaker_work_queue_add_task(__insights_report_config_update, NULL); + } +} +#else +void esp_insights_report_config_update(void) +{ + ESP_LOGI(TAG, "Not reporting config when cmd_resp is not enabled"); +} +#endif + static void data_store_event_handler(void* arg, esp_event_base_t event_base, int32_t event_id, void* event_data) { @@ -630,7 +750,7 @@ static void variables_init(void) ESP_LOGW(TAG, "Failed to initialize network variables"); } #endif /* CONFIG_DIAG_ENABLE_NETWORK_VARIABLES */ - esp_diag_variable_register("diag", KEY_LOG_WR_FAIL, "Log write fail count", "Diagnostics.Log", ESP_DIAG_DATA_TYPE_UINT); + esp_diag_variable_register(TAG_DIAG, KEY_LOG_WR_FAIL, "Log write fail count", "Diagnostics.Log", ESP_DIAG_DATA_TYPE_UINT); return; } ESP_LOGE(TAG, "Failed to initialize param-values."); @@ -852,6 +972,9 @@ esp_err_t esp_insights_enable(esp_insights_config_t *config) #endif /* CONFIG_DIAG_ENABLE_VARIABLES */ s_insights_data.boot_msg_id = -1; +#if INSIGHTS_CMD_RESP + s_insights_data.conf_msg_id = -1; +#endif s_insights_data.data_send_timer = xTimerCreate("data_send_timer", CLOUD_REPORTING_TIMEOUT_TICKS, pdFALSE, NULL, data_send_timeout_cb); if (!s_insights_data.data_send_timer) { @@ -927,6 +1050,11 @@ esp_err_t esp_insights_init(esp_insights_config_t *config) } s_insights_data.init_done = true; + + err = esp_insights_cmd_resp_init() || esp_insights_cmd_resp_enable(); + if (err != ESP_OK) { /* device can keep working neverthless */ + ESP_LOGE(TAG, "Failed to enable insights_cmd_resp"); + } return ESP_OK; init_err: if (s_insights_data.node_id) { diff --git a/components/esp_insights/src/esp_insights_cbor_decoder.c b/components/esp_insights/src/esp_insights_cbor_decoder.c index dde03c3..3981da0 100644 --- a/components/esp_insights/src/esp_insights_cbor_decoder.c +++ b/components/esp_insights/src/esp_insights_cbor_decoder.c @@ -11,6 +11,7 @@ #include #include +#include "esp_insights_cbor_decoder.h" static const char *TAG = "insight_cbor_dec"; @@ -281,3 +282,101 @@ esp_err_t esp_insights_cbor_decode_dump(const uint8_t *buffer, int len) return ESP_OK; } + +bool esp_insights_cbor_decoder_at_end(cbor_parse_ctx_t *ctx) +{ + return cbor_value_at_end(&ctx->it[ctx->curr_itr]); +} + +esp_err_t esp_insights_cbor_decoder_advance(cbor_parse_ctx_t *ctx) +{ + if (CborNoError == cbor_value_advance(&ctx->it[ctx->curr_itr])) { + return ESP_OK; + } + return ESP_FAIL; +} + +CborType esp_insights_cbor_decode_get_value_type(cbor_parse_ctx_t *ctx) +{ + return cbor_value_get_type(&ctx->it[ctx->curr_itr]); +} + +char *esp_insights_cbor_decoder_get_string(CborValue *val) +{ + CborError ret = CborNoError; + char *buf = NULL; + size_t n; + if (cbor_value_get_type(val) != CborTextStringType) { + return NULL; + } + ret = cbor_value_dup_text_string(val, &buf, &n, val); + if (ret == CborNoError) { + return (char *) buf; + } + return NULL; +} + +esp_err_t esp_insights_cbor_decoder_enter_container(cbor_parse_ctx_t *ctx) +{ + CborError ret = CborNoError; + int curr_itr = ctx->curr_itr; + if (curr_itr >= INS_CBOR_MAX_DEPTH) { + ESP_LOGE(TAG, "Cannot parse depth more than %d", INS_CBOR_MAX_DEPTH); + return ESP_FAIL; + } + ret = cbor_value_enter_container(&ctx->it[curr_itr], &ctx->it[curr_itr + 1]); + if (ret != CborNoError) { + ESP_LOGE(TAG, "error entering container"); + return ESP_FAIL; + } + if (esp_insights_cbor_decoder_at_end(ctx)) { + return ESP_FAIL; + } + ctx->curr_itr++; + + return ESP_OK; +} + +esp_err_t esp_insights_cbor_decoder_exit_container(cbor_parse_ctx_t *ctx) +{ + CborError ret = CborNoError; + if (ctx->curr_itr <= 0) { + ESP_LOGE(TAG, "cannot exit, already at top"); + return ESP_FAIL; + } + ctx->curr_itr--; + int curr_itr = ctx->curr_itr; + ret = cbor_value_leave_container(&ctx->it[curr_itr], &ctx->it[curr_itr + 1]); + if (ret != CborNoError) { + ESP_LOGE(TAG, "error leaving container"); + return ESP_FAIL; + } + return ESP_OK; +} + +esp_err_t esp_insights_cbor_decoder_done(cbor_parse_ctx_t *ctx) +{ + if (ctx) { + free(ctx); + } + return ESP_OK; +} + +cbor_parse_ctx_t *esp_insights_cbor_decoder_start(const uint8_t *buffer, int len) +{ + if (!buffer || len == 0) { + return NULL; + } + cbor_parse_ctx_t *ctx = calloc(1, sizeof(cbor_parse_ctx_t)); + if (!ctx) { + ESP_LOGE(TAG, "failed to allocate cbor ctx"); + return NULL; + } + CborParser root_parser = ctx->root_parser; + CborValue *it = &ctx->it[0]; + if (cbor_parser_init(buffer, len, 0, &root_parser, it) != CborNoError) { + ESP_LOGE(TAG, "Error initializing cbor parser"); + return NULL; + } + return ctx; +} diff --git a/components/esp_insights/src/esp_insights_cbor_decoder.h b/components/esp_insights/src/esp_insights_cbor_decoder.h index abbfc40..cbeb6af 100644 --- a/components/esp_insights/src/esp_insights_cbor_decoder.h +++ b/components/esp_insights/src/esp_insights_cbor_decoder.h @@ -15,6 +15,30 @@ #include +#include + +#define INS_CBOR_MAX_DEPTH 12 // depth to which we can traverse + +typedef struct cbor_parse_ctx { + CborParser root_parser; + CborValue it[INS_CBOR_MAX_DEPTH + 1]; + int curr_itr; +} cbor_parse_ctx_t; + +cbor_parse_ctx_t *esp_insights_cbor_decoder_start(const uint8_t *buffer, int len); +esp_err_t esp_insights_cbor_decoder_enter_and_check_value(cbor_parse_ctx_t *ctx, const char *val); +bool esp_insights_cbor_decoder_at_end(cbor_parse_ctx_t *ctx); + +esp_err_t esp_insights_cbor_decoder_advance(cbor_parse_ctx_t *ctx); +CborType esp_insights_cbor_decode_get_value_type(cbor_parse_ctx_t *ctx); +char *esp_insights_cbor_decoder_get_string(CborValue *val); + +esp_err_t esp_insights_cbor_decoder_enter_container(cbor_parse_ctx_t *ctx); +esp_err_t esp_insights_cbor_decoder_exit_container(cbor_parse_ctx_t *ctx); + +/* Do cleanups if any */ +esp_err_t esp_insights_cbor_decoder_done(cbor_parse_ctx_t *ctx); + /** * @brief decodes a cbor message and prints into json format * diff --git a/components/esp_insights/src/esp_insights_cbor_encoder.c b/components/esp_insights/src/esp_insights_cbor_encoder.c index d750c4b..68b2c68 100644 --- a/components/esp_insights/src/esp_insights_cbor_encoder.c +++ b/components/esp_insights/src/esp_insights_cbor_encoder.c @@ -26,11 +26,27 @@ #define METRICS_PATH_VALUE "M" #define VARIABLES_PATH_VALUE "P" -static CborEncoder s_encoder, s_result_map, s_diag_map, s_diag_data_map; +static CborEncoder s_encoder, s_result_map, s_diag_map, s_diag_data_map, s_diag_conf_map; static CborEncoder s_meta_encoder, s_meta_result_map, s_diag_meta_map, s_diag_meta_data_map; +#define CBOR_ENC_MAX_CBS 10 +static struct cbor_encoder_data { + insights_cbor_encoder_cb_t cb[CBOR_ENC_MAX_CBS]; + int cb_cnt; +} s_priv_data; + static inline void _cbor_encode_meta_hdr(CborEncoder *hdr_map, const rtc_store_meta_header_t *hdr); +esp_err_t esp_insights_cbor_encoder_register_meta_cb(insights_cbor_encoder_cb_t cb) +{ + if (s_priv_data.cb_cnt == CBOR_ENC_MAX_CBS) { + return ESP_ERR_NO_MEM; + } + ESP_LOGV(TAG, "Registering callback %p", cb); + s_priv_data.cb[s_priv_data.cb_cnt++] = cb; + return ESP_OK; +} + void esp_insights_cbor_encode_diag_begin(void *data, size_t data_size, const char *version) { cbor_encoder_init(&s_encoder, data, data_size, 0); @@ -65,11 +81,22 @@ void esp_insights_cbor_encode_diag_data_begin(void) cbor_encoder_create_map(&s_diag_map, &s_diag_data_map, CborIndefiniteLength); } +void esp_insights_cbor_encode_diag_conf_data_begin(void) +{ + cbor_encode_text_stringz(&s_diag_data_map, "configs"); + cbor_encoder_create_array(&s_diag_data_map, &s_diag_conf_map, CborIndefiniteLength); +} + void esp_insights_cbor_encode_diag_data_end(void) { cbor_encoder_close_container(&s_diag_map, &s_diag_data_map); } +void esp_insights_cbor_encode_diag_conf_data_end(void) +{ + cbor_encoder_close_container(&s_diag_data_map, &s_diag_conf_map); +} + #if CONFIG_ESP_INSIGHTS_COREDUMP_ENABLE void esp_insights_cbor_encode_diag_crash(esp_core_dump_summary_t *summary) { @@ -681,6 +708,25 @@ void esp_insights_cbor_encode_meta_data_end(void) cbor_encoder_close_container(&s_diag_meta_map, &s_diag_meta_data_map); } +void esp_insights_cbor_encode_conf_meta_data_begin(void) +{ + cbor_encode_text_stringz(&s_diag_meta_map, "data"); + cbor_encoder_create_map(&s_diag_meta_map, &s_diag_meta_data_map, CborIndefiniteLength); +#ifdef NEW_META_STRUCT + for (int i = 0; i < s_priv_data.cb_cnt; i++) { + if (s_priv_data.cb[i]) { + s_priv_data.cb[i] (&s_diag_meta_data_map, INSIGHTS_MSG_TYPE_META); + } + } +#endif +} + +/** Vikram: remove? */ +void esp_insights_cbor_encode_conf_meta_data_end(void) +{ + cbor_encoder_close_container(&s_diag_meta_map, &s_diag_meta_data_map); +} + #if CONFIG_DIAG_ENABLE_METRICS static void encode_metrics_meta_element(CborEncoder *map, const esp_diag_metrics_meta_t *metrics) { diff --git a/components/esp_insights/src/esp_insights_cbor_encoder.h b/components/esp_insights/src/esp_insights_cbor_encoder.h index 0470b42..8a47e29 100644 --- a/components/esp_insights/src/esp_insights_cbor_encoder.h +++ b/components/esp_insights/src/esp_insights_cbor_encoder.h @@ -20,6 +20,28 @@ #define NEW_META_STRUCT 1 #endif +typedef enum { + INSIGHTS_MSG_TYPE_META, + INSIGHTS_MSG_TYPE_DATA +} insights_msg_type_t; + +/** + * @brief cbor encoder callback + * + * @param map parent map to which new data will be encoded + * @param type information type to be collected + */ +typedef void (*insights_cbor_encoder_cb_t) (CborEncoder *map, insights_msg_type_t type); + +/** + * @brief register a meta collection callback + * + * @param cb callback of type \ref insights_cbor_encoder_cb_t + * + * @return ESP_OK on success, appropriate error otherwise + */ +esp_err_t esp_insights_cbor_encoder_register_meta_cb(insights_cbor_encoder_cb_t cb); + void esp_insights_cbor_encode_diag_begin(void *data, size_t data_size, const char *version); void esp_insights_cbor_encode_diag_data_begin(void); void esp_insights_cbor_encode_diag_boot_info(esp_diag_device_info_t *device_info); @@ -53,3 +75,14 @@ void esp_insights_cbor_encode_meta_variables(const esp_diag_variable_meta_t *var #endif /* CONFIG_DIAG_ENABLE_VARIABLES */ void esp_insights_cbor_encode_meta_data_end(void); size_t esp_insights_cbor_encode_meta_end(void *data); + + +/* For encoding conf data */ +void esp_insights_cbor_encode_conf_meta_begin(void *data, size_t data_size, const char *version, const char *sha256); +void esp_insights_cbor_encode_conf_meta_data_begin(void); +void esp_insights_cbor_encode_conf_meta_data_end(void); +size_t esp_insights_cbor_encode_conf_meta_end(void *data); + +void esp_insights_cbor_encode_diag_conf_data_begin(void); +void esp_insights_cbor_encode_diag_conf_data_end(void); +void esp_insights_cbor_encode_diag_conf_data(void); diff --git a/components/esp_insights/src/esp_insights_cmd_resp.c b/components/esp_insights/src/esp_insights_cmd_resp.c new file mode 100644 index 0000000..b68518d --- /dev/null +++ b/components/esp_insights/src/esp_insights_cmd_resp.c @@ -0,0 +1,679 @@ +/* + * SPDX-FileCopyrightText: 2022-2024 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @file esp_insights_cmd_resp.c + * @brief this file contains glue between received data parsing for insights config, + * which eventually calls the commands it has registered to + */ + +#include +#include + +static const char *TAG = "insights_cmd_resp"; + +#ifdef CONFIG_ESP_INSIGHTS_CMD_RESP_ENABLED +#include + +#include /* for nodeID */ +#include +#include + +#include "esp_insights_internal.h" +#include "esp_insights_cbor_decoder.h" +#include "esp_insights_cbor_encoder.h" + +#define INS_CONF_STR "config" +#define RMAKER_CFG_TOPIC_SUFFIX "config" +#define TO_NODE_TOPIC_SUFFIX "to-node" +#define FROM_NODE_TOPIC_SUFFIX "from-node" + +/** This is a common command identifier for Insights and then Insights internally has its own commands and their handlers. + */ +#define INSIGHTS_CONF_CMD 0x101 + +/* depth is dictated by cmd_depth */ +#define MAX_CMD_DEPTH 10 +#define CMD_STORE_SIZE 10 +#define SCRATCH_BUF_SIZE (1 * 1024) + +typedef esp_err_t (*esp_insights_cmd_cb_t)(const void *data, size_t data_len, const void *priv); + +typedef struct { + const char *cmd[MAX_CMD_DEPTH]; /* complete path of the command */ + int depth; + esp_insights_cmd_cb_t cb; /* callback function */ + void *prv_data; /* pointer to the private data */ +} generic_cmd_t; + +typedef struct { + generic_cmd_t cmd_store[CMD_STORE_SIZE]; + int cmd_cnt; /* total registered commands */ + uint8_t *scratch_buf; + bool enabled; + bool init_done; +} insights_cmd_resp_data_t; + +static insights_cmd_resp_data_t s_cmd_resp_data; +static bool reboot_report_pending = false; + +static esp_err_t reboot_cmd_handler(const void *data, size_t data_len, const void *prv_data) +{ + reboot_report_pending = true; + ESP_LOGI(TAG, "rebooting in 5 seconds..."); + esp_rmaker_reboot(5); + return ESP_OK; +} + +static void __collect_reboot_meta(CborEncoder *map) +{ + cbor_encode_text_stringz(map, "reboot"); /* name of the config */ + CborEncoder conf_map, conf_data_map; + cbor_encoder_create_map(map, &conf_map, CborIndefiniteLength); + cbor_encode_text_stringz(&conf_map, "c"); /* denotes this to be config */ + cbor_encoder_create_map(&conf_map, &conf_data_map, CborIndefiniteLength); + cbor_encode_text_stringz(&conf_data_map, "type"); + cbor_encode_uint(&conf_data_map, ESP_DIAG_DATA_TYPE_NULL); /* data_type */ + cbor_encoder_close_container(&conf_map, &conf_data_map); + cbor_encoder_close_container(map, &conf_map); +} + +static void __collect_reboot_data(CborEncoder *map) +{ + CborEncoder conf_map, key_arr; + cbor_encoder_create_map(map, &conf_map, CborIndefiniteLength); + cbor_encode_text_stringz(&conf_map, "n"); + cbor_encoder_create_array(&conf_map, &key_arr, CborIndefiniteLength); + cbor_encode_text_stringz(&key_arr, "reboot"); + cbor_encoder_close_container(&conf_map, &key_arr); + cbor_encode_text_stringz(&conf_map, "t"); + cbor_encode_uint(&conf_map, esp_diag_timestamp_get()); + cbor_encoder_close_container(map, &conf_map); +} + +static void esp_insights_cbor_reboot_msg_cb(CborEncoder *map, insights_msg_type_t type) +{ + if (type == INSIGHTS_MSG_TYPE_META) { + __collect_reboot_meta(map); + } else if (reboot_report_pending) { + __collect_reboot_data(map); + reboot_report_pending = false; + } +} + +esp_err_t esp_insights_cmd_resp_register_cmd(esp_insights_cmd_cb_t cb, void *prv_data, int cmd_depth, ...) +{ + int idx = s_cmd_resp_data.cmd_cnt; + + va_list valist; + va_start(valist, cmd_depth); + for (int i = 0; i < cmd_depth; i++) { + s_cmd_resp_data.cmd_store[idx].cmd[i] = va_arg(valist, char *); + } + /* clean memory reserved for valist */ + va_end(valist); + + s_cmd_resp_data.cmd_store[idx].depth = cmd_depth; + s_cmd_resp_data.cmd_store[idx].prv_data = prv_data; + s_cmd_resp_data.cmd_store[idx].cb = cb; + s_cmd_resp_data.cmd_cnt += 1; + + return ESP_OK; +} + +static esp_err_t insights_cmd_resp_search_execute_cmd_store(char **cmd_tree, int cmd_depth) +{ + for(int i = 0; i< s_cmd_resp_data.cmd_cnt; i++) { + if (cmd_depth == s_cmd_resp_data.cmd_store[i].depth) { + bool match_found = true; + /* the command depth matches, now go for whole path */ + for (int j = 0; j < cmd_depth; j++) { + if (strcmp(cmd_tree[j], s_cmd_resp_data.cmd_store[i].cmd[j]) != 0) { + match_found = false; + break; /* break at first mismatch */ + } + } + if (match_found) { + ESP_LOGI(TAG, "match found in cmd_store... Executing the callback"); + s_cmd_resp_data.cmd_store[i].cb(NULL, 0, s_cmd_resp_data.cmd_store[i].prv_data); + return ESP_OK; + } + } + } + return ESP_ERR_NOT_FOUND; +} + +static void insights_cmd_parser_clear_cmd_tree(char *cmd_tree[]) +{ + for (int i = 0; i < MAX_CMD_DEPTH; i++) { + if (cmd_tree[i]) { + free(cmd_tree[i]); + cmd_tree[i] = NULL; + } else { + return; + } + } +} + +static void insights_cmd_parser_add_cmd_to_tree(char * cmd_tree[], char *cmd, int pos) +{ + ESP_LOGV(TAG, "Adding %s to command path\n", cmd); + /* free depth already consumed */ + for (int i = pos; i < MAX_CMD_DEPTH; i++) { + if (cmd_tree[i]) { + free(cmd_tree[i]); + cmd_tree[i] = NULL; + } else { + break; + } + } + + /* insert at the position */ + cmd_tree[pos] = cmd; +} + +static void insights_cmd_parser_print_cmd_tree(const char *cmd_tree[], int depth) +{ + if (depth <= 0) { + ESP_LOGI(TAG, "No command found to be printed"); + return; + } + printf("The command is: "); + for (int i = 0; i < depth - 1; i++) { + if (cmd_tree[i]) { + printf("%s > ", cmd_tree[i]); + } else { + printf("\ncouldn't find complete depth\n"); + return; + } + } + if (cmd_tree[depth - 1]) { + printf("%s\n", cmd_tree[depth - 1]); + } else { + printf("\ncouldn't find complete depth\n"); + } +} + +#define MAX_BUFFER_SIZE 100 + +/* extract top level fields from CBOR and check for sanity */ +esp_err_t check_top_fields_from_cbor(const uint8_t *cbor_data, size_t cbor_data_len) +{ + CborParser parser; + CborValue map, value; + CborError err; + + /* Initialize the parser */ + cbor_parser_init(cbor_data, cbor_data_len, 0, &parser, &map); + + /* Parse the top-level map */ + if (!cbor_value_is_map(&map)) { + ESP_LOGE(TAG, "Invalid CBOR format: top-level map expected"); + return ESP_FAIL; + } + + if (CborNoError != cbor_value_enter_container(&map, &value)) { + ESP_LOGE(TAG, "Error entering the container"); + return ESP_FAIL; + } + + /* Iterate through the map and extract the desired fields */ + while (!cbor_value_at_end(&value)) { + CborValue map_key; + + /* Check the map key and extract the corresponding field */ + if (cbor_value_is_text_string(&value)) { + char buffer[MAX_BUFFER_SIZE]; + size_t buffer_size = sizeof(buffer); + err = cbor_value_copy_text_string(&value, buffer, &buffer_size, &map_key); + if (err != CborNoError) { + ESP_LOGE(TAG, "CBOR value copy text string failed: %d", err); + return ESP_FAIL; + } + + buffer_size = sizeof(buffer); + if (strcmp(buffer, "ver") == 0) { + if (cbor_value_is_text_string(&map_key)) { + err = cbor_value_copy_text_string(&map_key, buffer, &buffer_size, &map_key); + if (err != CborNoError) { + ESP_LOGE(TAG, "CBOR value copy text string failed: %d", err); + return ESP_FAIL; + } + ESP_LOGI(TAG, "ver: %s", buffer); + } else { + ESP_LOGE(TAG, "Invalid CBOR format: text string expected as ver key"); + } + } else if (strcmp(buffer, "ts") == 0) { + CborType _type = cbor_value_get_type(&map_key); + ESP_LOGI(TAG, "ts is of type %d", _type); + } else if (strcmp(buffer, "sha256") == 0) { + if (cbor_value_is_text_string(&map_key)) { + err = cbor_value_copy_text_string(&map_key, buffer, &buffer_size, &map_key); + if (err != CborNoError) { + ESP_LOGE(TAG, "CBOR value copy text string failed: %d", err); + return ESP_FAIL; + } + ESP_LOGI(TAG, "sha256: %s", buffer); + } else { + ESP_LOGE(TAG, "Invalid CBOR format: text string expected as sha256 key"); + } + } else if (strcmp(buffer, INS_CONF_STR) == 0) { + /* Nothing to do here */ + } + + /* skip this string now */ + err = cbor_value_advance(&value); + if (err != CborNoError) { + ESP_LOGE(TAG, "CBOR value advance failed: %d", err); + return ESP_FAIL; + } + } + + /* advance the value */ + CborType _type = cbor_value_get_type(&value); + ESP_LOGI(TAG, "Skipping type %d", _type); + err = cbor_value_advance(&value); + if (err != CborNoError) { + ESP_LOGE(TAG, "CBOR value advance failed: %d", err); + return ESP_FAIL; + } + } + return ESP_OK; +} + +/** + * @brief Get next config entry from data + * + */ +static esp_err_t esp_insights_cmd_resp_parse_one_entry(cbor_parse_ctx_t *ctx) +{ + char *tmp_str = NULL; + int cmd_depth = 0; + bool cmd_value_b; + size_t val_sz = 0; + esp_err_t ret = ESP_OK; + char *cmd_tree[MAX_CMD_DEPTH] = {0, }; + + /* parse till we are at the end */ + while (!esp_insights_cbor_decoder_at_end(ctx)) { + CborType type = esp_insights_cbor_decode_get_value_type(ctx); + CborValue *it = &ctx->it[ctx->curr_itr]; + switch (type) + { + case CborTextStringType: + tmp_str = esp_insights_cbor_decoder_get_string(it); + if (!tmp_str) { + return ESP_FAIL; + } + ESP_LOGI(TAG, "found \"%s\"", tmp_str); + if (strcmp(tmp_str, "n") == 0) { + CborType _type = esp_insights_cbor_decode_get_value_type(ctx); + if (_type == CborArrayType) { + if (esp_insights_cbor_decoder_enter_container(ctx) == ESP_OK) { + while(!esp_insights_cbor_decoder_at_end(ctx)) { + char *buffer = esp_insights_cbor_decoder_get_string(&ctx->it[ctx->curr_itr]); + if (!buffer) { + ESP_LOGE(TAG, "Invalid entry"); + break; + } + insights_cmd_parser_add_cmd_to_tree(cmd_tree, buffer, cmd_depth); + ++cmd_depth; + } + + insights_cmd_parser_print_cmd_tree((const char **) cmd_tree, cmd_depth); + esp_insights_cbor_decoder_exit_container(ctx); + } + } else { + ESP_LOGE(TAG, "A config name must be of array type"); + } + } else if (strcmp(tmp_str, "v") == 0) { + /* decide the type of the value first and then fetch it (bool for now) */ + esp_diag_data_type_t type = ESP_DIAG_DATA_TYPE_BOOL; + /* get the value in val */ + switch (type) + { + case ESP_DIAG_DATA_TYPE_BOOL: + cbor_value_get_boolean(it, &cmd_value_b); + val_sz = 1; + break; + default: + val_sz = 0; + break; + } + cbor_value_advance_fixed(it); + } else { + esp_insights_cbor_decoder_advance(ctx); + } + + if (tmp_str) { + free(tmp_str); + tmp_str = NULL; + } + break; + + default: + esp_insights_cbor_decoder_advance(ctx); + break; + } + } + + insights_cmd_resp_search_execute_cmd_store(cmd_tree, cmd_depth); + insights_cmd_parser_clear_cmd_tree(cmd_tree); + + return ret; +} + +static esp_err_t esp_insights_cmd_resp_parse_execute(cbor_parse_ctx_t *ctx) +{ + esp_insights_cbor_decoder_enter_container(ctx); + int cmd_cnt = 0; + /* iterate till we are done with current container */ + while (!esp_insights_cbor_decoder_at_end(ctx)) { + esp_insights_cbor_decoder_enter_container(ctx); + esp_insights_cmd_resp_parse_one_entry(ctx); + esp_insights_cbor_decoder_exit_container(ctx); + cmd_cnt++; + } + if (cmd_cnt) { + esp_insights_report_config_update(); + } + esp_insights_cbor_decoder_exit_container(ctx); + ESP_LOGI(TAG, "parsed and executed %d commands", cmd_cnt); + return ESP_OK; +} + +static esp_err_t esp_insights_cmd_resp_iterate_to_cmds_array(cbor_parse_ctx_t *ctx) +{ + /* layer one */ + if (esp_insights_cbor_decoder_at_end(ctx)) { + ESP_LOGW(TAG, "invalid cmd_resp payload"); + return ESP_FAIL; + } + + if (esp_insights_cbor_decode_get_value_type(ctx) != CborMapType) { + ESP_LOGE(TAG, "invalid cmd_resp payload. line (%d)", __LINE__); + return ESP_FAIL; + } + + if (esp_insights_cbor_decoder_enter_container(ctx) == ESP_OK) { + while(!esp_insights_cbor_decoder_at_end(ctx)) { + char *buffer = esp_insights_cbor_decoder_get_string(&ctx->it[ctx->curr_itr]); + + if (!buffer) { + ESP_LOGE(TAG, "Parsing problem..."); + return ESP_FAIL; + } + + if (strcmp(buffer, INS_CONF_STR) == 0) { + free(buffer); + buffer = NULL; + ESP_LOGI(TAG, "Found commands array:"); + return ESP_OK; + } else { + ESP_LOGI(TAG, "skipping token %s", buffer); + } + free(buffer); + buffer = NULL; + /* skip the value and find next for INS_CONF_STR */ + esp_insights_cbor_decoder_advance(ctx); + } + ESP_LOGI(TAG, "failed to find a `config` array!"); + return ESP_FAIL; + } + + ESP_LOGI(TAG, "invalid payload type"); + return ESP_FAIL; +} + +static char resp_data[100]; /* FIXME: assumed that the response size is < 100 bytes */ +/** This is a common handler registered with the lower layer command response framework. + * It parses the received CBOR payload aqnd redirects to appropriate internal insights command callback. */ +static esp_err_t esp_insights_cmd_handler(const void *in_data, size_t in_len, void **out_data, + size_t *out_len, esp_rmaker_cmd_ctx_t *ctx, void *priv) +{ + esp_err_t ret = ESP_FAIL; + if (in_data == NULL || in_len == 0) { + ESP_LOGE(TAG, "No data received"); + return ESP_FAIL; + } + +#if CONFIG_ESP_INSIGHTS_DEBUG_ENABLED + ESP_LOG_BUFFER_HEX_LEVEL(TAG, in_data, in_len, ESP_LOG_INFO); + ESP_LOGI(TAG, "Received command, len %d: ", in_len); + esp_insights_cbor_decode_dump((uint8_t *) in_data, in_len); +#endif + + if (ESP_FAIL == check_top_fields_from_cbor(in_data, in_len)) { + snprintf(resp_data, sizeof(resp_data), "{\"status\":\"payload error\"}"); + goto out; + } + cbor_parse_ctx_t *cbor_ctx = esp_insights_cbor_decoder_start(in_data, in_len); + if (cbor_ctx) { + ret = esp_insights_cmd_resp_iterate_to_cmds_array(cbor_ctx); + if (ret == ESP_OK) { + esp_insights_cmd_resp_parse_execute(cbor_ctx); /* it is okay if this is empty */ + snprintf(resp_data, sizeof(resp_data), "{\"status\":\"success\"}"); + } else { + snprintf(resp_data, sizeof(resp_data), "{\"status\":\"payload error\"}"); + } + esp_insights_cbor_decoder_done(cbor_ctx); + } else { + snprintf(resp_data, sizeof(resp_data), "{\"status\":\"internal error\"}"); + } +out: + *out_data = resp_data; + *out_len = strlen(resp_data); + return ret; +} + +const uint8_t test_buf0[] = { + 0xA1, 0x68, 0x64, 0x69, 0x61, 0x67, 0x6D, 0x65, 0x74, 0x61, 0xA4, 0x63, 0x76, 0x65, 0x72, 0x63, + 0x31, 0x2E, 0x31, 0x62, 0x74, 0x73, 0x1B, 0x00, 0x05, 0xFB, 0x53, 0xF9, 0x42, 0x0C, 0x39, 0x66, + 0x73, 0x68, 0x61, 0x32, 0x35, 0x36, 0x70, 0x39, 0x61, 0x65, 0x38, 0x30, 0x36, 0x36, 0x61, 0x30, + 0x37, 0x65, 0x38, 0x37, 0x38, 0x66, 0x64, 0x64, 0x64, 0x61, 0x74, 0x61, 0xA1, 0x61, 0x64, 0xA2, + 0x67, 0x65, 0x6E, 0x61, 0x62, 0x6C, 0x65, 0x64, 0xA1, 0x61, 0x63, 0xA2, 0x64, 0x74, 0x79, 0x70, + 0x65, 0x00, 0x61, 0x76, 0xF5, 0x67, 0x6D, 0x65, 0x74, 0x72, 0x69, 0x63, 0x73, 0xA1, 0x61, 0x64, + 0xA2, 0x67, 0x65, 0x6E, 0x61, 0x62, 0x6C, 0x65, 0x64, 0xA1, 0x61, 0x63, 0xA2, 0x64, 0x74, 0x79, + 0x70, 0x65, 0x00, 0x61, 0x76, 0xF5, 0x64, 0x68, 0x65, 0x61, 0x70, 0xA1, 0x61, 0x64, 0xA2, 0x67, + 0x65, 0x6E, 0x61, 0x62, 0x6C, 0x65, 0x64, 0xA1, 0x61, 0x63, 0xA2, 0x64, 0x74, 0x79, 0x70, 0x65, + 0x00, 0x61, 0x76, 0xF5, 0x6A, 0x61, 0x6C, 0x6C, 0x6F, 0x63, 0x5F, 0x66, 0x61, 0x69, 0x6C, 0xA1, + 0x61, 0x64, 0xA1, 0x67, 0x65, 0x6E, 0x61, 0x62, 0x6C, 0x65, 0x64, 0xA1, 0x61, 0x63, 0xA2, 0x64, + 0x74, 0x79, 0x70, 0x65, 0x00, 0x61, 0x76, 0xF5 +}; + +/** Test vector 2 */ +const uint8_t test_buf1[] = { + 0xA1, 0x68, 0x64, 0x69, 0x61, 0x67, 0x6D, 0x65, 0x74, 0x61, 0xA4, 0x63, 0x76, 0x65, 0x72, 0x63, + 0x31, 0x2E, 0x31, 0x62, 0x74, 0x73, 0x1B, 0x00, 0x05, 0xFB, 0x53, 0xF9, 0x42, 0x0C, 0x39, 0x66, + 0x73, 0x68, 0x61, 0x32, 0x35, 0x36, 0x70, 0x39, 0x61, 0x65, 0x38, 0x30, 0x36, 0x36, 0x61, 0x30, + 0x37, 0x65, 0x38, 0x37, 0x38, 0x66, 0x64, 0x64, 0x64, 0x61, 0x74, 0x61, 0xA1, 0x61, 0x64, 0xA3, + 0x67, 0x65, 0x6E, 0x61, 0x62, 0x6C, 0x65, 0x64, 0xA1, 0x61, 0x63, 0xA2, 0x64, 0x74, 0x79, 0x70, + 0x65, 0x00, 0x61, 0x76, 0xF5, 0x67, 0x6D, 0x65, 0x74, 0x72, 0x69, 0x63, 0x73, 0xA1, 0x61, 0x64, + 0xA2, 0x67, 0x65, 0x6E, 0x61, 0x62, 0x6C, 0x65, 0x64, 0xA1, 0x61, 0x63, 0xA2, 0x64, 0x74, 0x79, + 0x70, 0x65, 0x00, 0x61, 0x76, 0xF5, 0x64, 0x68, 0x65, 0x61, 0x70, 0xA1, 0x61, 0x64, 0xA3, 0x67, + 0x65, 0x6E, 0x61, 0x62, 0x6C, 0x65, 0x64, 0xA1, 0x61, 0x63, 0xA2, 0x64, 0x74, 0x79, 0x70, 0x65, + 0x00, 0x61, 0x76, 0xF5, 0x6A, 0x61, 0x6C, 0x6C, 0x6F, 0x63, 0x5F, 0x66, 0x61, 0x69, 0x6C, 0xA1, + 0x61, 0x64, 0xA1, 0x67, 0x65, 0x6E, 0x61, 0x62, 0x6C, 0x65, 0x64, 0xA1, 0x61, 0x63, 0xA2, 0x64, + 0x74, 0x79, 0x70, 0x65, 0x00, 0x61, 0x76, 0xF5, 0x64, 0x66, 0x72, 0x65, 0x65, 0xA1, 0x61, 0x64, + 0xA1, 0x67, 0x65, 0x6E, 0x61, 0x62, 0x6C, 0x65, 0x64, 0xA1, 0x61, 0x63, 0xA2, 0x64, 0x74, 0x79, + 0x70, 0x65, 0x00, 0x61, 0x76, 0xF5, 0x66, 0x70, 0x61, 0x72, 0x61, 0x6D, 0x73, 0xA1, 0x61, 0x64, + 0xA2, 0x67, 0x65, 0x6E, 0x61, 0x62, 0x6C, 0x65, 0x64, 0xA1, 0x61, 0x63, 0xA2, 0x64, 0x74, 0x79, + 0x70, 0x65, 0x00, 0x61, 0x76, 0xF5, 0x64, 0x77, 0x69, 0x66, 0x69, 0xA1, 0x61, 0x64, 0xA1, 0x67, + 0x65, 0x6E, 0x61, 0x62, 0x6C, 0x65, 0x64, 0xA1, 0x61, 0x63, 0xA2, 0x64, 0x74, 0x79, 0x70, 0x65, + 0x00, 0x61, 0x76, 0xF5 +}; + +/* test vec 2 */ +const uint8_t test_buf2[] = { + 0xA4, 0x63, 0x76, 0x65, 0x72, 0x63, 0x32, 0x2E, 0x30, 0x62, 0x74, 0x73, + 0x1B, 0x00, 0x05, 0xC5, 0x85, 0x48, 0x4E, 0xCF, 0x80, 0x66, 0x73, 0x68, + 0x61, 0x32, 0x35, 0x36, 0x70, 0x37, 0x63, 0x32, 0x65, 0x64, 0x62, 0x31, + 0x39, 0x34, 0x39, 0x36, 0x33, 0x39, 0x61, 0x37, 0x33, 0x66, 0x63, 0x6F, + 0x6E, 0x66, 0x69, 0x67, 0x82, 0xA2, 0x61, 0x6E, 0x83, 0x64, 0x68, 0x65, + 0x61, 0x70, 0x6A, 0x61, 0x6C, 0x6C, 0x6F, 0x63, 0x5F, 0x66, 0x61, 0x69, + 0x6C, 0x66, 0x65, 0x6E, 0x61, 0x62, 0x6C, 0x65, 0x61, 0x76, 0xF5, 0xA2, + 0x61, 0x6E, 0x82, 0x64, 0x77, 0x69, 0x66, 0x69, 0x66, 0x65, 0x6E, 0x61, + 0x62, 0x6C, 0x65, 0x61, 0x76, 0xF5, +}; + +esp_err_t esp_insights_test_cmd_handler() +{ + int encoded_sz = sizeof (test_buf2); + const uint8_t *test_buffer = test_buf2; + ESP_LOGI(TAG, "Performing commands decode on test_data0"); + void *resp_data = NULL; + size_t resp_len = 0; + esp_err_t ret = esp_insights_cmd_handler(test_buffer, encoded_sz, &resp_data, &resp_len, NULL, NULL); + ESP_LOGI(TAG, "response: %s", (char *) resp_data); + + ESP_LOGI(TAG, "Performing commands decode on test_data1"); + encoded_sz = sizeof (test_buf1); + test_buffer = test_buf1; + resp_data = NULL; + resp_len = 0; + ret = esp_insights_cmd_handler(test_buffer, encoded_sz, &resp_data, &resp_len, NULL, NULL); + ESP_LOGI(TAG, "response: %s", (char *) resp_data); + return ret; +} + +static void esp_insights_cmd_callback(const char *topic, void *payload, size_t payload_len, void *priv_data) +{ + void *output = NULL; + size_t output_len = 0; + /* Any command data received is directly sent to the command response framework and on success, + * the response (if any) is sent back to the MQTT Broker. + */ + if (esp_rmaker_cmd_response_handler(payload, payload_len, &output, &output_len) == ESP_OK) { + if (output) { + char publish_topic[100]; + snprintf(publish_topic, sizeof(publish_topic), "node/%s/%s", esp_insights_get_node_id(), FROM_NODE_TOPIC_SUFFIX); + if (esp_insights_mqtt_publish(publish_topic, output, output_len, RMAKER_MQTT_QOS1, NULL) != ESP_OK) { + ESP_LOGE(TAG, "Failed to publish reponse."); + } + free(output); + } else { + ESP_LOGE(TAG, "No output generated by command-response handler."); + } + } +} + +const char notify_cmd_resp_str[] = + "{\ + \"node_id\": \"%s\",\ + \"config_version\": \"2019-09-11\",\ + \"attributes\": [\ + {\ + \"name\": \"cmd-resp\",\ + \"value\": \"1\"\ + }\ + ],\ + }"; + +esp_err_t esp_insights_cmd_resp_init(void) +{ + esp_err_t err = ESP_FAIL; + if (s_cmd_resp_data.init_done) { + ESP_LOGI(TAG, "already initialized. Skipped"); + return ESP_OK; + } + + s_cmd_resp_data.scratch_buf = MEM_ALLOC_EXTRAM(SCRATCH_BUF_SIZE); + if (!s_cmd_resp_data.scratch_buf) { + ESP_LOGE(TAG, "Failed to allocate memory for scratch buffer."); + err = ESP_ERR_NO_MEM; + goto init_err; + } + + const int topic_size = 100; + char *mqtt_topic = (char *) s_cmd_resp_data.scratch_buf; + const char *node_id = esp_insights_get_node_id(); + if (!node_id) { + ESP_LOGE(TAG, "node_id not found. Bailing out..."); + goto init_err; + } + snprintf(mqtt_topic, topic_size, "node/%s/%s", node_id, TO_NODE_TOPIC_SUFFIX); + err = esp_insights_mqtt_subscribe(mqtt_topic, esp_insights_cmd_callback, RMAKER_MQTT_QOS1, NULL); + if(err != ESP_OK) { + ESP_LOGE(TAG, "Failed to subscribe to %s. Error %d", mqtt_topic, err); + goto init_err; + } + + /* Notify rmaker about our command response capability */ + char *publish_data = mqtt_topic + topic_size; + sprintf(publish_data, notify_cmd_resp_str, esp_insights_get_node_id()); + snprintf(mqtt_topic, topic_size, "node/%s/%s", esp_insights_get_node_id(), RMAKER_CFG_TOPIC_SUFFIX); + if (esp_insights_mqtt_publish(mqtt_topic, publish_data, strlen(publish_data), RMAKER_MQTT_QOS1, NULL) != ESP_OK) { + ESP_LOGE(TAG, "Failed to publish cmd-resp attrib"); + } + + s_cmd_resp_data.init_done = true; + ESP_LOGI(TAG, "Command-Response Module initialized"); + return ESP_OK; + +init_err: + if (s_cmd_resp_data.scratch_buf) { + free(s_cmd_resp_data.scratch_buf); + s_cmd_resp_data.scratch_buf = NULL; + } + return err; +} + +esp_err_t esp_insights_cmd_resp_enable(void) +{ + esp_err_t err = ESP_FAIL; + + if (s_cmd_resp_data.enabled == true) { + ESP_LOGI(TAG, "already enabled. Skipped"); + return ESP_OK; + } + + if (!s_cmd_resp_data.scratch_buf) { + s_cmd_resp_data.scratch_buf = MEM_ALLOC_EXTRAM(SCRATCH_BUF_SIZE); + } + if (!s_cmd_resp_data.scratch_buf) { + ESP_LOGE(TAG, "Failed to allocate memory for scratch buffer."); + err = ESP_ERR_NO_MEM; + goto enable_err; + } + + esp_insights_cbor_encoder_register_meta_cb(&esp_insights_cbor_reboot_msg_cb); + /* register `reboot` command to our commands store */ + esp_insights_cmd_resp_register_cmd(reboot_cmd_handler, NULL, 1, "reboot"); + + ESP_LOGI(TAG, "Enabling Command-Response Module."); + + /* Register our config parsing command to cmd_resp module */ + err = esp_rmaker_cmd_register(INSIGHTS_CONF_CMD, ESP_RMAKER_USER_ROLE_SUPER_ADMIN, + esp_insights_cmd_handler, false, NULL); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to register INSIGHTS_CONF_CMD"); + goto enable_err; + } + + s_cmd_resp_data.enabled = true; + return ESP_OK; + +enable_err: + if (s_cmd_resp_data.scratch_buf) { + free(s_cmd_resp_data.scratch_buf); + s_cmd_resp_data.scratch_buf = NULL; + } + s_cmd_resp_data.init_done = false; + return err; +} + +#else /* CONFIG_ESP_INSIGHTS_CMD_RESP_ENABLED */ +esp_err_t esp_insights_cmd_resp_init(void) +{ + return ESP_FAIL; +} + +esp_err_t esp_insights_cmd_resp_enable(void) { + ESP_LOGE(TAG, "Please enable CONFIG_ESP_INSIGHTS_CMD_RESP_ENABLED=y, line %d", __LINE__); + return ESP_FAIL; +} + +#endif /* CONFIG_ESP_INSIGHTS_CMD_RESP_ENABLED */ diff --git a/components/esp_insights/src/esp_insights_encoder.c b/components/esp_insights/src/esp_insights_encoder.c index f3afbb0..611a849 100644 --- a/components/esp_insights/src/esp_insights_encoder.c +++ b/components/esp_insights/src/esp_insights_encoder.c @@ -32,6 +32,7 @@ #define INSIGHTS_DATA_TYPE 0x02 #define INSIGHTS_META_DATA_TYPE 0x03 +#define INSIGHTS_CONF_DATA_TYPE 0x12 #define TLV_OFFSET 3 static void esp_insights_encode_meta_data(void) @@ -85,6 +86,27 @@ esp_err_t esp_insights_encode_data_begin(void *out_data, size_t out_data_size) return ESP_OK; } +size_t esp_insights_encode_conf_meta(uint8_t *out_data, size_t out_data_size, char *sha256) +{ + if (!out_data || !out_data_size) { + return 0; + } + esp_insights_cbor_encode_meta_begin(out_data + TLV_OFFSET, + out_data_size - TLV_OFFSET, + INSIGHTS_META_VERSION, sha256); + esp_insights_cbor_encode_conf_meta_data_begin(); + /* TODO: Implement and collect diagnostics specific conf meta */ + // esp_insights_encode_conf_meta_data(); + esp_insights_cbor_encode_conf_meta_data_end(); + + uint16_t len = esp_insights_cbor_encode_meta_end(out_data + TLV_OFFSET); + + out_data[0] = INSIGHTS_META_DATA_TYPE; /* Data type inidcation diagnostics meta - 1 byte */ + memcpy(&out_data[1], &len, sizeof(len)); /* Data length - 2 bytes */ + len += TLV_OFFSET; + return len; +} + void esp_insights_encode_boottime_data(void) { /* encode device info */ @@ -112,6 +134,29 @@ void esp_insights_encode_boottime_data(void) #endif /* CONFIG_ESP_INSIGHTS_COREDUMP_ENABLE */ } +void esp_insights_encode_conf_data() +{ + /* collect the configs */ + esp_insights_cbor_encode_diag_conf_data_begin(); + esp_insights_cbor_encode_diag_conf_data(); + esp_insights_cbor_encode_diag_conf_data_end(); +} + + +size_t esp_insights_encode_conf_end(uint8_t *out_data) +{ + if (!out_data) { + return 0; + } + esp_insights_cbor_encode_diag_data_end(); + uint16_t len = esp_insights_cbor_encode_diag_end(out_data + TLV_OFFSET); + + out_data[0] = INSIGHTS_CONF_DATA_TYPE; /* Data type indicating diagnostics - 1 byte */ + memcpy(&out_data[1], &len, sizeof(len)); /* Data length - 2 bytes */ + len += TLV_OFFSET; + return len; +} + size_t esp_insights_encode_critical_data(const void *data, size_t data_size) { size_t consumed = 0; diff --git a/components/esp_insights/src/esp_insights_encoder.h b/components/esp_insights/src/esp_insights_encoder.h index 6a27336..1f12cfe 100644 --- a/components/esp_insights/src/esp_insights_encoder.h +++ b/components/esp_insights/src/esp_insights_encoder.h @@ -11,7 +11,7 @@ #endif size_t esp_insights_encode_meta(uint8_t *out_data, size_t out_data_size, char *sha256); - +size_t esp_insights_encode_conf_meta(uint8_t *out_data, size_t out_data_size, char *sha256); esp_err_t esp_insights_encode_data_begin(uint8_t *out_data, size_t out_data_size); void esp_insights_encode_boottime_data(void); diff --git a/components/esp_insights/src/esp_insights_internal.h b/components/esp_insights/src/esp_insights_internal.h index 2915575..7f10232 100644 --- a/components/esp_insights/src/esp_insights_internal.h +++ b/components/esp_insights/src/esp_insights_internal.h @@ -9,8 +9,14 @@ #include #ifdef CONFIG_ESP_INSIGHTS_TRANSPORT_MQTT +#include + /* Default configurations for rmaker mqtt glue lib */ extern esp_insights_transport_config_t g_default_insights_transport_mqtt; + +/* other functions for more granularity */ +esp_err_t esp_insights_mqtt_publish(const char *topic, void *data, size_t data_len, uint8_t qos, int *msg_id); +esp_err_t esp_insights_mqtt_subscribe(const char *topic, esp_rmaker_mqtt_subscribe_cb_t cb, uint8_t qos, void *priv_data); #else /* Default configurations for https */ extern esp_insights_transport_config_t g_default_insights_transport_https; @@ -41,6 +47,11 @@ void esp_insights_transport_disconnect(void); */ int esp_insights_transport_data_send(void *data, size_t len); +/** + * @brief Send update to the cloud about new state + */ +void esp_insights_report_config_update(void); + /** * @brief Get node id * diff --git a/components/esp_insights/src/transport/esp_insights_mqtt.c b/components/esp_insights/src/transport/esp_insights_mqtt.c index fa3649b..6c78622 100644 --- a/components/esp_insights/src/transport/esp_insights_mqtt.c +++ b/components/esp_insights/src/transport/esp_insights_mqtt.c @@ -156,7 +156,7 @@ static void esp_insights_mqtt_disconnect(void) } } -static esp_err_t esp_insights_mqtt_publish(const char *topic, void *data, size_t data_len, uint8_t qos, int *msg_id) +esp_err_t esp_insights_mqtt_publish(const char *topic, void *data, size_t data_len, uint8_t qos, int *msg_id) { if (s_mqtt_data.mqtt_config.publish) { return s_mqtt_data.mqtt_config.publish(topic, data, data_len, qos, msg_id); @@ -165,6 +165,16 @@ static esp_err_t esp_insights_mqtt_publish(const char *topic, void *data, size_t return ESP_ERR_NOT_FOUND; } +esp_err_t esp_insights_mqtt_subscribe(const char *topic, esp_rmaker_mqtt_subscribe_cb_t cb, uint8_t qos, void *priv_data) +{ + if (s_mqtt_data.mqtt_config.subscribe) { + ESP_LOGI(TAG, "subscribing to %s", topic); + return s_mqtt_data.mqtt_config.subscribe(topic, cb, qos, priv_data); + } + ESP_LOGW(TAG, "esp_insights_mqtt_subscribe not registered"); + return ESP_OK; +} + static int esp_insights_mqtt_data_send(void *data, size_t len) { char topic[128]; diff --git a/components/rmaker_common b/components/rmaker_common index 41fadd3..6398f40 160000 --- a/components/rmaker_common +++ b/components/rmaker_common @@ -1 +1 @@ -Subproject commit 41fadd324e511bb338c6ee08e337b2f321b280f9 +Subproject commit 6398f401f2d4333cf0ed712d51f8fce3830cadf6 diff --git a/examples/diagnostics_smoke_test/main/idf_component.yml b/examples/diagnostics_smoke_test/main/idf_component.yml index 92bb199..fc0a22c 100644 --- a/examples/diagnostics_smoke_test/main/idf_component.yml +++ b/examples/diagnostics_smoke_test/main/idf_component.yml @@ -1,5 +1,5 @@ ## IDF Component Manager Manifest File. Requires idf-component-manager > v1.2.3 dependencies: espressif/esp_insights: - version: "~1.1.0" + version: ">=1.1.0" override_path: "../../../components/esp_insights" diff --git a/examples/minimal_diagnostics/main/idf_component.yml b/examples/minimal_diagnostics/main/idf_component.yml index 92bb199..fc0a22c 100644 --- a/examples/minimal_diagnostics/main/idf_component.yml +++ b/examples/minimal_diagnostics/main/idf_component.yml @@ -1,5 +1,5 @@ ## IDF Component Manager Manifest File. Requires idf-component-manager > v1.2.3 dependencies: espressif/esp_insights: - version: "~1.1.0" + version: ">=1.1.0" override_path: "../../../components/esp_insights"