ESP32——用原生API app_update + web server + bin加密的方式进行本地OTA

前言

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

结束语

  如果对你有用,请点个赞吧!

  • 4
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
要在Zuul中使用WebSocket和SockJS,您需要进行以下配置: 1. 添加依赖项 ```xml <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-zuul</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-hystrix</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-zuul-websocket</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency> ``` 2. 配置Zuul路由 ```yml zuul: routes: websocket: path: /websocket/** url: ws://localhost:8081 ``` 这将把所有以“/websocket”开头的请求路由到WebSocket服务器上。 3. 配置SockJS ```java @Configuration @EnableWebSocketMessageBroker public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer { @Override public void configureMessageBroker(MessageBrokerRegistry config) { config.enableSimpleBroker("/topic"); config.setApplicationDestinationPrefixes("/app"); } @Override public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint("/websocket").setAllowedOrigins("*").withSockJS(); } } ``` 这将配置一个SockJS端点,它将处理所有以“/websocket”开头的请求,并使用简单的代理模式将消息转发到“/topic”目的地。 4. 启用Zuul ```java @SpringBootApplication @EnableZuulProxy public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } } ``` 这将启用Zuul代理,并将它们路由到相应的WebSocket服务器和SockJS端点。 现在,您应该可以在Zuul中使用WebSocket和SockJS了。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值