前言
OTA 是 “Over-The-Air” 的缩写,指的是通过无线通信网络(如Wi-Fi、蜂窝网络等)对设备进行固件或软件的更新和升级。这种更新方式允许在设备部署后远程进行固件更新,而无需物理连接到计算机或其他设备上。OTA技术在物联网(IoT)领域中非常常见,因为它提供了一种方便、快捷且经济的方式来更新大量分布在不同地点的设备。
ESP-IDF 提供两种方法执行无线 (OTA) 升级:使用组件提供的原生 API app_update;
使用组件提供的简化API esp_https_ota,提供作为客户端,通过HTTPS升级的功能。官方例程native_ota_example和simple_ota_example分别演示了这两组API的使用。
此博客不讲解OTA的原理,旨在使用原生API app_update + web server + bin加密的方式进行本地OTA演示,可应用在某些不能联外网的环境中,并可防止生产文件外泄。
参考例程:native_ota_example、simple_ota_example、pre_encrypted_ota。
准备
- Windows系统;
- OpenSSL (百度网盘:https://pan.baidu.com/s/19m1WAQzWCcY4QCTlNZduWg?pwd=0q6f 提取码:0q6f);
- 装了ESP-IDF的VsCode(ESP-IDF v5.2.1);
- ESP32开发板(ESP32、ESP32-S2、ESP32-S3皆可);
- 已经搭建好web server的工程(若不理解此工程,可访问此工程的教程)。
步骤
- 打开工程进入menuconfig输入Partition Table,在Partition Table选择Factory app, two OTA definitions,此举目的在于选择内置的分区表,可以看到给app程序分配的都为1M,若是编译代码后生成的bin文件大于1M,或者flash size不满足如此分配,则需要自定义分区表;
- (此步骤需要安装好OpenSSL)在工程的根目录下新建rsa_key文件夹,在电脑的开始菜单中输入并打开Win64 OpenSSL Command Prompt,进入到工程的根目录,并输入下面的命令,会自动生成一个RSA 私钥 private.pem,密钥的长度为 3072 位;
- 注意:此私钥是唯一的,移植OTA功能时,请重新生成此私钥;
openssl genrsa -out rsa_key/private.pem 3072
- 在工程的根目录下新建components文件夹,依照例程pre_encrypted_ota的README.md文件提示,将加密所需要的组件ESP Encrypted Image Abstraction Layer下载并解压到里面;
- 在main目录下添加ota.c和ota.h文件,在CMakeLists.txt中添加ota.c为源代码文件,将private.pem添加为嵌入文本文件,并调用组件提供的函数create_esp_enc_img。如此编译时,组件会将外部提供的RSA私钥与自动生成的 AES-GCM 密钥和初始化向量(IV)结合使用,生成一个*_secure.bin的文件;
idf_build_get_property(project_dir PROJECT_DIR)
idf_component_register(SRCS "station_example_main.c" "web_server.c" "ota.c"
INCLUDE_DIRS "."
EMBED_FILES "web_client.html"
EMBED_TXTFILES ${project_dir}/rsa_key/private.pem
)
create_esp_enc_img(${CMAKE_BINARY_DIR}/${CMAKE_PROJECT_NAME}.bin
${project_dir}/rsa_key/private.pem ${CMAKE_BINARY_DIR}/${CMAKE_PROJECT_NAME}_secure.bin app)
- 在ota.h文件添加内容:
#ifndef _OTA_H_
#define _OTA_H_
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
#include "web_server.h"
#include "cJSON.h"
typedef enum
{
TIMEOUT = -4, //超时
FORMAT_ERROR = -3, //格式错误
WRONG_VERSION = -2, //错误的版本
STATE_ERROR = -1, //状态错误
STOP = 0, //停止
READY=1, //就绪
UNDERWAY=2 , //进行中
SUCCEED=3, //成功
}OTA_STATE_TYPE;
int ota_send_state(OTA_STATE_TYPE state);
void ota_version_init(void);
int ota_rece_data( DATA_PARCEL* buffer);
int ota_control(cJSON* json_msg , int socket);
#endif
- 在ota.c添加内容:
#include "ota.h"
#include <string.h>
#include "esp_log.h"
#include "esp_encrypted_img.h"
#include "esp_app_format.h"
#include "esp_ota_ops.h"
#include "esp_wifi.h"
#include "driver/gpio.h"
typedef struct
{
char FileName[33]; //文件名字
uint32_t FileSize; //文件大小
uint32_t need_rece_num ; //需要接收的次数
uint32_t rece_num; //已经接收的次数
uint32_t rece_size; //已经接收大小
float rece_progress ; //接收进度%
OTA_STATE_TYPE state;
int ws_client;//客户端
}OTA_FILE_TYPE;
extern const char rsa_private_pem_start[] asm("_binary_private_pem_start");
extern const char rsa_private_pem_end[] asm("_binary_private_pem_end");
static QueueHandle_t ota_rece_data_queue = NULL ; //接收数据句柄
static TaskHandle_t ota_task_handle = NULL; //ota任务句柄
static esp_ota_handle_t ota_handle = 0 ; //ota句柄
static esp_decrypt_handle_t decrypt_handle= NULL ; //解码句柄
static OTA_FILE_TYPE bin_msg; //bin文件信息
#define HASH_LEN 32 /* SHA256 摘要长度*/
static const char *TAG = "OTA_TASK";
/*诊断功能*/
static bool diagnostic(void)
{
gpio_config_t io_conf;
io_conf.intr_type = GPIO_INTR_DISABLE;
io_conf.mode = GPIO_MODE_INPUT;
io_conf.pin_bit_mask = (1ULL << 4);
io_conf.pull_down_en = GPIO_PULLDOWN_DISABLE;
io_conf.pull_up_en = GPIO_PULLUP_ENABLE;
gpio_config(&io_conf);
ESP_LOGI(TAG, "Diagnostics (5 sec)...");
vTaskDelay(5000 / portTICK_PERIOD_MS);
bool diagnostic_is_ok = gpio_get_level(4);
gpio_reset_pin(4);
return diagnostic_is_ok;
}
static void print_sha256 (const uint8_t *image_hash, const char *label)
{
char hash_print[HASH_LEN * 2 + 1];
hash_print[HASH_LEN * 2] = 0;
for (int i = 0; i < HASH_LEN; ++i) {
sprintf(&hash_print[i * 2], "%02x", image_hash[i]);
}
ESP_LOGI(TAG, "%s: %s", label, hash_print);
}
/*删除ota任务*/
static void ota_delete_task(OTA_STATE_TYPE state)
{
printf("ota_delete_task state:%d\r\n",state);
esp_ota_abort(ota_handle);//结束OTA,释放句柄
ota_handle = 0;
ota_send_state(state);//将状态发送
vQueueDelete(ota_rece_data_queue);//删除消息队列
ota_rece_data_queue = NULL;
esp_encrypted_img_decrypt_end(decrypt_handle);//Esp加密img解密结束
decrypt_handle = NULL;
vTaskDelete(ota_task_handle);//删除OTA任务
}
/*解密 根据pre_encrypted_ota例程的函数修改*/
static esp_err_t decrypt(DATA_PARCEL *args)
{
esp_err_t err = ESP_FAIL;
pre_enc_decrypt_arg_t pargs = {};
pargs.data_in = (char *)args->data;//指向要解密的数据的指针
pargs.data_in_len = args->len;//数据长度
err = esp_encrypted_img_decrypt_data(decrypt_handle, &pargs);//开始解密,内部使用了malloc来存解析后的数据
if (err != ESP_OK && err != ESP_ERR_NOT_FINISHED) {//操作尚未完全完成
return err;
}
if (pargs.data_out_len > 0)
{
memset(args,0,sizeof(DATA_PARCEL));
memcpy(args->data,pargs.data_out,pargs.data_out_len);
args->len = pargs.data_out_len;
err = ESP_OK;
}
if(pargs.data_out!=NULL){
free(pargs.data_out);
}
return err;
}
/*发送回复信号*/
int ota_send_state(OTA_STATE_TYPE state)
{
bin_msg.state = state;//记录当前的状态
cJSON* json_parcel = cJSON_CreateObject(); // 创建JSON叶结构体
if (json_parcel == NULL)
{
cJSON_Delete(json_parcel);
return ESP_FAIL;
}
cJSON_AddNumberToObject(json_parcel,"state" ,state);
char *json_str = cJSON_Print(json_parcel);
if (json_str!=NULL) {
ws_server_send(json_str,strlen(json_str),bin_msg.ws_client);
cJSON_free(json_str); // 释放 cJSON_Print ()分配出来的内存空间
}
cJSON_Delete(json_parcel);
return ESP_OK;
}
/*ota任务*/
void ota_task(void *p)
{
printf("ota_task start\r\n");
esp_err_t err;
const esp_partition_t *update_partition = NULL;
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%08"PRIx32", but running from offset 0x%08"PRIx32,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%08"PRIx32")", running->type, running->subtype, running->address);
update_partition = esp_ota_get_next_update_partition(NULL);//获取下一个更新分区
assert(update_partition != NULL);
ESP_LOGI(TAG, "Writing to partition subtype %d at offset 0x%"PRIx32, update_partition->subtype, update_partition->address);
esp_decrypt_cfg_t cfg = {};
cfg.rsa_priv_key = rsa_private_pem_start;
cfg.rsa_priv_key_len = rsa_private_pem_end - rsa_private_pem_start;
decrypt_handle = esp_encrypted_img_decrypt_start(&cfg);
if (!decrypt_handle) {
ESP_LOGE(TAG, "OTA upgrade failed");
ota_delete_task(STATE_ERROR);
}
bool image_header_was_checked = false;
DATA_PARCEL ota_task_buffer;
uint32_t task_timeout = 0;
bin_msg.rece_size = 0;
bin_msg.rece_num = 0;
ota_send_state(READY);//已经准备就绪
while (1)
{
memset(&ota_task_buffer,0,sizeof(ota_task_buffer));
if(xQueueReceive(ota_rece_data_queue,&ota_task_buffer,pdMS_TO_TICKS(10)))
{
if (decrypt(&ota_task_buffer) != ESP_OK)//解密
{
ota_delete_task(FORMAT_ERROR);
}
if (image_header_was_checked == false)
{
if (ota_task_buffer.len > sizeof(esp_image_header_t) + sizeof(esp_image_segment_header_t) + sizeof(esp_app_desc_t) )
{
//下载时检查当前版本
esp_app_desc_t new_app_info;
memcpy(&new_app_info, &ota_task_buffer.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);//新传入固件的版本
esp_app_desc_t running_app_info;
if (esp_ota_get_partition_description(running, &running_app_info) == ESP_OK) //获取当前运行分区的描述
{
ESP_LOGI(TAG, "Running firmware version: %s", running_app_info.version);
}
const esp_partition_t* last_invalid_app = esp_ota_get_last_invalid_partition();//获取最后一次OTA无效存储的分区(文件写入后没有通过检测,被回滚)
esp_app_desc_t invalid_app_info;
if (esp_ota_get_partition_description(last_invalid_app, &invalid_app_info) == ESP_OK)
{
ESP_LOGI(TAG, "Last invalid firmware version: %s", invalid_app_info.version);//最后一个无效的固件版本
}
/*检查带有最后一个无效分区的当前版本,若是版本相同,则结束OTA*/
if (last_invalid_app != NULL)
{
if (memcmp(invalid_app_info.version, new_app_info.version, sizeof(new_app_info.version)) == 0) {
ESP_LOGW(TAG, "New version is the same as invalid version.");//新版本与无效版本相同
ESP_LOGW(TAG, "Previously, there was an attempt to launch the firmware with %s version, but it failed.", invalid_app_info.version);
ota_delete_task(WRONG_VERSION);
}
}
/*新版本与当当前运行版本作对比,若是版本相同,则结束OTA*/
if (memcmp(new_app_info.version, running_app_info.version, sizeof(new_app_info.version)) == 0)
{
ESP_LOGW(TAG, "Current running version is the same as a new. We will not continue the update.");//当前运行的版本与新版本相同。我们将不再继续更新
ota_delete_task(WRONG_VERSION);
}
image_header_was_checked = true;
err = esp_ota_begin(update_partition, OTA_WITH_SEQUENTIAL_WRITES, &ota_handle);
if (err != ESP_OK)
{
ESP_LOGE(TAG, "esp_ota_begin failed (%s)", esp_err_to_name(err));
ota_delete_task(STATE_ERROR);
}
ESP_LOGI(TAG, "esp_ota_begin succeeded");
}
else
{
ESP_LOGE(TAG, "received package is not fit len");
ota_delete_task(STATE_ERROR);
}
}
err = esp_ota_write( ota_handle, (const void *)ota_task_buffer.data, ota_task_buffer.len);
if (err != ESP_OK)
{
ota_delete_task(STATE_ERROR);
}
bin_msg.rece_size += ota_task_buffer.len; //记录已经接收了多少数据
bin_msg.rece_num++; //记录已经接收了的次数,用于判断是否已经接收完成整个bin文件
bin_msg.rece_progress = bin_msg.rece_size*100.f /bin_msg.FileSize; //用于记录当前的接收进度,不记录也行
printf("rece_size = %ld , rece_num = %ld , rece_progress = %.1f%%\r\n",bin_msg.rece_size,bin_msg.rece_num,bin_msg.rece_progress);
if (bin_msg.need_rece_num == bin_msg.rece_num){
break;
}
ota_send_state(UNDERWAY);//接收完成,接收下一帧
task_timeout = 0;
}
else
{
task_timeout++;
if (task_timeout==1000) //接收超时了
{
ota_delete_task(TIMEOUT);
}
}
}
err = esp_ota_end(ota_handle);
if (err != ESP_OK)
{
if (err == ESP_ERR_OTA_VALIDATE_FAILED)
{
ESP_LOGE(TAG, "Image validation failed, image is corrupted");
}
else
{
ESP_LOGE(TAG, "esp_ota_end failed (%s)!", esp_err_to_name(err));
}
ota_delete_task(STATE_ERROR);
}
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));
ota_delete_task(STATE_ERROR);
}
ESP_LOGI(TAG, "Prepare to restart system!");
ota_send_state(SUCCEED);//已经成功
vTaskDelay(100); //留些时间将成功的消息发出去
esp_restart();
/*不需要删除任务,因为已经要重启了*/
}
/*ota控制*/
int ota_control(cJSON* json_msg , int socket)
{
if(json_msg == NULL) {
return ESP_FAIL;
}
if (cJSON_GetObjectItem(json_msg,"stop"))
{
printf("OTA STOP\r\n");
ota_delete_task(STOP);
}
else if (cJSON_GetObjectItem(json_msg,"FileSize") && cJSON_GetObjectItem (json_msg,"FileName"))//收到启动OTA命令
{
if (bin_msg.state != STOP ) {
return ESP_FAIL;
}
bin_msg.FileSize=cJSON_GetObjectItem(json_msg,"FileSize")->valueint;
strncpy(bin_msg.FileName ,cJSON_GetObjectItem (json_msg,"FileName")->valuestring,sizeof(bin_msg.FileName));
bin_msg.need_rece_num = (bin_msg.FileSize/BUFFER_LEN);//获取需要接收的次数
if(bin_msg.FileSize%BUFFER_LEN){
bin_msg.need_rece_num+=1;
}
printf("OTA:%ld , FileSize:%ld\r",bin_msg.need_rece_num,bin_msg.FileSize);
vTaskDelay(1);
clear_other_client(socket); //清除其它的ws客户端
bin_msg.ws_client = socket; //保存客户端套接字,以便发送回复信号
if (ota_rece_data_queue == NULL)
{
ota_rece_data_queue = xQueueCreate(1, sizeof(DATA_PARCEL));//采用收完就回复的方式,不需要多个缓存
if (ota_rece_data_queue == NULL )
{
printf("ota_rece_data_queue ERROR\r\n");
return ESP_FAIL;
}
}
if(xTaskCreatePinnedToCore(ota_task,"ota_task", 10240 , NULL,20, &ota_task_handle, tskNO_AFFINITY) != pdPASS)
{
printf("xTaskCreatePinnedToCore ota_task error!\r\n");
return ESP_FAIL;
}
esp_wifi_set_ps(WIFI_PS_NONE); //确保禁用任何WiFi省电模式,这可以实现最佳吞吐量,从而为整体OTA操作提供最佳时间
}
else
{
return ESP_FAIL;
}
return ESP_OK;
}
/*OTA初始化*/
void ota_version_init(void)
{
printf("ota_version_init\r\n");
uint8_t sha_256[HASH_LEN] = { 0 };
esp_partition_t partition;
//获取分区表的sha256摘要
partition.address = ESP_PARTITION_TABLE_OFFSET;
partition.size = ESP_PARTITION_TABLE_MAX_LEN;
partition.type = ESP_PARTITION_TYPE_DATA;
esp_partition_get_sha256(&partition, sha_256);
print_sha256(sha_256, "SHA-256 for the partition table: ");
//为引导加载程序获取sha256摘要
partition.address = ESP_BOOTLOADER_OFFSET;
partition.size = ESP_PARTITION_TABLE_OFFSET;
partition.type = ESP_PARTITION_TYPE_APP;
esp_partition_get_sha256(&partition, sha_256);
print_sha256(sha_256, "SHA-256 for bootloader: ");
//获取运行分区的sha256摘要
esp_partition_get_sha256(esp_ota_get_running_partition(), sha_256);
print_sha256(sha_256, "SHA-256 for current firmware: ");
const esp_partition_t *running = esp_ota_get_running_partition();//获取正在运行的分区
esp_ota_img_states_t ota_state;
if (esp_ota_get_state_partition(running, &ota_state) == ESP_OK) {
if (ota_state == ESP_OTA_IMG_PENDING_VERIFY)
{
//运行诊断功能
bool diagnostic_is_ok = diagnostic();
if (diagnostic_is_ok) {
ESP_LOGI(TAG, "Diagnostics completed successfully! Continuing execution ...");//诊断成功完成!继续执行
esp_ota_mark_app_valid_cancel_rollback();
} else {
ESP_LOGE(TAG, "Diagnostics failed! Start rollback to the previous version ...");//诊断失败!开始回滚到以前的版本
esp_ota_mark_app_invalid_rollback_and_reboot();
}
}
}
}
/*去接收数据*/
int ota_rece_data(DATA_PARCEL* buffer)
{
int red = ESP_FAIL;
if (ota_rece_data_queue == NULL) {
return ESP_FAIL;
}
if (xQueueSend(ota_rece_data_queue , buffer , pdMS_TO_TICKS(10)))
{
red = ESP_OK;
}
return red;
}
- 在mian目录下添加version.txt文件。此文件用与版本控制,会自动写入bin文件中,且会在程序中以此作为版本号进行版本对比;
V1
- 若是不需要版本对比或者处于OTA功能调试的时候,则屏蔽ota.c中以下代码,否则需要频繁去修改version.txt的内容;
/*新版本与当当前运行版本作对比,若是版本相同,则结束OTA*/
if (memcmp(new_app_info.version, running_app_info.version, sizeof(new_app_info.version)) == 0)
{
ESP_LOGW(TAG, "Current running version is the same as a new. We will not continue the update.");//当前运行的版本与新版本相同。我们将不再继续更新
ota_delete_task(WRONG_VERSION);
}
- 在HTML文件中添加接口,脚本功能为:选择文件后将bin文件使用websocket协议拆解发送,收发方式为一问一答,并在过程中提供状态提示;
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WS OTA</title>
</head>
<body>
<h1>WS OTA</h1>
<p>
<button id="updata_but" onclick="updata_but()">固件升级</button>
<input type="file" id="updata_file" style="display: none;" onchange="updata_file()" accept=".bin">
<input type="text" readonly id="updata_state" onclick="updata_state(this)" value="点击选择文件">
</p>
</body>
</html>
<script type="text/javascript">
//本地主机地址
//烧录进ESP32时使用 "ws://"+window.location.host+"/ws"
//调试html时直接写 "ws://192.168.31.117/ws"
const ws_client = new WebSocket("ws://"+window.location.host+"/ws");
// const ws_client = new WebSocket("ws://192.168.31.117/ws");
/*ws_client连接成功事件*/
ws_client.onopen = function (event)
{
console.log("ws_client连接成功");
};
/*ws_client错误事件*/
ws_client.onerror = function(error)
{
console.log("ws_client错误");
};
/*ws_client接收数据*/
ws_client.onmessage = function (event)
{
const json_data = JSON.parse(event.data);
ota_rece_msg(json_data);
};
var BIN_SEND_INT_NUM = 0; //根据文件大小获得发送次数,用发送次数来控制发送的关闭
var BIN_SEND_CHUNK_SZIE = 1024 //每次发送的大小,需要和ESP32中数据缓存区大小一样
var BIN_SEND_DELAY = 1; // 每次尝试发送间隔ms
var BIN_SEND_NEXT_FRAME;//可以发下一帧
/*发送文件*/
function ws_send_file(file , state)
{
if(file.files[0])
{
BIN_SEND_INT_NUM = Math.ceil(file.files[0].size / BIN_SEND_CHUNK_SZIE);//向上取整,获取发送次数
const reader = new FileReader();
reader.onload = (event) =>
{
const data = event.target.result;
let offset = 0; //地址偏移
let progress = 0;//进展
function sendChunk()
{
if(BIN_SEND_NEXT_FRAME)
{
const chunk = data.slice(offset, offset + BIN_SEND_CHUNK_SZIE);
ws_client.send(chunk);
offset += chunk.byteLength;//下一帧数据的起始地址
BIN_SEND_INT_NUM--;
progress = offset/file.files[0].size*100;
state.value = "传输进度:"+ progress.toFixed(2) + "%" ;
if (BIN_SEND_INT_NUM>0)
{
setTimeout(sendChunk, BIN_SEND_DELAY); // 每次发送完成后等待 1 毫秒尝试再次启动
}
else if(offset > data.byteLength) {
return;
}
BIN_SEND_NEXT_FRAME = false;
}
else
{
setTimeout(sendChunk, BIN_SEND_DELAY); // 每次发送完成后等待 1 毫秒
}
}
sendChunk();
};
reader.readAsArrayBuffer(file.files[0]);
}
else
{
showAlert('文件错误');
}
}
function ota_rece_msg(json_msg)
{
var file = document.getElementById("updata_file");
var but = document.getElementById("updata_but");
var state= document.getElementById("updata_state");
if(json_msg.state <=0 )
{
but.textContent = initial_text;
file.value = null;
state.value = "点击选择文件";
BIN_SEND_INT_NUM = 0;
file.disabled = false;
if(json_msg.state == 0)
{
showAlert("已停止传输");
}
else if(json_msg.state == -1)
{
showAlert("设备状态错误");
}
else if(json_msg.state == -2)
{
showAlert("错误的版本");
}
else if(json_msg.state == -3)
{
showAlert("文件内容错误");
}
else if(json_msg.state == -4)
{
showAlert("传输超时");
}
}
else
{
if(json_msg.state == 1)//就绪
{
console.log("开始传输");
but.textContent = "停止传输";
file.disabled = true;
BIN_SEND_NEXT_FRAME = true;
ws_send_file(file ,state , BIN_SEND_INT_NUM);
}
else if(json_msg.state == 2)//进行中
{
BIN_SEND_NEXT_FRAME = true;
}
else if(json_msg.state == 3)//成功
{
showAlert("写入已完成");
}
}
}
/*固件升级按钮*/
function updata_but()
{
var but = document.getElementById("updata_but") ;
if(but.textContent == "固件升级")
{
var file = document.getElementById("updata_file").files[0];
if(file)
{
var parcel={};
parcel["FileName"] = file.name;
parcel["FileSize"] = file.size;
ws_client.send(JSON.stringify(parcel));
}
else {
showAlert("请选择文件");
}
}
else if(but.textContent == "停止传输")
{
var parcel={};
parcel["stop"] = "stop";
ws_client.send(JSON.stringify(parcel));
}
}
/*固件升级文件选择*/
function updata_file()
{
var file = document.getElementById("updata_file").files[0];
if (file) {
document.getElementById("updata_state").value = file.name;
}
else {
document.getElementById("updata_state").value ="点击选择文件"
}
}
/*固件升级状态*/
function updata_state()
{
if(!document.getElementById("updata_state").innerText.includes("传输进度"))
{
document.getElementById("updata_file").click();
}
}
function showAlert(message) {
// 在下一个事件循环中执行alert,使其非阻塞
setTimeout(function() {
alert(message);
}, 0);
}
</script>
- OTA以后系统重启,程序需要自测,自检不成功可能需要回退,将ota_version_init()函数放入app_main()的最前面。回退的相关说明参照官网说明;
void app_main(void)
{
ota_version_init();
//Initialize NVS
esp_err_t ret = nvs_flash_init();
if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
ESP_ERROR_CHECK(nvs_flash_erase());
ret = nvs_flash_init();
}
ESP_ERROR_CHECK(ret);
ESP_LOGI(TAG, "ESP_WIFI_MODE_STA");
wifi_init_sta();
web_server_init();
}
- 编译,烧录,可以看到build下多生成了一个main_secure.bin文件,此文件正是经过加密的文件,OTA时使用的就是它;
- 注意:若是要使用加密bin进行OTA,请在调试完成后,删除build,重新编译,以生成最新的main_secure.bin;
- 使电脑和ESP32连接同一个局域网,在浏览器输入ESP32被路由器分配的IP; - 按提示操作,选择main_secure.bin文件后点击“固件升级”。文件传输中,需要保持页面在最前端;
- 为简单起见,没有修改基础工程多少,因为一次只发1024字节,所以OTA进行得比较慢,若想要增快OTA速度,建议是定义一个较大缓存的的结构体放入OTA消息队列,同时在websocket接收回调函数中用malloc来创建能存储大量二进制数据的缓存;
- 写入完成;
- 写入完成重启后,需要再重启一次,OTA才算结束;
- 注意:decrypt()函数的功能是解密bin文件,若是OTA时传入的的bin文件是不加密的,则可屏蔽此函数。
资源获取
免费下载链接:ESP32_web_ota.zip
结束语
如果对你有用,请点个赞吧!