diff --git a/CMakeLists.txt b/CMakeLists.txt index a08bf89..9d7de64 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,7 +4,7 @@ cmake_minimum_required(VERSION 3.16) # (Not part of the boilerplate) # This example uses an extra component for common functions such as Wi-Fi and Ethernet connection. -set(EXTRA_COMPONENT_DIRS $ENV{IDF_PATH}/examples/common_components/protocol_examples_common) +#set(EXTRA_COMPONENT_DIRS $ENV{IDF_PATH}/examples/common_components/protocol_examples_common) include($ENV{IDF_PATH}/tools/cmake/project.cmake) -project(ota) +project(ota_ws) diff --git a/include/ota_ws.h b/include/ota_ws.h new file mode 100644 index 0000000..cec02d5 --- /dev/null +++ b/include/ota_ws.h @@ -0,0 +1,23 @@ +#pragma once + +#include "ota_ws_private.h" + +#ifdef __cplusplus +extern "C" +{ +#endif + + +/* +* @brief register provision handlers ( web page & ws handlers) on existing httpd server with ws support +* uri page -> CONFIG_DEFAULT_URI +* @param httpd_handle_t server -> existing server handle +* @return +* ESP_OK -> register OK +* ESP_FAIL -> register FAIL +*/ +esp_err_t ota_ws_register_uri_handler(httpd_handle_t server); + +#ifdef __cplusplus +} +#endif \ No newline at end of file diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index eae7473..729cbbd 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -2,9 +2,9 @@ idf_component_register( SRCS - + example_ota_ws.c INCLUDE_DIRS "." - EMBED_FILES - + #EMBED_FILES + ota_ws.html ) \ No newline at end of file diff --git a/main/ota_ws.c b/main/example_ota_ws.c similarity index 79% rename from main/ota_ws.c rename to main/example_ota_ws.c index 80b3788..ef26536 100644 --- a/main/ota_ws.c +++ b/main/example_ota_ws.c @@ -12,13 +12,6 @@ #include "esp_event.h" #include "esp_log.h" #include "esp_ota_ops.h" -#include "esp_http_client.h" -#include "esp_https_ota.h" -#include "protocol_examples_common.h" -#include "string.h" - -#include "nvs.h" -#include "nvs_flash.h" #include "esp_wifi.h" //#include @@ -26,9 +19,6 @@ #include "prv_wifi_connect.h" static const char *TAG = "ota_ws"; -void example_echo_ws_server(void); -esp_err_t example_register_uri_handler(httpd_handle_t server); - #define MDNS #ifdef MDNS #include "mdns.h" @@ -62,6 +52,6 @@ void app_main(void) netbiosns_set_name("esp"); #endif // MDNS - prv_start_http_server(PRV_MODE_STAY_ACTIVE,example_register_uri_handler); // run server + prv_start_http_server(PRV_MODE_STAY_ACTIVE,NULL); // run server //example_echo_ws_server(); } diff --git a/private_include/jsmn.h b/private_include/jsmn.h new file mode 100644 index 0000000..f1b2bb7 --- /dev/null +++ b/private_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/private_include/ota_ws_private.h b/private_include/ota_ws_private.h new file mode 100644 index 0000000..7945938 --- /dev/null +++ b/private_include/ota_ws_private.h @@ -0,0 +1,7 @@ +#pragma once + +#include "esp_event.h" +#include "freertos/event_groups.h" +#include +#include +#include "esp_http_server.h" \ No newline at end of file diff --git a/source/ota_esp.c b/source/ota_esp.c new file mode 100644 index 0000000..5737a37 --- /dev/null +++ b/source/ota_esp.c @@ -0,0 +1,96 @@ + +#include "esp_ota_ops.h" +#include "esp_flash_partitions.h" +#include "esp_partition.h" + +static const char *TAG = "ota_esp"; +/*an ota data write buffer ready to write to the flash*/ +//static char ota_write_data[BUFFSIZE + 1] = {0}; +static const esp_partition_t *update_partition = NULL; +static bool image_header_was_checked = false; +static int binary_file_length = 0; +static esp_ota_handle_t update_handle = 0; + +esp_err_t start_ota(void) +{ +// return ESP_OK; // debug return + + 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 != running) + { + ESP_LOGW(TAG, "Configured OTA boot partition at offset 0x%08x, but running from offset 0x%08x", + 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%08x)", + 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%x", + update_partition->subtype, update_partition->address); + + err = esp_ota_begin(update_partition, OTA_SIZE_UNKNOWN, &update_handle); + if (err != ESP_OK) + { + ESP_LOGI(TAG, "esp_ota_begin failed "); + return -1; + } + ESP_LOGI(TAG, "esp_ota_begin succeeded"); + + image_header_was_checked = false; + return ESP_OK; +} + +esp_err_t write_ota(int data_read, uint8_t *ota_write_data) +{ +// return data_read; // 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 -1; + } + } + esp_err_t err = esp_ota_write(update_handle, (const void *)ota_write_data, data_read); + if (err != ESP_OK) + { + return -1; + } + binary_file_length += data_read; + ESP_LOGD(TAG, "Written image length %d", binary_file_length); + return ESP_OK; +} + +esp_err_t end_ota(void) +{ + 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 -1; + } + 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 -1; + } + return ESP_OK; +} \ No newline at end of file diff --git a/source/ota_ws.c b/source/ota_ws.c new file mode 100644 index 0000000..5f1ecc8 --- /dev/null +++ b/source/ota_ws.c @@ -0,0 +1,138 @@ +#include "ota_ws_private.h" +#include "ota_ws.h" + +#include "freertos/task.h" +#include "freertos/queue.h" + +#include "jsmn.h" + +#define OTA_DEFAULT_WS_URI "/ws" +#define OTA_DEFAULT_URI "/" + +static const char *TAG = "ota_ws"; + +// 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; +} +static void 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); + httpd_ws_send_frame(req, &ws_pkt); +} +// write wifi data from ws to nvs +static esp_err_t ota_ws_handler(httpd_req_t *req) +{ + if (req->method == HTTP_GET) + { + ESP_LOGI(TAG, "Handshake done, the new connection was opened"); + send_nvs_data(req); // read & send initial wifi data from nvs + return ESP_OK; + } + httpd_ws_frame_t ws_pkt; + uint8_t *buf = NULL; + memset(&ws_pkt, 0, sizeof(httpd_ws_frame_t)); + // ws_pkt.type = HTTPD_WS_TYPE_TEXT; + /* 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) + { + ESP_LOGE(TAG, "httpd_ws_recv_frame failed to get frame len with %d", ret); + 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) + { + ESP_LOGE(TAG, "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) + { + ESP_LOGE(TAG, "httpd_ws_recv_frame failed with %d", ret); + goto _recv_ret; + } + } + ret = ESP_OK; + if (ws_pkt.type == HTTPD_WS_TYPE_TEXT) + { + // json data + } + else if (ws_pkt.type == HTTPD_WS_TYPE_BIN) + { + // ota data + } + else + { + ESP_LOGE(TAG, "httpd_ws_recv_frame unknown frame type %d", ws_pkt.type); + ret = ESP_FAIL; + goto _recv_ret; + } +_recv_ret: + free(buf); + return ret; +} +static esp_err_t ota_get_handler(httpd_req_t *req) +{ + extern const unsigned char ota_ws_html_start[] asm("_binary_ota_ws_html_start"); + extern const unsigned char ota_ws_html_end[] asm("_binary_ota_ws_html_end"); + const size_t ota_ws_html_size = (ota_ws_html_end - ota_ws_html_start); + + httpd_resp_send_chunk(req, (const char *)ota_ws_html_start, ota_ws_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}; +esp_err_t ota_ws_register_uri_handler(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/main/ota_ws.html b/source/ota_ws.html similarity index 100% rename from main/ota_ws.html rename to source/ota_ws.html