前言
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 + bin加密的方式在Web中对ESP32进行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;