OTA升级
前言
OTA(Over the air)即空中下载技术,通过网络远程为设备更新软件程序。
首先将需要升级的固件文件上传至涂鸦服务器,配置好升级操作。蓝牙/WIFI模组收到云端推送后会根据以下协议将文件进行分包传输,最终 MCU 接收升级文件并写入本地闪存,实现固件的升级。
1:环境
VSCODE+idf5.4
ESP32 C3
2:上代码
main.c
#include <stdio.h>
#include <string.h>
#include "esp_system.h"
#include "esp_event.h"
#include "esp_log.h"
#include "esp_ota_ops.h"
#include "esp_http_client.h"
#include "esp_flash_partitions.h"
#include "esp_partition.h"
#include "nvs.h"
#include "nvs_flash.h"
#include "esp_wifi.h"
#include "driver/gpio.h"
#include "esp_timer.h"// 固件版本(100=1.0,200=2.0,便于整数比较)
#define FIRMWARE_VERSION 100
// 版本号字符串(用于HTTP请求)
#define FIRMWARE_VERSION_STR "1.0"// LED引脚定义
#define LED_PIN_1 GPIO_NUM_12
#define LED_PIN_2 GPIO_NUM_13// WiFi配置
#define WIFI_SSID "****"
#define WIFI_PASSWORD "******"// OTA配置 //http://192.168.1.3:8080/download?version=1.0 // "%s?version=%s", OTA_SERVER_BASE_URL, FIRMWARE_VERSION_STR);
#define OTA_SERVER_BASE_URL "http://192.168.1.3:8080/download" // 基础URL,不带版本参数
#define OTA_TIMEOUT_MS 40000 // 单次请求超时时间
#define MAX_RETRY_COUNT 3 // 最大重试次数
#define RETRY_DELAY_MS 50000 // 重试间隔
#define COOLDOWN_PERIOD_MS 60000 // 冷却时间static const char *TAG = "OTA_VERSION";
static bool s_ota_finish = false;
static int binary_file_length = 0;
static bool s_wifi_connected = false;// LED状态控制
typedef enum {LED_STATE_IDLE, // 正常状态LED_STATE_CONNECTING, // 连接中(慢闪)LED_STATE_FAILED, // 连接失败(快闪)LED_STATE_SUCCESS // 成功(双闪)
} LedState;static void set_led_state(LedState state) {static uint32_t last_tick = 0;static bool blink_flag = false;switch (state) {case LED_STATE_IDLE:gpio_set_level(LED_PIN_1, 1);gpio_set_level(LED_PIN_2, 0);break;case LED_STATE_CONNECTING:if (esp_timer_get_time() / 1000 - last_tick > 1000) {blink_flag = !blink_flag;gpio_set_level(LED_PIN_1, blink_flag ? 1 : 0);last_tick = esp_timer_get_time() / 1000;}break;case LED_STATE_FAILED:if (esp_timer_get_time() / 1000 - last_tick > 200) {blink_flag = !blink_flag;gpio_set_level(LED_PIN_1, blink_flag ? 1 : 0);gpio_set_level(LED_PIN_2, blink_flag ? 1 : 0);last_tick = esp_timer_get_time() / 1000;}break;case LED_STATE_SUCCESS:gpio_set_level(LED_PIN_1, 1);gpio_set_level(LED_PIN_2, 1);vTaskDelay(pdMS_TO_TICKS(500));gpio_set_level(LED_PIN_1, 0);gpio_set_level(LED_PIN_2, 0);vTaskDelay(pdMS_TO_TICKS(200));gpio_set_level(LED_PIN_1, 1);gpio_set_level(LED_PIN_2, 1);break;}
}// 初始化LED
static void led_init(void) {gpio_reset_pin(LED_PIN_1);gpio_reset_pin(LED_PIN_2);gpio_set_direction(LED_PIN_1, GPIO_MODE_OUTPUT);gpio_set_direction(LED_PIN_2, GPIO_MODE_OUTPUT);#if FIRMWARE_VERSION == 100set_led_state(LED_STATE_IDLE);#elif FIRMWARE_VERSION == 200gpio_set_level(LED_PIN_1, 1);gpio_set_level(LED_PIN_2, 1);#endif
}// 获取下一个OTA分区
static const esp_partition_t* get_next_ota_partition(void) {const esp_partition_t* running = esp_ota_get_running_partition();if (!running) {ESP_LOGE(TAG, "get current partition failed");//获取当前分区失败");return NULL;}const esp_partition_t* ota0 = esp_partition_find_first(ESP_PARTITION_TYPE_APP, (esp_partition_subtype_t)ESP_PARTITION_SUBTYPE_APP_OTA_0, NULL);const esp_partition_t* ota1 = esp_partition_find_first(ESP_PARTITION_TYPE_APP, (esp_partition_subtype_t)ESP_PARTITION_SUBTYPE_APP_OTA_1, NULL);if (!ota0 || !ota1) {ESP_LOGE(TAG, "OTA partition no found,please check partition talbe");return NULL;}uint8_t current_subtype = running->subtype;if (current_subtype == ESP_PARTITION_SUBTYPE_APP_FACTORY || current_subtype == ESP_PARTITION_SUBTYPE_APP_OTA_0) {return ota1;} else if (current_subtype == ESP_PARTITION_SUBTYPE_APP_OTA_1) {return ota0;}ESP_LOGE(TAG, "unknow partition type:%d", current_subtype);return NULL;
}// WiFi事件处理
static void event_handler(void* arg, esp_event_base_t event_base,int32_t event_id, void* event_data) {if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) {esp_wifi_connect();set_led_state(LED_STATE_CONNECTING);} else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) {s_wifi_connected = false;ESP_LOGW(TAG, "WiFi disconnected,try reconnect...");esp_wifi_connect();set_led_state(LED_STATE_CONNECTING);} else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) {s_wifi_connected = true;ip_event_got_ip_t* event = (ip_event_got_ip_t*) event_data;ESP_LOGI(TAG, "get IP:" IPSTR, IP2STR(&event->ip_info.ip));set_led_state(LED_STATE_IDLE);}
}// 初始化WiFi
static void wifi_init_sta(void) {ESP_ERROR_CHECK(nvs_flash_init());ESP_ERROR_CHECK(esp_netif_init());ESP_ERROR_CHECK(esp_event_loop_create_default());esp_netif_create_default_wifi_sta();wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();ESP_ERROR_CHECK(esp_wifi_init(&cfg));ESP_ERROR_CHECK(esp_event_handler_register(WIFI_EVENT, ESP_EVENT_ANY_ID, &event_handler, NULL));ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT, IP_EVENT_STA_GOT_IP, &event_handler, NULL));wifi_config_t wifi_config = {.sta = {.ssid = WIFI_SSID,.password = WIFI_PASSWORD,},};ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config));ESP_ERROR_CHECK(esp_wifi_start());ESP_LOGI(TAG, "WiFi init finish,connecting %s...", WIFI_SSID);
}// 等待WiFi连接(带超时)
static bool wait_for_wifi_connect(uint32_t timeout_ms) {uint32_t start_tick = esp_timer_get_time() / 1000;while (!s_wifi_connected) {if (esp_timer_get_time() / 1000 - start_tick > timeout_ms) {ESP_LOGE(TAG, "WiFi connect timeout(%ldms)", timeout_ms);return false;}vTaskDelay(pdMS_TO_TICKS(100));}return true;
}// 构建带版本号的OTA请求URL
static void build_ota_url(char* url_buf, size_t buf_len) {// 格式: 基础URL + "?version=当前版本号"snprintf(url_buf, buf_len, "%s?version=%s", OTA_SERVER_BASE_URL, FIRMWARE_VERSION_STR);
}// 版本比较函数(辅助函数)
// 返回值:1(server > current),0(相等),-1(server < current)
static int version_compare(const char *server_ver, const char *current_ver) {// 简单实现:按"主版本.次版本"格式比较(如"2.1" > "2.0")int s_major, s_minor, c_major, c_minor;if (sscanf(server_ver, "%d.%d", &s_major, &s_minor) != 2) {ESP_LOGE(TAG, "server version format:%s", server_ver);return -1; // 视为版本更低}if (sscanf(current_ver, "%d.%d", &c_major, &c_minor) != 2) {ESP_LOGE(TAG, "current version farmat:%s", current_ver);return 1; // 视为版本更低}if (s_major > c_major) return 1;if (s_major < c_major) return -1;if (s_minor > c_minor) return 1;if (s_minor < c_minor) return -1;return 0;
}
// HTTP事件处理(增加版本验证逻辑)
static char server_version[32] = {0};
static esp_err_t http_event_handler(esp_http_client_event_t *evt) {static esp_ota_handle_t update_handle = 0;static const esp_partition_t *update_partition = NULL;static int binary_file_downloaded = 0;ESP_LOGE(TAG, "http_event_handler:%d",evt->event_id);switch(evt->event_id) {case HTTP_EVENT_ERROR:ESP_LOGE(TAG, "HTTP error:connect failed");break;case HTTP_EVENT_ON_CONNECTED:ESP_LOGI(TAG, "HTTP connected");break;case HTTP_EVENT_HEADER_SENT:break;case HTTP_EVENT_ON_HEADER:{// 处理state=-1的异常(头部解析失败)if (evt->data_len == -1) {ESP_LOGE(TAG, "head parse fail=-1");break;}// 解析自定义头部X-File-Version(服务器返回的固件版本)if (evt->header_key && evt->header_value && strcasecmp(evt->header_key, "X-File-Version") == 0) {// 保存服务器版本号(如"2.0")// ESP_LOGI(TAG, "server version:%s", evt->header_value);if(evt->header_value[0]=='v' ||evt->header_value[0]=='V'){strncpy(server_version, evt->header_value+1, sizeof(server_version)-1);}else{strncpy(server_version, evt->header_value, sizeof(server_version)-1);}ESP_LOGI(TAG, "server version:%s", server_version);// 与当前版本比较(示例:当前版本为"1.0")if (version_compare(server_version, FIRMWARE_VERSION_STR) <= 0) {ESP_LOGI(TAG, "cur version%s", FIRMWARE_VERSION_STR);binary_file_length = -1; // 标记无需更新}}// 解析Content-Length(仅当需要更新时)else if (binary_file_length != -1 && evt->header_key && evt->header_value &&strcasecmp(evt->header_key, "Content-Length") == 0) {binary_file_length = atoi(evt->header_value);ESP_LOGI(TAG, "固件大小:%d bytes", binary_file_length);}}break;case HTTP_EVENT_ON_DATA:if (binary_file_length > 0) { // 仅当需要更新且有文件大小时处理if (update_handle == 0) {update_partition = get_next_ota_partition();if (!update_partition) {return ESP_FAIL;}ESP_LOGI(TAG, "write partition: %s", update_partition->label);esp_err_t err = esp_ota_begin(update_partition, OTA_SIZE_UNKNOWN, &update_handle);if (err != ESP_OK) {ESP_LOGE(TAG, "OTA begin fail: %s", esp_err_to_name(err));return err;}}esp_err_t err = esp_ota_write(update_handle, evt->data, evt->data_len);if (err != ESP_OK) {ESP_LOGE(TAG, "OTA write fial:%s", esp_err_to_name(err));return err;}binary_file_downloaded += evt->data_len;ESP_LOGI(TAG, "download progress: %d/%d (%.1f%%)",binary_file_downloaded, binary_file_length,(binary_file_downloaded * 100.0f) / binary_file_length);}break;case HTTP_EVENT_ON_FINISH:if (binary_file_length == -1) {// 版本相同,无需更新ESP_LOGI(TAG, "HTTP request finish,no need update");//HTTP} else if (update_handle != 0 && update_partition) {// 完成更新esp_err_t err = esp_ota_end(update_handle);if (err != ESP_OK) {ESP_LOGE(TAG, "OTA end fail: %s", esp_err_to_name(err));return err;}err = esp_ota_set_boot_partition(update_partition);if (err != ESP_OK) {ESP_LOGE(TAG, "set setup partition fail:%s", esp_err_to_name(err));return err;}s_ota_finish = true;}break;case HTTP_EVENT_DISCONNECTED:ESP_LOGI(TAG, "HTTP disconnected");break;default:break; }return ESP_OK;
}// 执行单次OTA尝试(带版本参数)
// 执行单次OTA尝试(带版本参数)
static esp_err_t ota_try_once(void) {if (!s_wifi_connected) {ESP_LOGW(TAG, "WiFi connect,retry reconnect...");if (!wait_for_wifi_connect(10000)) {return ESP_ERR_TIMEOUT;}}// 构建带版本号的URLchar ota_url[256];build_ota_url(ota_url, sizeof(ota_url));ESP_LOGI(TAG, "request OTA update:%s", ota_url);// 创建一个 HTTP 客户端配置esp_http_client_config_t config = {.method = HTTP_METHOD_GET,.url = ota_url,.event_handler = http_event_handler,.disable_auto_redirect = true,};esp_http_client_handle_t client = esp_http_client_init(&config);if (!client) {ESP_LOGE(TAG, "HTTP client init fail");return ESP_FAIL;}// 使用goto标签确保资源清理(替代defer)esp_err_t err = ESP_OK;ESP_LOGE(TAG, "esp_http_client_perform");// 发送请求err = esp_http_client_perform(client);if (err != ESP_OK) {ESP_LOGE(TAG, "HTTP request fail: %s", esp_err_to_name(err));goto cleanup; // 出错时跳转到清理}ESP_LOGE(TAG, "HTTP client perform err=%d",err);// 处理服务器响应状态码int status_code = esp_http_client_get_status_code(client);if (status_code == 304) {// 304表示版本相同,无需更新(成功状态)err = ESP_OK;} else if (status_code != 200) {ESP_LOGE(TAG, "server return status code: %d", status_code);err = ESP_ERR_HTTP_CONNECT;}cleanup: // 集中清理资源esp_http_client_cleanup(client); // 无论成功失败都执行清理return err;
}// OTA任务(带版本检查逻辑)
static void ota_task(void *pvParameters) {int retry_count = 0;while (retry_count < MAX_RETRY_COUNT) {ESP_LOGI(TAG, "OTA try %d/%d(cur version:%s)", retry_count + 1, MAX_RETRY_COUNT, FIRMWARE_VERSION_STR);set_led_state(LED_STATE_CONNECTING);binary_file_length = 0; // 重置文件长度标记esp_err_t err = ota_try_once();// 处理结果if (err == ESP_OK) {if (s_ota_finish) {// 升级成功set_led_state(LED_STATE_SUCCESS);ESP_LOGI(TAG, "OTA update success,restarting...");vTaskDelay(pdMS_TO_TICKS(1000));esp_restart();return;} else if (binary_file_length == -1) {// 版本相同,无需更新ESP_LOGI(TAG, "current version is the newest version,exit ota task");set_led_state(LED_STATE_IDLE);vTaskDelete(NULL);return;}}// 失败重试retry_count++;set_led_state(LED_STATE_FAILED);ESP_LOGW(TAG, "OTA try %d fail,remain count:%d", retry_count, MAX_RETRY_COUNT - retry_count);if (retry_count >= MAX_RETRY_COUNT) {ESP_LOGI(TAG, "retyr max count,sleep(%dS)", COOLDOWN_PERIOD_MS / 1000);vTaskDelay(pdMS_TO_TICKS(COOLDOWN_PERIOD_MS));retry_count = 0;} else {vTaskDelay(pdMS_TO_TICKS(RETRY_DELAY_MS));}}vTaskDelete(NULL);
}void app_main(void) {led_init();wifi_init_sta();// 等待WiFi初始连接if (!wait_for_wifi_connect(15000)) {ESP_LOGW(TAG, "WiFi connect fail,back retry connect");}// 创建OTA任务xTaskCreate(&ota_task, "ota_task", 8192, NULL, 3, NULL);int printfcount =0;bool bflag = false;const uint32_t firmwarever = FIRMWARE_VERSION/100;// 主循环while (1) {vTaskDelay(pdMS_TO_TICKS(1000));if(printfcount < 300 && ((++printfcount &31) ==1) ){ESP_LOGI(TAG, "running version(%s)", FIRMWARE_VERSION_STR);}bflag =!bflag;//单数版本 亮灭一个LED,双数 亮灭 2个LEDif(bflag){if((firmwarever &1)==1){gpio_set_level(LED_PIN_1, 1);}else{gpio_set_level(LED_PIN_1, 1);gpio_set_level(LED_PIN_2, 1);}}else{if((firmwarever &1)==1){gpio_set_level(LED_PIN_1, 0);}else{gpio_set_level(LED_PIN_1, 0);gpio_set_level(LED_PIN_2, 0);}}}
}
httpserver_file
package mainimport ("context""fmt""log""net/http""os""os/signal""path/filepath""strconv""strings""syscall""time"
)const (baseDir = "./downloads" // 文件存储基础目录latestVersion = "v2.0" // 服务器最新版本号downloadfilename = "testota2.bin" //下载文件名
)var (gobal_verson = "" //简单使用,就不加锁了 //正式时,记得加锁
)func setupRouter() http.Handler {mux := http.NewServeMux()// 注册路由
// mux.HandleFunc("/", homeHandler)mux.HandleFunc("/download", downloadHandler)mux.HandleFunc("/version", versionHandler)// 使用中间件包装路由return loggingMiddleware(mux)
}func main() {// http.HandleFunc("/download", downloadHandler)// log.Println("Server started at :8080")gobal_verson = latestVersionlog.Printf("Latest server version: %s", gobal_verson)server := &http.Server{Addr: ":8080",Handler: setupRouter(),ReadTimeout: 5 * time.Second,WriteTimeout: 50 * time.Second,IdleTimeout: 30 * time.Second,}// 启动服务器的goroutinego func() {log.Printf("服务器启动,监听端口: %s\n", server.Addr)if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {log.Fatalf("服务器启动失败: %v\n", err)}}()// 优雅关闭服务器waitForShutdown(server)//log.Fatal(http.ListenAndServe(":8080", nil))
}// http://192.168.1.3:8080/download?version=1&file=123.txt
func downloadHandler(w http.ResponseWriter, r *http.Request) {log.Printf("recv client remote ip=%v", r.RemoteAddr)// 获取客户端请求的版本号clientVersion := r.URL.Query().Get("version")// 获取文件名参数filename := r.URL.Query().Get("file")if filename == "" {//http.Error(w, "Missing file parameter", http.StatusBadRequest)//returnfilename = downloadfilename}// 比较客户端版本和服务器最新版本versionStatus := compareVersions(clientVersion, gobal_verson)switch versionStatus {case -1: // 客户端版本较旧log.Printf("Client version %s is older than server (%s), serving latest",clientVersion, latestVersion)serveFile(w, r, gobal_verson, filename)case 0: // 版本相同log.Printf("Client has latest version (%s), returning OK", latestVersion)w.Header().Set("Content-Type", "text/plain")w.Header().Set("X-File-Version", gobal_verson)w.WriteHeader(http.StatusNotModified) //(http.StatusOK)w.Write([]byte("OK"))case 1: // 客户端版本较新(理论上不会发生)log.Printf("Client version %s is newer than server (%s), serving requested version",clientVersion, gobal_verson)serveFile(w, r, clientVersion, filename)default:// 无效版本号,尝试提供最新版本log.Printf("Invalid client version: %s, serving latest", clientVersion)serveFile(w, r, gobal_verson, filename)}
}// 版本比较函数
// 返回: -1 (client < server), 0 (相等), 1 (client > server), -2 (无效版本)
func compareVersions(clientVer, serverVer string) int {// 处理空版本号情况if clientVer == "" {return -1}// 简单化处理:提取数字部分比较extractNumbers := func(v string) []int {v = strings.TrimPrefix(v, "v")parts := strings.Split(v, ".")nums := make([]int, 0, len(parts))for _, p := range parts {num, err := strconv.Atoi(p)if err != nil {return nil // 无效版本}nums = append(nums, num)}return nums}clientNums := extractNumbers(clientVer)serverNums := extractNumbers(serverVer)// 无效版本格式if clientNums == nil || serverNums == nil {return -2}// 比较每个数字部分for i := 0; i < len(clientNums) && i < len(serverNums); i++ {if clientNums[i] < serverNums[i] {return -1}if clientNums[i] > serverNums[i] {return 1}}// 如果共同部分相同,比较长度if len(clientNums) < len(serverNums) {return -1}if len(clientNums) > len(serverNums) {return 1}return 0 // 完全相等
}// 安全提供文件
func serveFile(w http.ResponseWriter, r *http.Request, version, filename string) {// 安全构造文件路径filePath, err := safeFilePath(version, filename)if err != nil {http.Error(w, "Invalid file path", http.StatusBadRequest)return}// 检查文件是否存在if _, err := os.Stat(filePath); os.IsNotExist(err) {http.Error(w, "File not found", http.StatusNotFound)return}// 设置响应头w.Header().Set("Content-Disposition",fmt.Sprintf("attachment; filename=\"%s\"", filename))w.Header().Set("Content-Type", "application/octet-stream")w.Header().Set("X-File-Version", version) // 在响应头中返回实际使用的版本号// 发送文件内容http.ServeFile(w, r, filePath)log.Printf("Served file: %s (Version: %s)", filename, version)
}// 安全构造文件路径,防止路径遍历攻击
func safeFilePath(version, filename string) (string, error) {// 清理路径组件cleanVersion := filepath.Base(version) // 移除路径分隔符cleanFile := filepath.Base(filename) // 移除路径分隔符// 验证文件名有效性if cleanFile == "." || cleanFile == "/" {return "", fmt.Errorf("invalid filename")}// 构造安全路径return filepath.Join(baseDir, cleanVersion, cleanFile), nil
}// 等待关闭信号并优雅地关闭服务器func waitForShutdown(server *http.Server) {interruptChan := make(chan os.Signal, 1)signal.Notify(interruptChan, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)// 等待中断信号<-interruptChanlog.Println("接收到关闭信号,开始优雅关闭服务器...")// 创建一个5秒的超时时间来关闭服务器ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)defer cancel()// 优雅关闭服务器,等待现有请求完成if err := server.Shutdown(ctx); err != nil {log.Printf("服务器关闭错误: %v\n", err)} else {log.Println("服务器已优雅关闭")}
}func homeHandler(w http.ResponseWriter, r *http.Request) {w.Write([]byte("OK"))log.Printf("homeHandler_ok")
}// http://192.168.1.3:8080/version?v=v3.0
func versionHandler(w http.ResponseWriter, r *http.Request) {// 获取客户端请求的版本号version := r.URL.Query().Get("v")log.Printf("get version=%v", version)newerversion := ""if len(version) >= 3 {bytestring := []byte(version)if bytestring[0] == 'v' || bytestring[0] == 'V' {newerversion = version} else if bytestring[0] > '0' && bytestring[0] <= '9' {newerversion = "v"newerversion += version[0:3]}if len(newerversion) > 1 && strings.Compare(newerversion, gobal_verson) != 0 {gobal_verson = newerversionlog.Printf("set new version=%v", gobal_verson)}}w.Write([]byte("OK"))
}
3:配置
预定义的全在 \esp-idf\components\partition_table 目录下
1>Single factory app, no OTA (CONFIG_PARTITION_TABLE_SINGLE_APP)
这是默认的分区表,设计用于 2MB 或更大容量的闪存,包含一个 1MB 的应用分区。IDF 目录中对应的 CSV 文件为 components/partition_table/partitions_singleapp.csv
此分区表不适用于需要 OTA(空中升级)功能的应用。
2>Single factory app (large), no OTA (CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE)
这是默认分区表的一个变体,将 1MB 的应用分区大小扩展到 1.5MB,以容纳更多代码。IDF 目录中对应的 CSV 文件为 components/partition_table/partitions_singleapp_large.csv
此分区表不适用于需要 OTA(空中升级)功能的应用。
3>Factory app, two OTA definitions (CONFIG_PARTITION_TABLE_TWO_OTA)
这是一个基本的支持 OTA 的分区表,包含一个工厂应用分区和两个 OTA 应用分区。所有分区均为 1MB,因此此分区表需要 4MB 或更大容量的闪存。IDF 目录中对应的 CSV 文件为 components/partition_table/partitions_two_ota.csv
4>Two large size OTA partitions (CONFIG_PARTITION_TABLE_TWO_OTA_LARGE)
这是一个基本的支持 OTA 的分区表,包含两个 OTA 应用分区。每个应用分区大小均为 1700K,因此此分区表需要 4MB 或更大容量的闪存。IDF 目录中对应的 CSV 文件为
components/partition_table/partitions_two_ota_large.csv
5>Custom partition table CSV (CONFIG_PARTITION_TABLE_CUSTOM)
指定项目要使用的分区表 CSV 的路径。
有关更多信息,请参阅《ESP-IDF 编程指南》中的 “分区表” 部分。
4:测试结果 如果对你又帮助,麻烦点个赞,加个关注
先修改升级版本(*.bin)放到 filehttpserver 相应目录下(eg:downloads/v2.0)
#define FIRMWARE_VERSION 200
// 版本号字符串(用于HTTP请求)
#define FIRMWARE_VERSION_STR “2.0”
跟热更新一样,检查,有新版本就下载道ota_N 分区,重启,
下载的BIN 正式时一定要放到cdn或专门的文件服务器,正式需要分块下载/断点续传,校验等等