Compare commits

1 Commits
main ... dev

Author SHA1 Message Date
c61e1eec45 wip: 2025-11-17 16:19:14 +03:00
10 changed files with 1307 additions and 2 deletions

View File

@@ -1 +1 @@
idf_component_register(SRCS "main.c" INCLUDE_DIRS "include")
idf_component_register(SRCS "zh_ota_server.c" INCLUDE_DIRS "include" REQUIRES app_update esp_http_server EMBED_FILES "zh_ota_server.html")

43
Kconfig.projbuild Normal file
View File

@@ -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

View File

@@ -1,3 +1,39 @@
# esp_component_template
esp_component_template
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);
}

View File

530
include/jsmn.h Normal file
View File

@@ -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 <stddef.h>
#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 */

View File

29
include/zh_ota_server.h Normal file
View File

@@ -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 <esp_log.h>
#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

0
main.c
View File

420
zh_ota_server.c Normal file
View File

@@ -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 <esp_log.h>
// #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;
}

247
zh_ota_server.html Normal file
View File

@@ -0,0 +1,247 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<title>OTA UPDATE</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
<style>
.column {
float: left;
width: 100%;
margin-top: 2px;
margin-bottom: 2px;
}
.btn {
float: left;
width: 100%;
margin: 2px;
}
.cl1 {
float: left;
width: 100%;
margin: 2px;
margin-top: 2px;
margin-bottom: 2px;
}
.cl01 {
float: left;
width: 100%;
text-align: center;
margin-top: 2px;
margin-bottom: 2px;
}
.cl02 {
float: left;
width: 100%;
text-align: center;
margin-top: 2px;
margin-bottom: 2px;
}
.hdr {
float: left;
width: 100%;
text-align: center;
color: white;
background-color: blue;
padding: 5px;
margin: 5px;
}
.logstr {
width: 100%;
float: left;
}
</style>
</head>
<body>
<div class="hdr">OTA UPDATE</div>
<div class="column">
<button class="btn" id="goHome">Home Page</button>
</div>
<div id="rollback" style="display:none">
<div class="column">
<button class="btn" id="otaVerifyApp">Click to confirm and commit OTA update</button>
</div>
<div class="column">
<button class="btn" id="otaRollback">Cancel OTA. Click to rollback update and restart</button>
</div>
</div>
<div id="update" style="display:block">
<div class="cl1" style="display:none">
<label class="cl01" for="otaFile">Select the new OTA firmware file</label>
<input class="cl02" type="file" id="otaFile" placeholder="select file" onchange="readOtaFile(this)">
</div>
<div class="column" style="display:block" id="otaFileSelectVisible">
<button class="btn" id="otaFileSelect" onclick="document.getElementById('otaFile').click()">File
Select</button>
</div>
<div class="column" style="display:none" id="otaStartVisible">
<button class="btn" id="otaStartCancel">Start OTA update</button>
</div>
<div class="column" style="display:none" id="otaReStartVisible">
<button class="btn" id="otaReStart">Reboot with new OTA firmware</button>
</div>
<div id="otaProgressVisible" style="display:none">
<div class="cl1">
<progress class="cl02" id="otaPogress" max=100 value=0>
</div>
</div>
</div>
<script>
let otaData;
let otaSetChunkSize = 0;
let otaStartsegment = 0;
let otaStarted = 0;
function readOtaFile(input) {
let reader = new FileReader();
let file = input.files[0];
document.getElementById('otaFileSelect').innerHTML = "Selected firmware file: " + file.name;
reader.readAsArrayBuffer(file);
input.value = null;
reader.onload = function () {
otaData = new Uint8Array(reader.result);
document.getElementById("otaStartVisible").style.display = "block";
document.getElementById("otaProgressVisible").style.display = "none";
document.getElementById("otaReStartVisible").style.display = "none";
};
reader.onerror = function () {
console.log(reader.error);
};
}
</script>
<script>
document.getElementById("otaStartCancel").addEventListener("click", function (e) {
if (otaData.length > 0 && otaStarted == 0) {
socket.send(JSON.stringify({ name: "otaSize", value: otaData.length }));
otaStarted = 1;
this.innerHTML = "Click to Cancel";
document.getElementById("otaFileSelect").disabled = true;
document.getElementById("otaProgressVisible").style.display = "block";
document.getElementById("otaPogress").max = otaData.length;
}
else {
otaStarted = 0;
socket.send(JSON.stringify({ name: "otaCancel", value: "Cancel" }));
}
});
document.getElementById("goHome").addEventListener("click", function (e) {
//onclick="window.location.href = '/'"
socket.close();
window.location.href = '/';
});
document.getElementById("otaReStart").addEventListener("click", function (e) {
socket.send(JSON.stringify({ name: "otaRestartEsp", value: "restart" }));
});
function receiveWsData(data) {
try {
let obj = JSON.parse(data);
switch (obj.name) {
case "otaSetChunkSize":
otaSetChunkSize = obj.value;
break;
case "otaGetChunk":
let otaDataSend = otaData.subarray(obj.value, obj.value + otaSetChunkSize);
document.getElementById("otaPogress").value = obj.value;
document.getElementById("otaStartCancel").innerHTML = "Ota download. Size = " + otaData.length + " Segment = " + obj.value + " Click to Cancel";
socket.send(otaDataSend);
break;
case "otaEnd":
otaStartsegment = 0;
otaStarted = 0;
document.getElementById("otaStartVisible").style.display = "none";
document.getElementById("otaStartCancel").innerHTML = "Start OTA update";
document.getElementById("otaPogress").value = otaData.length;
document.getElementById("otaFileSelect").disabled = false;
document.getElementById("otaReStartVisible").style.display = "block";
document.getElementById("otaReStart").innerHTML = "The firmware is loaded. Click to reboot with new OTA firmware";
document.getElementById("otaReStart").disabled = false;
break;
case "otaError":
case "otaCancel":
otaStartsegment = 0;
otaStarted = 0;
document.getElementById("otaStartVisible").style.display = "none";
document.getElementById("otaStartCancel").innerHTML = "Start OTA update";
document.getElementById("otaPogress").value = otaData.length;
document.getElementById("otaFileSelect").disabled = false;
document.getElementById("otaReStartVisible").style.display = "block";
document.getElementById("otaReStart").innerHTML = "ОТА firmware download canceled " + obj.value;
document.getElementById("otaReStart").disabled = true;
break;
case "otaCheckRollback":
document.getElementById("rollback").style.display = "block";
document.getElementById("update").style.display = "none";
break;
}
}
catch
{
console.log(data + "Error msg");
}
};
</script>
<script> // rollback
document.getElementById("otaVerifyApp").addEventListener("click", function (e) {
socket.send(JSON.stringify({ name: "otaProcessRollback", value: "false" }));
document.getElementById("rollback").style.display = "none";
document.getElementById("update").style.display = "block";
});
document.getElementById("otaRollback").addEventListener("click", function (e) {
socket.send(JSON.stringify({ name: "otaProcessRollback", value: "true" }));
document.getElementById("rollback").style.display = "none";
document.getElementById("update").style.display = "block";
});
</script>
<script> // основной старт скрипта, открыть сокет
// создать сокет по адресу
let protocol = "ws:"
if(document.location.protocol == "https:") protocol = "wss:"
let wsHostStr = protocol + "//" + document.location.host + document.location.pathname;
wsHostStr += (document.location.pathname == '/') ? "ws" : "/ws";
var socket = new WebSocket(wsHostStr);
socket.binaryType = "arraybuffer";
</script>
<script> // события WS
socket.onopen = function () {
console.log("connect ws");
};
socket.onclose = function (event) {
console.log("close ws - reload");
setTimeout(() => document.location.reload(), 2000);
};
socket.onerror = function () {
console.log("error ws");
setTimeout(() => document.location.reload(), 2000);
};
socket.onmessage = function (event) {
receiveWsData(event.data);
};
</script>
</body>
</html>