天天看點

ESP32 HttpServer模式下 本地OTA 例程(基于ESP-IDF類似Arduino下OTAWebUpdater例程)

由于項目需要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包含:

  1. wifi連接配接初始化(包括AP模式和STA模式)
  2. OTA伺服器(端口89):包含固件上傳頁面URI、POST檔案接收URI、目前固件資訊查詢URI
  3. 固件上傳html:為原生js實作,post檔案上傳,上傳進度及速度顯示,錯誤顯示
  4. 固件診斷程式:通過将GPIO2拉高判斷固件是否運作成功,若失敗則復原固件
  5. BuildVer.sh:編譯并根據編譯時間生成版本号檔案小工具

工程下載下傳

ESP32 HttpServer模式下 本地OTA 例程(基于ESP-IDF類似Arduino下OTAWebUpdater例程)

https://download.csdn.net/download/l851285812/18808145

分區表

分區表相關配置自行度娘,為節省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;
}
           

固件上傳網頁

ESP32 HttpServer模式下 本地OTA 例程(基于ESP-IDF類似Arduino下OTAWebUpdater例程)