Files
zh_ota_server/zh_ota_server.c
2025-11-17 16:19:14 +03:00

421 lines
14 KiB
C

/*
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;
}