diff --git a/CMakeLists.txt b/CMakeLists.txt index 9ab5c51..dbd18c9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1 +1 @@ -idf_component_register(SRCS "main.c" INCLUDE_DIRS "include") \ No newline at end of file +idf_component_register(SRCS "zh_ota_server.c" INCLUDE_DIRS "include" REQUIRES app_update esp_http_server EMBED_FILES "zh_ota_server.html") \ No newline at end of file diff --git a/Kconfig.projbuild b/Kconfig.projbuild new file mode 100644 index 0000000..6ba39ac --- /dev/null +++ b/Kconfig.projbuild @@ -0,0 +1,43 @@ +menu "OTA websocket update" + + config OTA_DEFAULT_URI + string "OTA page URI" + default "/ota" + help + WEB page URI to OTA update. + + config OTA_DEFAULT_WS_URI + string "OTA ws URI" + default "/ota/ws" + help + WEB ws URI to OTA update. + + config OTA_CHUNK_SIZE + int "Ota chunk size" + default 8192 + help + Ota download chunk size. + + config OTA_PRE_ENCRYPTED_MODE + bool "Ota pre-encrypted mode" + default n + help + Ota pre-encrypted mode. + + choice OTA_PRE_ENCRYPTED_RSA_KEY_LOCATION + depends on OTA_PRE_ENCRYPTED_MODE + prompt "RSA key directory" + default OTA_PRE_ENCRYPTED_RSA_KEY_ON_COMPONENT_LOCATION + config OTA_PRE_ENCRYPTED_RSA_KEY_ON_PROJECT_LOCATION + bool "PROJECT_DIR" + config OTA_PRE_ENCRYPTED_RSA_KEY_ON_COMPONENT_LOCATION + bool "COMPONENT_DIR" + endchoice + + config OTA_PRE_ENCRYPTED_RSA_KEY_DIRECTORY + depends on OTA_PRE_ENCRYPTED_MODE + string "Ota pre-encrypted RSA key directory" + default "rsa_key" + + +endmenu \ No newline at end of file diff --git a/README.md b/README.md index 3df3e4d..0dc79e1 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,39 @@ # esp_component_template -esp_component_template \ No newline at end of file +esp_component_template + +#include "zh_ota_server.h" +#include "esp_wifi.h" +#include "nvs_flash.h" + +#define WIFI_SSID "ZH_OTA_TEST" +#define WIFI_PASS "zh_ota_test" +#define WIFI_CHANNEL 1 +#define MAX_STA_CONNECTION 4 + +static httpd_handle_t ota_server_handle = NULL; + +void app_main(void) +{ + esp_log_level_set("zh_ota_server", ESP_LOG_ERROR); + nvs_flash_init(); + esp_event_loop_create_default(); + esp_netif_init(); + esp_netif_create_default_wifi_ap(); + wifi_init_config_t wifi_config = WIFI_INIT_CONFIG_DEFAULT(); + esp_wifi_init(&wifi_config); + wifi_config_t ap_config = { + .ap = { + .ssid = WIFI_SSID, + .password = WIFI_PASS, + .max_connection = 4, + .authmode = WIFI_AUTH_WPA2_PSK, + }, + }; + esp_wifi_set_mode(WIFI_MODE_AP); + esp_wifi_set_config(WIFI_IF_AP, &ap_config); + esp_wifi_start(); + httpd_config_t config = HTTPD_DEFAULT_CONFIG(); + httpd_start(&ota_server_handle, &config); + zh_ota_server_init(ota_server_handle); +} \ No newline at end of file diff --git a/component.mk b/component.mk deleted file mode 100644 index e69de29..0000000 diff --git a/include/jsmn.h b/include/jsmn.h new file mode 100644 index 0000000..f1b2bb7 --- /dev/null +++ b/include/jsmn.h @@ -0,0 +1,530 @@ +/* + * MIT License + * + * Copyright (c) 2010 Serge Zaitsev + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +#ifndef JSMN_H +#define JSMN_H + +#include + +#ifdef __cplusplus +extern "C" +{ +#endif + +#define JSMN_STATIC + +#ifdef JSMN_STATIC +#define JSMN_API static +#else +#define JSMN_API extern +#endif + + /** + * JSON type identifier. Basic types are: + * o Object + * o Array + * o String + * o Other primitive: number, boolean (true/false) or null + */ + typedef enum + { + JSMN_UNDEFINED = 0, + JSMN_OBJECT = 1, + JSMN_ARRAY = 2, + JSMN_STRING = 3, + JSMN_PRIMITIVE = 4 + } jsmntype_t; + + enum jsmnerr + { + /* Not enough tokens were provided */ + JSMN_ERROR_NOMEM = -1, + /* Invalid character inside JSON string */ + JSMN_ERROR_INVAL = -2, + /* The string is not a full JSON packet, more bytes expected */ + JSMN_ERROR_PART = -3 + }; + + /** + * JSON token description. + * type type (object, array, string etc.) + * start start position in JSON data string + * end end position in JSON data string + */ + typedef struct jsmntok + { + jsmntype_t type; + int start; + int end; + int size; +#ifdef JSMN_PARENT_LINKS + int parent; +#endif + } jsmntok_t; + + /** + * JSON parser. Contains an array of token blocks available. Also stores + * the string being parsed now and current position in that string. + */ + typedef struct jsmn_parser + { + unsigned int pos; /* offset in the JSON string */ + unsigned int toknext; /* next token to allocate */ + int toksuper; /* superior token node, e.g. parent object or array */ + } jsmn_parser; + + /** + * Create JSON parser over an array of tokens + */ + JSMN_API void jsmn_init(jsmn_parser *parser); + + /** + * Run JSON parser. It parses a JSON data string into and array of tokens, each + * describing + * a single JSON object. + */ + JSMN_API int jsmn_parse(jsmn_parser *parser, const char *js, const size_t len, + jsmntok_t *tokens, const unsigned int num_tokens); + +#ifndef JSMN_HEADER + /** + * Allocates a fresh unused token from the token pool. + */ + static jsmntok_t *jsmn_alloc_token(jsmn_parser *parser, jsmntok_t *tokens, + const size_t num_tokens) + { + jsmntok_t *tok; + if (parser->toknext >= num_tokens) + { + return NULL; + } + tok = &tokens[parser->toknext++]; + tok->start = tok->end = -1; + tok->size = 0; +#ifdef JSMN_PARENT_LINKS + tok->parent = -1; +#endif + return tok; + } + + /** + * Fills token type and boundaries. + */ + static void jsmn_fill_token(jsmntok_t *token, const jsmntype_t type, + const int start, const int end) + { + token->type = type; + token->start = start; + token->end = end; + token->size = 0; + } + + /** + * Fills next available token with JSON primitive. + */ + static int jsmn_parse_primitive(jsmn_parser *parser, const char *js, + const size_t len, jsmntok_t *tokens, + const size_t num_tokens) + { + jsmntok_t *token; + int start; + + start = parser->pos; + + for (; parser->pos < len && js[parser->pos] != '\0'; parser->pos++) + { + switch (js[parser->pos]) + { +#ifndef JSMN_STRICT + /* In strict mode primitive must be followed by "," or "}" or "]" */ + case ':': +#endif + case '\t': + case '\r': + case '\n': + case ' ': + case ',': + case ']': + case '}': + goto found; + default: + /* to quiet a warning from gcc*/ + break; + } + if (js[parser->pos] < 32 || js[parser->pos] >= 127) + { + parser->pos = start; + return JSMN_ERROR_INVAL; + } + } +#ifdef JSMN_STRICT + /* In strict mode primitive must be followed by a comma/object/array */ + parser->pos = start; + return JSMN_ERROR_PART; +#endif + + found: + if (tokens == NULL) + { + parser->pos--; + return 0; + } + token = jsmn_alloc_token(parser, tokens, num_tokens); + if (token == NULL) + { + parser->pos = start; + return JSMN_ERROR_NOMEM; + } + jsmn_fill_token(token, JSMN_PRIMITIVE, start, parser->pos); +#ifdef JSMN_PARENT_LINKS + token->parent = parser->toksuper; +#endif + parser->pos--; + return 0; + } + + /** + * Fills next token with JSON string. + */ + static int jsmn_parse_string(jsmn_parser *parser, const char *js, + const size_t len, jsmntok_t *tokens, + const size_t num_tokens) + { + jsmntok_t *token; + + int start = parser->pos; + + parser->pos++; + + /* Skip starting quote */ + for (; parser->pos < len && js[parser->pos] != '\0'; parser->pos++) + { + char c = js[parser->pos]; + + /* Quote: end of string */ + if (c == '\"') + { + if (tokens == NULL) + { + return 0; + } + token = jsmn_alloc_token(parser, tokens, num_tokens); + if (token == NULL) + { + parser->pos = start; + return JSMN_ERROR_NOMEM; + } + jsmn_fill_token(token, JSMN_STRING, start + 1, parser->pos); +#ifdef JSMN_PARENT_LINKS + token->parent = parser->toksuper; +#endif + return 0; + } + + /* Backslash: Quoted symbol expected */ + if (c == '\\' && parser->pos + 1 < len) + { + int i; + parser->pos++; + switch (js[parser->pos]) + { + /* Allowed escaped symbols */ + case '\"': + case '/': + case '\\': + case 'b': + case 'f': + case 'r': + case 'n': + case 't': + break; + /* Allows escaped symbol \uXXXX */ + case 'u': + parser->pos++; + for (i = 0; i < 4 && parser->pos < len && js[parser->pos] != '\0'; + i++) + { + /* If it isn't a hex character we have an error */ + if (!((js[parser->pos] >= 48 && js[parser->pos] <= 57) || /* 0-9 */ + (js[parser->pos] >= 65 && js[parser->pos] <= 70) || /* A-F */ + (js[parser->pos] >= 97 && js[parser->pos] <= 102))) + { /* a-f */ + parser->pos = start; + return JSMN_ERROR_INVAL; + } + parser->pos++; + } + parser->pos--; + break; + /* Unexpected symbol */ + default: + parser->pos = start; + return JSMN_ERROR_INVAL; + } + } + } + parser->pos = start; + return JSMN_ERROR_PART; + } + + /** + * Parse JSON string and fill tokens. + */ + JSMN_API int jsmn_parse(jsmn_parser *parser, const char *js, const size_t len, + jsmntok_t *tokens, const unsigned int num_tokens) + { + int r; + int i; + jsmntok_t *token; + int count = parser->toknext; + + for (; parser->pos < len && js[parser->pos] != '\0'; parser->pos++) + { + char c; + jsmntype_t type; + + c = js[parser->pos]; + switch (c) + { + case '{': + case '[': + count++; + if (tokens == NULL) + { + break; + } + token = jsmn_alloc_token(parser, tokens, num_tokens); + if (token == NULL) + { + return JSMN_ERROR_NOMEM; + } + if (parser->toksuper != -1) + { + jsmntok_t *t = &tokens[parser->toksuper]; +#ifdef JSMN_STRICT + /* In strict mode an object or array can't become a key */ + if (t->type == JSMN_OBJECT) + { + return JSMN_ERROR_INVAL; + } +#endif + t->size++; +#ifdef JSMN_PARENT_LINKS + token->parent = parser->toksuper; +#endif + } + token->type = (c == '{' ? JSMN_OBJECT : JSMN_ARRAY); + token->start = parser->pos; + parser->toksuper = parser->toknext - 1; + break; + case '}': + case ']': + if (tokens == NULL) + { + break; + } + type = (c == '}' ? JSMN_OBJECT : JSMN_ARRAY); +#ifdef JSMN_PARENT_LINKS + if (parser->toknext < 1) + { + return JSMN_ERROR_INVAL; + } + token = &tokens[parser->toknext - 1]; + for (;;) + { + if (token->start != -1 && token->end == -1) + { + if (token->type != type) + { + return JSMN_ERROR_INVAL; + } + token->end = parser->pos + 1; + parser->toksuper = token->parent; + break; + } + if (token->parent == -1) + { + if (token->type != type || parser->toksuper == -1) + { + return JSMN_ERROR_INVAL; + } + break; + } + token = &tokens[token->parent]; + } +#else + for (i = parser->toknext - 1; i >= 0; i--) + { + token = &tokens[i]; + if (token->start != -1 && token->end == -1) + { + if (token->type != type) + { + return JSMN_ERROR_INVAL; + } + parser->toksuper = -1; + token->end = parser->pos + 1; + break; + } + } + /* Error if unmatched closing bracket */ + if (i == -1) + { + return JSMN_ERROR_INVAL; + } + for (; i >= 0; i--) + { + token = &tokens[i]; + if (token->start != -1 && token->end == -1) + { + parser->toksuper = i; + break; + } + } +#endif + break; + case '\"': + r = jsmn_parse_string(parser, js, len, tokens, num_tokens); + if (r < 0) + { + return r; + } + count++; + if (parser->toksuper != -1 && tokens != NULL) + { + tokens[parser->toksuper].size++; + } + break; + case '\t': + case '\r': + case '\n': + case ' ': + break; + case ':': + parser->toksuper = parser->toknext - 1; + break; + case ',': + if (tokens != NULL && parser->toksuper != -1 && + tokens[parser->toksuper].type != JSMN_ARRAY && + tokens[parser->toksuper].type != JSMN_OBJECT) + { +#ifdef JSMN_PARENT_LINKS + parser->toksuper = tokens[parser->toksuper].parent; +#else + for (i = parser->toknext - 1; i >= 0; i--) + { + if (tokens[i].type == JSMN_ARRAY || tokens[i].type == JSMN_OBJECT) + { + if (tokens[i].start != -1 && tokens[i].end == -1) + { + parser->toksuper = i; + break; + } + } + } +#endif + } + break; +#ifdef JSMN_STRICT + /* In strict mode primitives are: numbers and booleans */ + case '-': + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + case 't': + case 'f': + case 'n': + /* And they must not be keys of the object */ + if (tokens != NULL && parser->toksuper != -1) + { + const jsmntok_t *t = &tokens[parser->toksuper]; + if (t->type == JSMN_OBJECT || + (t->type == JSMN_STRING && t->size != 0)) + { + return JSMN_ERROR_INVAL; + } + } +#else + /* In non-strict mode every unquoted value is a primitive */ + default: +#endif + r = jsmn_parse_primitive(parser, js, len, tokens, num_tokens); + if (r < 0) + { + return r; + } + count++; + if (parser->toksuper != -1 && tokens != NULL) + { + tokens[parser->toksuper].size++; + } + break; + +#ifdef JSMN_STRICT + /* Unexpected char in strict mode */ + default: + return JSMN_ERROR_INVAL; +#endif + } + } + + if (tokens != NULL) + { + for (i = parser->toknext - 1; i >= 0; i--) + { + /* Unmatched opened object or array */ + if (tokens[i].start != -1 && tokens[i].end == -1) + { + return JSMN_ERROR_PART; + } + } + } + + return count; + } + + /** + * Creates a new parser based over a given buffer with an array of tokens + * available. + */ + JSMN_API void jsmn_init(jsmn_parser *parser) + { + parser->pos = 0; + parser->toknext = 0; + parser->toksuper = -1; + } + +#endif /* JSMN_HEADER */ + +#ifdef __cplusplus +} +#endif + +#endif /* JSMN_H */ \ No newline at end of file diff --git a/include/main.h b/include/main.h deleted file mode 100644 index e69de29..0000000 diff --git a/include/zh_ota_server.h b/include/zh_ota_server.h new file mode 100644 index 0000000..f9758c8 --- /dev/null +++ b/include/zh_ota_server.h @@ -0,0 +1,29 @@ +#pragma once + +#include "esp_ota_ops.h" +#include "esp_flash_partitions.h" +#include "esp_partition.h" +#include "esp_image_format.h" +#include +#include "esp_http_server.h" + +#include "jsmn.h" + +#ifdef __cplusplus +extern "C" +{ +#endif + +/* +* @brief register ota_ws httpd handlers ( web page & ws handlers) on existing httpd server with ws support +* uri page -> CONFIG_OTA_DEFAULT_WS_URI +* @param httpd_handle_t server -> existing server handle +* @return +* ESP_OK -> register OK +* ESP_FAIL -> register FAIL +*/ +esp_err_t zh_ota_server_init(httpd_handle_t server); + +#ifdef __cplusplus +} +#endif \ No newline at end of file diff --git a/main.c b/main.c deleted file mode 100644 index e69de29..0000000 diff --git a/zh_ota_server.c b/zh_ota_server.c new file mode 100644 index 0000000..2049e9d --- /dev/null +++ b/zh_ota_server.c @@ -0,0 +1,420 @@ +/* + 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 "ota_ws_update_private.h" +#include "zh_ota_server.h" +// #include "esp_ota_ops.h" +// #include "esp_flash_partitions.h" +// #include "esp_partition.h" +// #include "esp_image_format.h" +// #include +// #include "esp_http_server.h" + +// #include "jsmn.h" + +#define OTA_RESTART_ESP "otaRestartEsp" +#define OTA_SIZE_START "otaSize" +#define OTA_SET_CHUNK_SIZE "otaSetChunkSize" +#define OTA_GET_CHUNK "otaGetChunk" +#define OTA_END "otaEnd" +#define OTA_ERROR "otaError" +#define OTA_CANCEL "otaCancel" +#define OTA_CHECK_ROLLBACK "otaCheckRollback" +#define OTA_PROCESS_ROLLBACK "otaProcessRollback" + + +esp_err_t start_ota_ws(void); +esp_err_t write_ota_ws(int data_read, uint8_t *ota_write_data); +esp_err_t end_ota_ws(void); +esp_err_t abort_ota_ws(void); +bool check_ota_ws_rollback_enable(void); +esp_err_t rollback_ota_ws(bool rollback); + +#define OTA_DEFAULT_WS_URI CONFIG_OTA_DEFAULT_WS_URI +#define OTA_DEFAULT_URI CONFIG_OTA_DEFAULT_URI +#define OTA_CHUNK_SIZE (CONFIG_OTA_CHUNK_SIZE & ~0xf) + + +static const char *TAG = "zh_ota_server"; + +static int ota_size; // ota firmware size +static int ota_start_chunk; // start address of http chunk +static int ota_started; // ota download started + +static esp_err_t json_to_str_parm(char *jsonstr, char *nameStr, char *valStr); +static esp_err_t send_json_string(char *str, httpd_req_t *req); +static esp_err_t ota_ws_handler(httpd_req_t *req); +static void ota_error(httpd_req_t *req, char *code, char *msg); + +static const esp_partition_t *update_partition = NULL; +static bool image_header_was_checked = false; +static esp_ota_handle_t update_handle = 0; + +//static int tstc=0; + +esp_err_t start_ota_ws(void) +{ + //return ESP_OK; // debug return + //tstc=0; + + esp_err_t err; + ESP_LOGI(TAG, "Starting OTA"); + + const esp_partition_t *configured = esp_ota_get_boot_partition(); + const esp_partition_t *running = esp_ota_get_running_partition(); + if(configured==NULL || running == NULL) + { + ESP_LOGE(TAG,"OTA data not found"); + return ESP_FAIL; + } + + if (configured != running) + { + ESP_LOGW(TAG, "Configured OTA boot partition at offset 0x%08lx, but running from offset 0x%08lx", + configured->address, running->address); + ESP_LOGW(TAG, "(This can happen if either the OTA boot data or preferred boot image become corrupted somehow.)"); + } + ESP_LOGI(TAG, "Running partition type %d subtype %d (offset 0x%08lx)", + running->type, running->subtype, running->address); + + update_partition = esp_ota_get_next_update_partition(NULL); + ESP_LOGI(TAG, "Writing to partition subtype %d at offset 0x%lx", + update_partition->subtype, update_partition->address); + + err = esp_ota_begin(update_partition, OTA_SIZE_UNKNOWN, &update_handle); + if (err != ESP_OK) + { + ESP_LOGE(TAG, "esp_ota_begin failed "); + return ESP_FAIL; + } + ESP_LOGI(TAG, "esp_ota_begin succeeded"); + + image_header_was_checked = false; + return ESP_OK; +} +esp_err_t write_ota_ws(int data_read, uint8_t *ota_write_data) +{ + //return ESP_OK; // debug return + + + if (image_header_was_checked == false) // first segment + { + esp_app_desc_t new_app_info; + if (data_read > sizeof(esp_image_header_t) + sizeof(esp_image_segment_header_t) + sizeof(esp_app_desc_t)) + { + // check current version with downloading + memcpy(&new_app_info, &ota_write_data[sizeof(esp_image_header_t) + sizeof(esp_image_segment_header_t)], sizeof(esp_app_desc_t)); + ESP_LOGI(TAG, "New firmware version: %s", new_app_info.version); + + image_header_was_checked = true; + } + else + { + ESP_LOGE(TAG, "Received package is not fit len"); + return ESP_FAIL; + } + } + esp_err_t err = esp_ota_write(update_handle, (const void *)ota_write_data, data_read); + //tstc+=data_read; + if (err != ESP_OK) + { + return ESP_FAIL; + } + //ESP_LOGI("tstc","%d",tstc); + return ESP_OK; +} +esp_err_t end_ota_ws(void) +{ + //return ESP_OK; // debug return + + esp_err_t err = esp_ota_end(update_handle); + if (err != ESP_OK) { + if (err == ESP_ERR_OTA_VALIDATE_FAILED) { + ESP_LOGE(TAG, "Image validation failed, image is corrupted"); + } + ESP_LOGE(TAG, "esp_ota_end failed (%s)!", esp_err_to_name(err)); + return ESP_FAIL; + } + err = esp_ota_set_boot_partition(update_partition); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_ota_set_boot_partition failed (%s)!", esp_err_to_name(err)); + return ESP_FAIL; + } + return ESP_OK; +} +esp_err_t abort_ota_ws(void) +{ + return esp_ota_abort(update_handle); +} +// false - rollback disable +// true - rollback enable +bool check_ota_ws_rollback_enable(void) +{ +#ifdef CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE + esp_ota_img_states_t ota_state_running_part; + const esp_partition_t *running = esp_ota_get_running_partition(); + if (esp_ota_get_state_partition(running, &ota_state_running_part) == ESP_OK) { + if (ota_state_running_part == ESP_OTA_IMG_PENDING_VERIFY) { + ESP_LOGI(TAG, "Running app has ESP_OTA_IMG_PENDING_VERIFY state"); + return true; + } + } +#endif + return false; +} +// rollback == true - rollback +// rollback == false - app valid? confirm update -> no rollback +esp_err_t rollback_ota_ws(bool rollback) +{ +#ifdef CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE + if(rollback == false) + { + return esp_ota_mark_app_valid_cancel_rollback(); // app valid + } + else + { + return esp_ota_mark_app_invalid_rollback_and_reboot(); // app rolback & reboot + } +#endif + return ESP_FAIL; +} + +// abort OTA, send error/cancel msg to ws +static void ota_error(httpd_req_t *req, char *code, char *msg) +{ + char json_str[128]; + ota_size = ota_start_chunk = ota_started = 0; + abort_ota_ws(); + ESP_LOGE(TAG, "%s %s", code, msg); + snprintf(json_str, sizeof(json_str), "{\"name\":\"%s\",\"value\":\"%s\"}", code, msg); + send_json_string(json_str, req); +} + +// simple json parse -> only one parametr name/val +static esp_err_t json_to_str_parm(char *jsonstr, char *nameStr, char *valStr) // распаковать строку json в пару name/val +{ + int r; // количество токенов + jsmn_parser p; + jsmntok_t t[5]; // только 2 пары параметров и obj + + jsmn_init(&p); + r = jsmn_parse(&p, jsonstr, strlen(jsonstr), t, sizeof(t) / sizeof(t[0])); + if (r < 2) + { + valStr[0] = 0; + nameStr[0] = 0; + return ESP_FAIL; + } + strncpy(nameStr, jsonstr + t[2].start, t[2].end - t[2].start); + nameStr[t[2].end - t[2].start] = 0; + if (r > 3) + { + strncpy(valStr, jsonstr + t[4].start, t[4].end - t[4].start); + valStr[t[4].end - t[4].start] = 0; + } + else + valStr[0] = 0; + return ESP_OK; +} +// send string to ws +static esp_err_t send_json_string(char *str, httpd_req_t *req) +{ + httpd_ws_frame_t ws_pkt; + memset(&ws_pkt, 0, sizeof(httpd_ws_frame_t)); + ws_pkt.type = HTTPD_WS_TYPE_TEXT; + ws_pkt.payload = (uint8_t *)str; + ws_pkt.len = strlen(str); + return httpd_ws_send_frame(req, &ws_pkt); +} +// main ws OTA handler +// Handshake and process OTA +static esp_err_t ota_ws_handler(httpd_req_t *req) +{ + char json_key[64] = {0}; + char json_value[64] = {0}; + char json_str[128] = {0}; + + httpd_ws_frame_t ws_pkt; + uint8_t *buf = NULL; + + + if (req->method == HTTP_GET) + { + ESP_LOGI(TAG, "Handshake done, the new connection was opened"); + if(check_ota_ws_rollback_enable()) // check rollback enable, send cmd to enable rollback dialog on html + { + snprintf(json_str, sizeof(json_str), "{\"name\":\"%s\",\"value\":\"%s\" }", OTA_CHECK_ROLLBACK, "true"); + send_json_string(json_str, req); + } + return ESP_OK; + } + memset(&ws_pkt, 0, sizeof(httpd_ws_frame_t)); + // Set max_len = 0 to get the frame len + esp_err_t ret = httpd_ws_recv_frame(req, &ws_pkt, 0); + if (ret != ESP_OK) + { + ota_error(req, OTA_ERROR, "httpd_ws_recv_frame failed to get frame len"); + return ret; + } + if (ws_pkt.len) + { + // ws_pkt.len + 1 is for NULL termination as we are expecting a string + buf = calloc(1, ws_pkt.len + 1); + if (buf == NULL) + { + ota_error(req, OTA_ERROR, "Failed to calloc memory for buf"); + return ESP_ERR_NO_MEM; + } + ws_pkt.payload = buf; + // Set max_len = ws_pkt.len to get the frame payload + ret = httpd_ws_recv_frame(req, &ws_pkt, ws_pkt.len); + if (ret != ESP_OK) + { + ota_error(req, OTA_ERROR, "httpd_ws_recv_frame failed"); + goto _recv_ret; + } + } + ret = ESP_OK; + if (ws_pkt.type == HTTPD_WS_TYPE_TEXT) // process json cmd + { + if (json_to_str_parm((char *)buf, json_key, json_value)) // decode json to key/value parm + { + ota_error(req, OTA_ERROR, "Error json str"); + goto _recv_ret; + } + if (strncmp(json_key, OTA_SIZE_START, sizeof(OTA_SIZE_START)) == 0) // start ota + { + ota_size = atoi(json_value); + if (ota_size == 0) + { + ota_error(req, OTA_ERROR, "Error ota size = 0"); + goto _recv_ret; + } + ret = start_ota_ws(); + if (ret) + { + ota_error(req, OTA_ERROR, "Error start ota"); + goto _recv_ret; + } + ota_started = 1; + ota_start_chunk = 0; + snprintf(json_str, sizeof(json_str), "{\"name\":\"%s\",\"value\":%d}", OTA_SET_CHUNK_SIZE, OTA_CHUNK_SIZE); // set download chunk + send_json_string(json_str, req); + snprintf(json_str, sizeof(json_str), "{\"name\":\"%s\",\"value\":%d}", OTA_GET_CHUNK, ota_start_chunk); // cmd -> send first chunk with start addresss = 0 + send_json_string(json_str, req); + } + if (strncmp(json_key, OTA_CANCEL, sizeof(OTA_CANCEL)) == 0) // cancel ota + { + ota_error(req, OTA_CANCEL, "Cancel command"); + ret = ESP_OK; + goto _recv_ret; + } + if (strncmp(json_key, OTA_ERROR, sizeof(OTA_ERROR)) == 0) // error ota + { + ota_error(req, OTA_ERROR, "Error command"); + ret = ESP_OK; + goto _recv_ret; + } + if (strncmp(json_key, OTA_PROCESS_ROLLBACK, sizeof(OTA_PROCESS_ROLLBACK)) == 0) // process rollback & + { + if(strncmp(json_value,"true",sizeof("true")) == 0) + { + ESP_LOGI(TAG,"Rollback and restart"); + ret = rollback_ota_ws(true); // rollback and restart + } + else + { + ESP_LOGI(TAG,"App veryfied, fix ota update"); + ret = rollback_ota_ws(false); // app veryfied + } + goto _recv_ret; + } + if (strncmp(json_key, OTA_RESTART_ESP, sizeof(OTA_RESTART_ESP)) == 0) // cancel ota + { + esp_restart(); + } + + } + else if (ws_pkt.type == HTTPD_WS_TYPE_BINARY && ota_started) // download OTA firmware with chunked part + { + + if (ota_start_chunk + ws_pkt.len < ota_size) //read chuk of ota + { + ret = write_ota_ws(ws_pkt.len, buf); // write chunk of ota + if (ret) + { + ota_error(req, OTA_ERROR, "Error write ota"); + goto _recv_ret; + } + ota_start_chunk += ws_pkt.len; + snprintf(json_str, sizeof(json_str), "{\"name\":\"%s\",\"value\": %d }", OTA_GET_CHUNK, ota_start_chunk); // cmd -> next chunk + send_json_string(json_str, req); + + } + else // last chunk and end ota + { + ret = write_ota_ws(ws_pkt.len, buf); // write last chunk of ota + if (ret) + { + ota_error(req, OTA_ERROR, "Error write ota"); + goto _recv_ret; + } + ret = end_ota_ws(); // end ota + if (ret) + { + ota_error(req, OTA_ERROR, "Error end ota"); + goto _recv_ret; + } + ota_size = 0; + ota_start_chunk = 0; + ota_started = 0; + ESP_LOGI(TAG,"OTA END OK"); + snprintf(json_str, sizeof(json_str), "{\"name\":\"%s\",\"value\":\"%s\" }", OTA_END, "OK"); // send ota end cmd ( ota ok ) + send_json_string(json_str, req); + } + } +_recv_ret: + free(buf); + return ret; +} +// main http get handler +// send http initial page and js code +static esp_err_t ota_get_handler(httpd_req_t *req) +{ + extern const unsigned char ota_ws_update_html_start[] asm("_binary_zh_ota_server_html_start"); + extern const unsigned char ota_ws_update_html_end[] asm("_binary_zh_ota_server_html_end"); + const size_t ota_ws_update_html_size = (ota_ws_update_html_end - ota_ws_update_html_start); + + httpd_resp_send_chunk(req, (const char *)ota_ws_update_html_start, ota_ws_update_html_size); + httpd_resp_sendstr_chunk(req, NULL); + return ESP_OK; +} +static const httpd_uri_t gh = { + .uri = OTA_DEFAULT_URI, + .method = HTTP_GET, + .handler = ota_get_handler, + .user_ctx = NULL}; +static const httpd_uri_t ws = { + .uri = OTA_DEFAULT_WS_URI, + .method = HTTP_GET, + .handler = ota_ws_handler, + .user_ctx = NULL, + .is_websocket = true}; + +// register all ota uri handler +esp_err_t zh_ota_server_init(httpd_handle_t server) +{ + esp_err_t ret = ESP_OK; + ret = httpd_register_uri_handler(server, &gh); + if (ret) + goto _ret; + ret = httpd_register_uri_handler(server, &ws); + if (ret) + goto _ret; +_ret: + return ret; +} diff --git a/zh_ota_server.html b/zh_ota_server.html new file mode 100644 index 0000000..4d7e3c4 --- /dev/null +++ b/zh_ota_server.html @@ -0,0 +1,247 @@ + + + + + + OTA UPDATE + + + + + + + +
OTA UPDATE
+ +
+ +
+ +
+ +
+ +
+ + + + +
+ + + + + + + + + + + + \ No newline at end of file