由于项目需要ESP32连接app进行OTA,为了支持AP模式下与STA模式下的本地局域网OTA功能(不需要OTA服务器)。
咨询乐鑫技术支持,ESP-IDF下没有该模式的官方例程。网上也一直没有找到相关例程,翻出来手册看了看倒也不难。基于esp-idf\examples\system\ota\native_ota_example与esp-idf\examples\http_server\file_serving两个例程,整理出来了这个demo分享并记录一下。
demo包含:
- wifi连接初始化(包括AP模式和STA模式)
- OTA服务器(端口89):包含固件上传页面URI、POST文件接收URI、当前固件信息查询URI
- 固件上传html:为原生js实现,post文件上传,上传进度及速度显示,错误显示
- 固件诊断程序:通过将GPIO2拉高判断固件是否运行成功,若失败则回滚固件
- BuildVer.sh:编译并根据编译时间生成版本号文件小工具
工程下载
分区表
分区表相关配置自行度娘,为节省flash空间该demo未使用factory工厂分区,使用sta_0分区作为默认分区
Name | Type | SubType | Offset | Size | Flags |
---|---|---|---|---|---|
nvs | data | nvs | 0x9000 | 16k | |
otadata | data | ota | 0xd000 | 8k | |
ota_0 | app | ota_0 | 1000k | ||
ota_1 | app | ota_1 | 1000k |
部分代码如下
1. OTA服务器初始化
首先初始化三个URI并加载到89端口上启动服务器,另以防意外将最大连接客户端数量设置为1(config.max_open_sockets = 1)
void HttpOTA_server_init()
{
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
config.max_open_sockets = 1;
config.stack_size = 8192;
config.server_port = 89;
config.ctrl_port = 32779;
ESP_LOGI(TAG, "Starting OTA server on port: '%d'", config.server_port);
if (httpd_start(&HttpOTA_httpd, &config) == ESP_OK)
{
httpd_uri_t HttpOTA_uri = { //OTA页面
.uri = "/HttpOTAWeb",
.method = HTTP_GET,
.handler = HttpOTA_handler,
.user_ctx = NULL
};
httpd_register_uri_handler(HttpOTA_httpd, &HttpOTA_uri);
httpd_uri_t Now_uri = { //当前固件信息
.uri = "/Now",
.method = HTTP_GET,
.handler = Now_handler,
.user_ctx = NULL
};
httpd_register_uri_handler(HttpOTA_httpd, &Now_uri);
/* URI处理程序,用于将文件上传到服务器*/
httpd_uri_t file_upload = {
.uri = "/upload",
.method = HTTP_POST,
.handler = upload_post_handler,
.user_ctx = NULL
};
httpd_register_uri_handler(HttpOTA_httpd, &file_upload);
}
}
2. 当前固件信息(以json字符串发送)
//当前固件信息
static esp_err_t Now_handler(httpd_req_t *req)
{
httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*"); //跨域传输协议
static char json_response[1024];
esp_app_desc_t running_app_info;
const esp_partition_t *running = esp_ota_get_running_partition();
esp_ota_get_partition_description(running, &running_app_info);
char * p = json_response;
*p++ = '{';
p+=sprintf(p, "\"OTAsubtype\":%d,", running->subtype - ESP_PARTITION_SUBTYPE_APP_OTA_MIN); //OTA分区
p+=sprintf(p, "\"address\":%d,", running->address); //地址
p+=sprintf(p, "\"version\":\"%s\",", running_app_info.version); //版本号
p+=sprintf(p, "\"date\":\"%s\",", running_app_info.date); //日期
p+=sprintf(p, "\"time\":\"%s\"", running_app_info.time); //时间
*p++ = '}';
*p++ = 0;
httpd_resp_set_type(req, "application/json"); // 设置http响应类型
return httpd_resp_send(req, json_response, strlen(json_response)); //发送一个完整的HTTP响应。内容在json_response中
}
3. 固件上传页面
使用Esp32HttpWeb_OTA\main\html\www\compress_pages.sh工具压缩为.gz网页烧写。
注意: html中的中文会被压缩成乱码。
注意: 需要在CMakeLfists.txt编译配置文件下注明该网页:
#嵌入二进制文件,该文件不会被格式化为c源文件,当前为压缩网页文件
set(COMPONENT_EMBED_FILES
"html/www/HttpOTA.html.gz"
)
固件上传页面URI:
//OTA 页面
static esp_err_t HttpOTA_handler(httpd_req_t *req)
{
httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*"); //跨域传输协议
extern const unsigned char HttpOTA_html_gz_start[] asm("_binary_HttpOTA_html_gz_start");
extern const unsigned char HttpOTA_html_gz_end[] asm("_binary_HttpOTA_html_gz_end");
size_t HttpOTA_html_gz_len = HttpOTA_html_gz_end - HttpOTA_html_gz_start;
httpd_resp_set_type(req, "text/html");
httpd_resp_set_hdr(req, "Content-Encoding", "gzip");
return httpd_resp_send(req, (const char *)HttpOTA_html_gz_start, HttpOTA_html_gz_len);
}
4. 固件接收URI
循环每次接收1k数据到缓冲区。当接收到完整固件头部后( 接收包大于 sizeof(esp_image_header_t) + sizeof(esp_image_segment_header_t) + sizeof(esp_app_desc_t) )
则判断固件是否合法(该固件仅判断了版本号是否为包含"ESP_"),可自行添加判断是否与正在运行固件版本号相同等,esp-idf\examples\system\ota\native_ota_example例程中包含相关代码。
/* 单个文件的最大大小*/
#define MAX_FILE_SIZE (1000*1024) // 1000 KB
#define MAX_FILE_SIZE_STR "1000KB"
/* 暂存缓冲区大小*/
#define SCRATCH_BUFSIZE 1024
/*将文件上传到服务器的处理程序*/
uint8_t Upload_Timeout_num;
static esp_err_t upload_post_handler(httpd_req_t *req)
{
httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*"); //跨域传输协议
esp_err_t err;
esp_ota_handle_t update_handle = 0 ;
const esp_partition_t *update_partition = NULL;
char SendStr[100];
Upload_Timeout_num = 0;
/* 文件不能大于限制*/
if (req->content_len > MAX_FILE_SIZE) {
ESP_LOGE(TAG, "文件过大 : %d bytes", req->content_len);
/* 回应400错误请求 */
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST,
"File size must be less than"
MAX_FILE_SIZE_STR "!");
/* 返回失败以关闭基础连接,否则传入的文件内容将使套接字繁忙 */
return ESP_FAIL;
}
/*请求的内容长度给出了要上传的文件的大小*/
int remaining = req->content_len;
int received,L_remaining = remaining;
bool image_header_was_checked = false; //固件头检查标识
char *OTA_buf = malloc(sizeof(char) * SCRATCH_BUFSIZE);
while (remaining > 0) {
// char str[6];
// sprintf(str,"%.2f",(float)(L_remaining-remaining)/L_remaining*100);
// ESP_LOGI(TAG, "剩余尺寸 : %d---%s%%", remaining,str);
/* 将文件部分接收到缓冲区中 */
if ((received = httpd_req_recv(req, OTA_buf, MIN(remaining, SCRATCH_BUFSIZE))) <= 0) {
if (received == HTTPD_SOCK_ERR_TIMEOUT) {
Upload_Timeout_num++;
ESP_LOGE(TAG, "接收文件超时 %d", Upload_Timeout_num);
/* 如果发生超时,请重试 */
if (Upload_Timeout_num >= 3)
{
Upload_Timeout_num = 0;
ESP_LOGE(TAG, "超时过多!");
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "File receiving timeout!");
return ESP_FAIL;
}
continue;
}
/* 如果出现无法恢复的错误,请关闭并删除未完成的文件*/
free(OTA_buf);
ESP_LOGE(TAG, "文件接收失败!");
/* 响应500内部服务器错误 */
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Unable to receive file!");
if(update_handle) esp_ota_end(update_handle); //若已begin OTA则停止OTA
return ESP_FAIL;
}
/*固件头校验*/
//接收到固件头
if (image_header_was_checked == false) {
esp_app_desc_t new_app_info; //存储新固件头
if (received > sizeof(esp_image_header_t) + sizeof(esp_image_segment_header_t) + sizeof(esp_app_desc_t)) {
esp_app_desc_t running_app_info;
const esp_partition_t *running = esp_ota_get_running_partition();
if (esp_ota_get_partition_description(running, &running_app_info) == ESP_OK)
{
ESP_LOGI(TAG, "当前运行固件版本: %s", running_app_info.version);
ESP_LOGI(TAG, "当前运行固件编译时间: %s,%s", running_app_info.date, running_app_info.time);
}
// 通过下载检查新固件版本
memcpy(&new_app_info, &OTA_buf[sizeof(esp_image_header_t) + sizeof(esp_image_segment_header_t)], sizeof(esp_app_desc_t));
if (strstr(new_app_info.version, "ESP_") == NULL) //版本错误
{
ESP_LOGE(TAG, "新固件头错误");
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Firmware header error!");
return ESP_FAIL;
}
ESP_LOGI(TAG, "新固件版本: %s", new_app_info.version);
ESP_LOGI(TAG, "新固件编译时间: %s, %s", new_app_info.date, new_app_info.time);
固件头校验完成后,配置新固件烧写目标分区并调用esp_ota_begin( )开始OTA,注意该步骤将擦除目标OTA分区。
注意: ESP32 OTA必须由esp_ota_begin( )开始并由esp_ota_end( )结束。
//返回下一个应使用新固件写入的OTA应用程序分区
#if 1
//esp_ota_get_next_update_partition 自动选择下一个可用ota分区
update_partition = esp_ota_get_next_update_partition(NULL);
ESP_LOGI(TAG, "写入分区子类型 %#X 偏移 %#x", update_partition->subtype, update_partition->address);
if (update_partition == NULL)
{
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "OTA Partition error!");
return ESP_FAIL;
}
#else
//手动选择ota分区
if (running->subtype == ESP_PARTITION_SUBTYPE_APP_OTA_0)
{
update_partition = esp_partition_find_first(ESP_PARTITION_TYPE_APP, ESP_PARTITION_SUBTYPE_APP_OTA_1, NULL);
}else{
update_partition = esp_partition_find_first(ESP_PARTITION_TYPE_APP, ESP_PARTITION_SUBTYPE_APP_OTA_0, NULL);
}
ESP_LOGI(TAG, "写入分区子类型 %#X 偏移 %#x", update_partition->subtype, update_partition->address);
if (update_partition == NULL)
{
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "OTA Partition error!");
return ESP_FAIL;
}
#endif
sprintf(SendStr, "To: OTA%d Ver: %s Time: %s, %s",
update_partition->subtype - ESP_PARTITION_SUBTYPE_APP_OTA_MIN,
new_app_info.version, new_app_info.date, new_app_info.time);
//开始OTA OTA_SIZE_UNKNOWN将擦除整个分区
err = esp_ota_begin(update_partition, OTA_SIZE_UNKNOWN, &update_handle);
if (err != ESP_OK) {
char str[25];
sprintf(str, "esp_ota_begin failed (%s)", esp_err_to_name(err));
ESP_LOGE(TAG, "%s", str);
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, str);
return ESP_FAIL;
}
ESP_LOGI(TAG, "esp_ota_begin succeeded");
image_header_was_checked = true; //固件头验证完成 可自行添加版本比对
}else{
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "received package is not fit len!");
return ESP_FAIL;
}
}
循环将每次接收到的数据写入目标OTA分区 esp_ota_write( );
/*将固件分块写入OTA分区*/
err = esp_ota_write(update_handle, (const void *)OTA_buf, received);
if (err != ESP_OK) {
char str[25];
sprintf(str, "esp_ota_write failed (%s)", esp_err_to_name(err));
ESP_LOGE(TAG, "%s", str);
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, str);
return ESP_FAIL;
}
/*跟踪剩余要上传的文件的剩余大小*/
remaining -= received;
}
free(OTA_buf);
ESP_LOGI(TAG, "文件接收完成: %dByte",L_remaining);
整个固件接收完成后调用esp_ota_end( )结束OTA并自动校验固件完整性。
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");
}
char str[25];
sprintf(str, "esp_ota_end failed (%s)", esp_err_to_name(err));
ESP_LOGE(TAG, "%s", str);
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, str);
return ESP_FAIL;
}
ESP_LOGI(TAG, "固件校验成功!");
最后调用esp_ota_set_boot_partition( )将otadata配置为新OTA分区(下次上电启动分区)
并软件复位 esp_restart();
注意: httpd_resp_sendstr后若无延时,客户端网页将接受不到成功回复。
err = esp_ota_set_boot_partition(update_partition);
if (err != ESP_OK) {
char str[50];
sprintf(str, "esp_ota_set_boot_partition failed (%s)", esp_err_to_name(err));
ESP_LOGE(TAG, "%s", str);
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, str);
return ESP_FAIL;
}
// httpd_resp_sendstr(req, "OTA successfully");
httpd_resp_sendstr(req,SendStr);
vTaskDelay(500 / portTICK_PERIOD_MS); //延时等待消息发送
ESP_LOGI(TAG, "准备重启系统!");
esp_restart();
return ESP_OK;
}