一、OTA两种方式:app_updateesp_https_ota 区别

  • ESP32/ESP32-S2/ESP32-C3等可通过Wi-Fi或以太网下载新固件到OTA分区实现运行时升级。ESP-IDF提供两种OTA升级方法:

    1. 使用app_update组件的原生API
    2. 使用esp_https_ota组件的简化API(支持HTTPS升级)
  • 本次主要介绍通过app_update原生API组件进行OTA升级

二、ESP32的OTA代码

我们的目标是实现基于HTTPS协议的OTA固件升级方案,具体流程为:设备通过HTTPS请求从服务器获取最新的固件包,完成下载后将其写入指定的OTA分区,随后更新启动配置信息,最终重启系统并从新烧写的OTA分区启动更新后的应用程序。这一过程确保了固件传输的安全性和升级的可靠性,同时支持系统无缝切换到新版本。

1. 代码解析

native ota API参考链接

  • #define BUFFSIZE 1024 每次从服务器读取流大小为1024个字节

  • 证书嵌入:
    通过 CMake 将服务器的 PEM 格式公钥证书(ca_cert.pem)嵌入到固件二进制文件中。在 CMakeLists.txt 中使用 EMBED_TXTFILES 指令将证书文件编译进程序,证书数据会被存储在设备的 NVS(非易失性存储)区域。

# Embed the server root certificate into the final binary
idf_build_get_property(project_dir PROJECT_DIR)
idf_component_register(SRCS "native_ota_example.c"INCLUDE_DIRS "."EMBED_TXTFILES ${project_dir}/server_certs/ca_cert.pem)
  • 证书访问:
    server_cert_pem_start 指向证书数据起始地址
    server_cert_pem_end 指向证书数据结束地址
extern const uint8_t server_cert_pem_start[] asm("_binary_ca_cert_pem_start");
extern const uint8_t server_cert_pem_end[] asm("_binary_ca_cert_pem_end");// 初始化HTTPS客户端配置
esp_http_client_config_t config = {.url = CONFIG_EXAMPLE_FIRMWARE_UPG_URL,.cert_pem = (char *)server_cert_pem_start,.timeout_ms = CONFIG_EXAMPLE_OTA_RECV_TIMEOUT,.keep_alive_enable = true,
};
  • 获取当前配置的启动分区configured、获取当前运行分区running,获取下一个更新分区update_partition
    const esp_partition_t *configured = esp_ota_get_boot_partition();const esp_partition_t *running = esp_ota_get_running_partition();update_partition = esp_ota_get_next_update_partition(NULL);
  • HTTPS的Get请求配置如下,URL在menuconfig里面填写,固件名为ota_1.bin
    // 初始化HTTP客户端配置esp_http_client_config_t config = {.url = CONFIG_EXAMPLE_FIRMWARE_UPG_URL,.cert_pem = (char *)server_cert_pem_start,.timeout_ms = CONFIG_EXAMPLE_OTA_RECV_TIMEOUT,.keep_alive_enable = true,};

在这里插入图片描述

  • 从API获取流数据时,每次读取BUFFSIZE1024个Byte
    while (1){int data_read = esp_http_client_read(client, ota_write_data, BUFFSIZE);
  • 首先需要检验固件头数据,擦除升级的flash区域,然后开始不断连续的写入到OTA区域。看注释
// 检查OTA头部信息,只在第一次接收数据时执行
if (image_header_was_checked == false) 
{esp_app_desc_t new_app_info;  // 用于存储新固件的描述信息// 检查接收到的数据长度是否足够包含完整的头部信息// 需要包含:映像头+段头+应用描述信息if (data_read > sizeof(esp_image_header_t) + sizeof(esp_image_segment_header_t) + sizeof(esp_app_desc_t)) {// 从接收数据中提取应用描述信息,跳过映像头和段头memcpy(&new_app_info, &ota_write_data[sizeof(esp_image_header_t) + sizeof(esp_image_segment_header_t)], sizeof(esp_app_desc_t));ESP_LOGI(TAG, "检测到新固件版本: %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, "当前运行版本: %s", running_app_info.version);}// 获取最后一个无效(启动失败)分区的信息const esp_partition_t *last_invalid_app = esp_ota_get_last_invalid_partition();esp_app_desc_t invalid_app_info;if (esp_ota_get_partition_description(last_invalid_app, &invalid_app_info) == ESP_OK) {ESP_LOGI(TAG, "最后无效固件版本: %s", invalid_app_info.version);}/* 版本验证逻辑 */// 情况1:新版本与最近失败版本相同if (last_invalid_app != NULL) {if (memcmp(invalid_app_info.version, new_app_info.version, sizeof(new_app_info.version)) == 0) {ESP_LOGW(TAG, "新版本与最近失败的版本相同!");ESP_LOGW(TAG, "上次尝试启动 %s 版本固件失败", invalid_app_info.version);ESP_LOGW(TAG, "系统已回滚到之前版本");http_cleanup(client);infinite_loop();  // 阻止继续升级}}// 情况2:新版本与当前运行版本相同(可通过配置跳过此检查)
#ifndef CONFIG_EXAMPLE_SKIP_VERSION_CHECKif (memcmp(new_app_info.version, running_app_info.version, sizeof(new_app_info.version)) == 0) {ESP_LOGW(TAG, "新版本与当前运行版本相同,终止升级");http_cleanup(client);infinite_loop();}
#endifimage_header_was_checked = true;  // 标记已完成头部检查// 初始化OTA写入操作,这里会擦除目标分区,OTA_WITH_SEQUENTIAL_WRITES 数据将按顺序写入err = esp_ota_begin(update_partition, OTA_WITH_SEQUENTIAL_WRITES, &update_handle);if (err != ESP_OK) {ESP_LOGE(TAG, "esp_ota_begin 失败 (%s)", esp_err_to_name(err));http_cleanup(client);esp_ota_abort(update_handle);task_fatal_error();}ESP_LOGI(TAG, "OTA写入初始化成功");}else {// 数据长度不足错误处理int head = sizeof(esp_image_header_t) + sizeof(esp_image_segment_header_t) + sizeof(esp_app_desc_t);ESP_LOGE(TAG, "接收数据长度不足!需要%d字节,实际%d字节", head, data_read);http_cleanup(client);esp_ota_abort(update_handle);task_fatal_error();}
}// 每次循环,写入接收到的数据块到OTA分区
err = esp_ota_write(update_handle, (const void *)ota_write_data, data_read);
if (err != ESP_OK) {http_cleanup(client);esp_ota_abort(update_handle);task_fatal_error();
}// 累计已写入数据量
binary_file_length += data_read;
ESP_LOGD(TAG, "已写入数据量: %d 字节", binary_file_length);
  • 结束处理
// 结束升级
err = esp_ota_end(update_handle);
// 下一次从update_partition启动
err = esp_ota_set_boot_partition(update_partition);
// 重启
esp_restart();

2. 全部代码

/* OTA exampleThis example code is in the Public Domain (or CC0 licensed, at your option.)Unless required by applicable law or agreed to in writing, thissoftware is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES ORCONDITIONS OF ANY KIND, either express or implied.
*/
#include <string.h>
#include <inttypes.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_system.h"
#include "esp_event.h"
#include "esp_log.h"
#include "esp_ota_ops.h"
#include "esp_app_format.h"
#include "esp_http_client.h"
#include "esp_flash_partitions.h"
#include "esp_partition.h"
#include "nvs.h"
#include "nvs_flash.h"
#include "driver/gpio.h"
#include "protocol_examples_common.h"
#include "errno.h"
#include "esp_netif.h"
#include <netdb.h>
#include <sys/socket.h>
#include <arpa/inet.h>#if CONFIG_EXAMPLE_CONNECT_WIFI
#include "esp_wifi.h"
#endif#define BUFFSIZE 1024
#define HASH_LEN 32 /* SHA-256 digest length */static const char *TAG = "native_ota_example";
/*an ota data write buffer ready to write to the flash*/
static char ota_write_data[BUFFSIZE + 1] = {0};
extern const uint8_t server_cert_pem_start[] asm("_binary_ca_cert_pem_start");
extern const uint8_t server_cert_pem_end[] asm("_binary_ca_cert_pem_end");#define OTA_URL_SIZE 256static void http_cleanup(esp_http_client_handle_t client)
{esp_http_client_close(client);esp_http_client_cleanup(client);
}static void __attribute__((noreturn)) task_fatal_error(void)
{ESP_LOGE(TAG, "Exiting task due to fatal error...");(void)vTaskDelete(NULL);while (1){;}
}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);
}static void infinite_loop(void)
{int i = 0;ESP_LOGI(TAG, "When a new firmware is available on the server, press the reset button to download it");while (1){ESP_LOGI(TAG, "Waiting for a new firmware ... %d", ++i);vTaskDelay(2000 / portTICK_PERIOD_MS);}
}void network_debug_info()
{// 获取默认网络接口esp_netif_t *netif = esp_netif_get_default_netif();if (!netif){ESP_LOGE(TAG, "No active network interface");return;}// 检查网络状态if (!esp_netif_is_netif_up(netif)){ESP_LOGE(TAG, "Network interface down");return;}// 获取DNS信息esp_netif_dns_info_t dns_info;if (esp_netif_get_dns_info(netif, ESP_NETIF_DNS_MAIN, &dns_info) == ESP_OK){ESP_LOGI(TAG, "Main DNS: " IPSTR, IP2STR(&dns_info.ip.u_addr.ip4));}
}/*** @brief 检查 hostname 是否可以解析* @param hostname 要检查的域名* @return*     - ESP_OK: 解析成功*     - ESP_FAIL: 解析失败*/
esp_err_t check_hostname_resolution(const char *hostname)
{struct addrinfo hints = {.ai_family = AF_INET, // 只检查IPv4.ai_socktype = SOCK_STREAM,.ai_flags = AI_CANONNAME,};struct addrinfo *result = NULL;ESP_LOGI(TAG, "尝试解析: %s", hostname);int ret = getaddrinfo(hostname, NULL, &hints, &result);if (ret != 0){ESP_LOGE(TAG, "解析失败");if (ret == EAI_NONAME){ESP_LOGE(TAG, "错误: 域名不存在或无法解析 (EAI_NONAME)");}else if (ret == EAI_AGAIN){ESP_LOGE(TAG, "错误: 临时DNS故障 (EAI_AGAIN)");}return ESP_FAIL;}// 打印解析到的IP地址char ip_str[INET_ADDRSTRLEN];struct sockaddr_in *addr = (struct sockaddr_in *)result->ai_addr;inet_ntop(AF_INET, &addr->sin_addr, ip_str, sizeof(ip_str));ESP_LOGI(TAG, "解析成功: %s -> %s", hostname, ip_str);freeaddrinfo(result);return ESP_OK;
}static void ota_example_task(void *pvParameter)
{esp_err_t err;/* update handle : set by esp_ota_begin(), must be freed via esp_ota_end() */esp_ota_handle_t update_handle = 0;const esp_partition_t *update_partition = NULL;ESP_LOGI(TAG, "Starting OTA example task");const esp_partition_t *configured = esp_ota_get_boot_partition();const esp_partition_t *running = esp_ota_get_running_partition();// 检查配置的OTA启动分区和正在运行的分区是否相同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);// 初始化HTTP客户端配置esp_http_client_config_t config = {.url = CONFIG_EXAMPLE_FIRMWARE_UPG_URL,.cert_pem = (char *)server_cert_pem_start,.timeout_ms = CONFIG_EXAMPLE_OTA_RECV_TIMEOUT,.keep_alive_enable = true,};// 添加网络诊断network_debug_info();#ifdef CONFIG_EXAMPLE_FIRMWARE_UPGRADE_URL_FROM_STDINchar url_buf[OTA_URL_SIZE];// 如果配置的URL为FROM_STDIN,则从标准输入读取URLif (strcmp(config.url, "FROM_STDIN") == 0){example_configure_stdin_stdout();fgets(url_buf, OTA_URL_SIZE, stdin);int len = strlen(url_buf);url_buf[len - 1] = '\0';config.url = url_buf;}else{ESP_LOGE(TAG, "Configuration mismatch: wrong firmware upgrade image url");abort();}
#endif#ifdef CONFIG_EXAMPLE_SKIP_COMMON_NAME_CHECKconfig.skip_cert_common_name_check = true;
#endif// 初始化HTTP客户端esp_http_client_handle_t client = esp_http_client_init(&config);if (client == NULL){ESP_LOGE(TAG, "Failed to initialise HTTP connection");task_fatal_error();}// 打开HTTP连接err = esp_http_client_open(client, 0);if (err != ESP_OK){ESP_LOGE(TAG, "Failed to open HTTP connection: %s", esp_err_to_name(err));esp_http_client_cleanup(client);task_fatal_error();}// 获取HTTP头部信息esp_http_client_fetch_headers(client);// 获取下一个OTA更新分区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);int binary_file_length = 0;/*deal with all receive packet*/bool image_header_was_checked = false;// 循环读取HTTP数据while (1){int data_read = esp_http_client_read(client, ota_write_data, BUFFSIZE);if (data_read < 0){ESP_LOGE(TAG, "Error: SSL data read error %d", data_read);http_cleanup(client);task_fatal_error();}else if (data_read > 0){// 检查OTA头部信息if (image_header_was_checked == false){esp_app_desc_t new_app_info;if (data_read > sizeof(esp_image_header_t) + sizeof(esp_image_segment_header_t) + sizeof(esp_app_desc_t)){// check current version with downloadingmemcpy(&new_app_info, &ota_write_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();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);}// check current version with last invalid partitionif (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);ESP_LOGW(TAG, "The firmware has been rolled back to the previous version.");http_cleanup(client);infinite_loop();}}
#ifndef CONFIG_EXAMPLE_SKIP_VERSION_CHECKif (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.");http_cleanup(client);infinite_loop();}
#endifimage_header_was_checked = true;err = esp_ota_begin(update_partition, OTA_WITH_SEQUENTIAL_WRITES, &update_handle);if (err != ESP_OK){ESP_LOGE(TAG, "esp_ota_begin failed (%s)", esp_err_to_name(err));http_cleanup(client);esp_ota_abort(update_handle);task_fatal_error();}ESP_LOGI(TAG, "esp_ota_begin succeeded");}else{int head = sizeof(esp_image_header_t) + sizeof(esp_image_segment_header_t) + sizeof(esp_app_desc_t);ESP_LOGE(TAG, "received package is not fit len %d , %d", data_read, head);http_cleanup(client);esp_ota_abort(update_handle);task_fatal_error();}}err = esp_ota_write(update_handle, (const void *)ota_write_data, data_read);if (err != ESP_OK){http_cleanup(client);esp_ota_abort(update_handle);task_fatal_error();}binary_file_length += data_read;ESP_LOGI(TAG, "Written image length %d", binary_file_length);}else if (data_read == 0){/** As esp_http_client_read never returns negative error code, we rely on* `errno` to check for underlying transport connectivity closure if any*/if (errno == ECONNRESET || errno == ENOTCONN){ESP_LOGE(TAG, "Connection closed, errno = %d", errno);break;}if (esp_http_client_is_complete_data_received(client) == true){ESP_LOGI(TAG, "Connection closed");break;}}}ESP_LOGI(TAG, "Total Write binary data length: %d", binary_file_length);if (esp_http_client_is_complete_data_received(client) != true){ESP_LOGE(TAG, "Error in receiving complete file");http_cleanup(client);esp_ota_abort(update_handle);task_fatal_error();}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");}else{ESP_LOGE(TAG, "esp_ota_end failed (%s)!", esp_err_to_name(err));}http_cleanup(client);task_fatal_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));http_cleanup(client);task_fatal_error();}ESP_LOGI(TAG, "Prepare to restart system!");esp_restart();return;
}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 << CONFIG_EXAMPLE_GPIO_DIAGNOSTIC);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(CONFIG_EXAMPLE_GPIO_DIAGNOSTIC);gpio_reset_pin(CONFIG_EXAMPLE_GPIO_DIAGNOSTIC);return diagnostic_is_ok;
}void app_main(void)
{ESP_LOGI(TAG, "OTA example app_main start");uint8_t sha_256[HASH_LEN] = {0};esp_partition_t partition;// get sha256 digest for the partition tablepartition.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: ");// get sha256 digest for bootloaderpartition.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: ");// get sha256 digest for running partitionesp_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){// 检查OTA状态if (ota_state == ESP_OTA_IMG_PENDING_VERIFY){// run diagnostic function ...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();}}}// Initialize NVS.esp_err_t err = nvs_flash_init();if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND){// OTA app partition table has a smaller NVS partition size than the non-OTA// partition table. This size mismatch may cause NVS initialization to fail.// If this happens, we erase NVS partition and initialize NVS again.ESP_ERROR_CHECK(nvs_flash_erase());err = nvs_flash_init();}ESP_ERROR_CHECK(err);ESP_ERROR_CHECK(esp_netif_init());ESP_ERROR_CHECK(esp_event_loop_create_default());/* This helper function configures Wi-Fi or Ethernet, as selected in menuconfig.* Read "Establishing Wi-Fi or Ethernet Connection" section in* examples/protocols/README.md for more information about this function.*/ESP_ERROR_CHECK(example_connect());#if CONFIG_EXAMPLE_CONNECT_WIFI/* Ensure to disable any WiFi power save mode, this allows best throughput* and hence timings for overall OTA operation.*/ESP_LOGI(TAG, "Disable WiFi power save");esp_wifi_set_ps(WIFI_PS_NONE);
#endif // CONFIG_EXAMPLE_CONNECT_WIFIxTaskCreate(&ota_example_task, "ota_example_task", 8192, NULL, 5, NULL);
}

三、node服务器

1. 代码解析

├── ota_test├── certs│   ├── server_cert.key│   └── server_cert.pem└── ota_files├── ota_1.bin└── ota_2.bin
  • 创建一个Express应用实例
function startOtaServer(config: ServerConfig): https.Server {const app = createApp(config);// 创建HTTPS服务器const server = https.createServer({// 读取SSL密钥文件key: fs.readFileSync(config.keyFile),// 读取SSL证书文件cert: fs.readFileSync(config.certFile)}, app);// 启动服务器监听指定端口server.listen(config.port, '0.0.0.0', () => {// 打印服务器启动信息,包括本地IP和端口console.log(`OTA Server running on https://${getLocalIp()}:${config.port}`);// 打印固件文件服务目录console.log(`Serving firmware from: ${config.firmwareDir}`);});// 返回创建的服务器实例return server;
}const config: ServerConfig = {port: 8002,firmwareDir: './ota_test/ota_files', // 固件的位置certFile: './ota_test/certs/server_cert.pem', // 公钥文件keyFile: './ota_test/certs/server_cert.key' // 私钥文件
};
  • 固件下载的API
app.get('/firmware/:filename', (req, res) => {// 从URL参数获取请求的文件名const filename = req.params.filename;// 拼接完整的固件文件路径const filePath = path.join(PROJECT_PATH, config.firmwareDir, filename);// 检查文件是否存在if (!fs.existsSync(filePath)) {return res.status(404).send('Firmware not found');}// 获取文件信息const stat = fs.statSync(filePath);const fileSize = stat.size;// 设置响应头res.setHeader('Content-Length', fileSize); // 文件大小res.setHeader('Transfer-Encoding', 'identity'); // 关键修复:禁用分块传输res.setHeader('Content-Type', 'application/octet-stream'); // 二进制流类型// 创建文件读取流const fileStream = fs.createReadStream(filePath);// 文件流打开事件fileStream.on('open', () => {// 将文件流管道传输到响应对象fileStream.pipe(res);});// 文件流错误处理fileStream.on('error', (err) => {console.error(`文件流错误: ${err.message}`);if (!res.headersSent) {// 如果响应头还未发送,返回500错误res.status(500).send('文件流错误');} else {// 如果响应头已发送,直接销毁响应res.destroy();}});
});

2. 全部代码

import express from 'express';
import https from 'https';
import fs from 'fs';
import path from 'path';
import os from 'os';
import { PROJECT_PATH } from './config';// 配置类型
interface ServerConfig {port: number;firmwareDir: string;certFile: string;keyFile: string;
}// 获取本机IP地址
function getLocalIp(): string {const interfaces = os.networkInterfaces();for (const name of Object.keys(interfaces)) {for (const iface of interfaces[name]!) {if (iface.family === 'IPv4' && !iface.internal) {return iface.address;}}}return 'localhost';
}// 创建并配置Express应用
function createApp(config: ServerConfig): express.Application {const app = express();// 修复后的固件下载端点app.get('/firmware/:filename', (req, res) => {const filename = req.params.filename;const filePath = path.join(PROJECT_PATH, config.firmwareDir, filename);if (!fs.existsSync(filePath)) {return res.status(404).send('Firmware not found');}const stat = fs.statSync(filePath);const fileSize = stat.size;res.setHeader('Content-Length', fileSize);res.setHeader('Transfer-Encoding', 'identity'); // 关键修复res.setHeader('Content-Type', 'application/octet-stream');const fileStream = fs.createReadStream(filePath);fileStream.on('open', () => {fileStream.pipe(res);});fileStream.on('error', (err) => {console.error(`File stream error: ${err.message}`);if (!res.headersSent) {res.status(500).send('File stream error');} else {res.destroy();}});res.on('close', () => {if (!fileStream.destroyed) {fileStream.destroy();}});});// 健康检查端点app.get('/health', (req, res) => {res.status(200).json({status: 'active',firmwareDir: config.firmwareDir,port: config.port});});return app;
}// 启动HTTPS服务器
function startOtaServer(config: ServerConfig): https.Server {const app = createApp(config);const server = https.createServer({key: fs.readFileSync(config.keyFile),cert: fs.readFileSync(config.certFile)}, app);server.listen(config.port, '0.0.0.0', () => {console.log(`OTA Server running on https://${getLocalIp()}:${config.port}`);console.log(`Serving firmware from: ${config.firmwareDir}`);});return server;
}// 使用示例
const config: ServerConfig = {port: 8002,firmwareDir: './ota_test/ota_files',certFile: './ota_test/certs/server_cert.pem',keyFile: './ota_test/certs/server_cert.key'
};const server = startOtaServer(config);// 处理退出信号
process.on('SIGINT', () => {console.log('Shutting down OTA server...');server.close(() => {process.exit();});
});

四、python服务器

  • 创建python独立环境
# 创建环境(Python 3.3+ 自带)
python -m venv test_env  # 激活环境
source test_env/bin/activate  # vscode
ctrl+shift+p 输入 python interpreter 选择test_env

在这里插入图片描述

  • 下面是官方提供的python例子,运行方式如下:
    pytest pytest_native_ota.py
    在这里插入图片描述
import http.server
import multiprocessing
import os
import random
import socket
import ssl
import struct
import subprocess
from typing import Callable
from typing import Tupleimport pexpect
import pytest
# from common_test_methods import get_host_ip4_by_dest_ip
from pytest_embedded import Dutdef get_host_ip4_by_dest_ip(dest_ip: str) -> str:"""通过尝试连接目标IP,自动选择正确的本地IP。参数:dest_ip: 目标IP地址返回:本地主机的IPv4地址"""with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:s.connect((dest_ip, 80))  # 80是任意端口,无需实际通信return s.getsockname()[0]  # 获取本地绑定的IP地址# 硬编码的测试用SSL证书(PEM格式)
server_cert = '-----BEGIN CERTIFICATE-----\n' \'MIIDWDCCAkACCQCbF4+gVh/MLjANBgkqhkiG9w0BAQsFADBuMQswCQYDVQQGEwJJ\n'\# ... 证书内容省略 ...'-----END CERTIFICATE-----\n'# 硬编码的测试用SSL私钥(PEM格式) 
server_key = '-----BEGIN PRIVATE KEY-----\n'\'MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDhxF/y7bygndxP\n'\# ... 私钥内容省略 ...'-----END PRIVATE KEY-----\n'def create_file(server_file: str, file_data: str) -> None:"""创建文件并写入内容参数:server_file: 文件路径file_data: 要写入的内容"""with open(server_file, 'w+') as file:file.write(file_data)def get_ca_cert(ota_image_dir: str) -> Tuple[str, str]:"""生成SSL证书和密钥文件参数:ota_image_dir: OTA镜像目录路径返回:元组(证书文件路径, 密钥文件路径)"""os.chdir(ota_image_dir)  # 切换到OTA镜像目录server_file = os.path.join(ota_image_dir, 'server_cert.pem')create_file(server_file, server_cert)  # 创建证书文件key_file = os.path.join(ota_image_dir, 'server_key.pem')create_file(key_file, server_key)  # 创建密钥文件return server_file, key_filedef https_request_handler() -> Callable[...,http.server.BaseHTTPRequestHandler]:"""创建自定义HTTP请求处理器,处理broken pipe异常返回:自定义的RequestHandler类"""class RequestHandler(http.server.SimpleHTTPRequestHandler):def finish(self) -> None:"""重写finish方法,优雅处理socket错误"""try:if not self.wfile.closed:self.wfile.flush()self.wfile.close()except socket.error:pass  # 忽略socket错误self.rfile.close()def handle(self) -> None:"""重写handle方法,捕获socket错误"""try:http.server.BaseHTTPRequestHandler.handle(self)except socket.error:pass  # 忽略socket错误return RequestHandlerdef start_https_server(ota_image_dir: str, server_ip: str, server_port: int) -> None:"""启动HTTPS服务器参数:ota_image_dir: OTA镜像目录server_ip: 服务器监听IPserver_port: 服务器监听端口"""server_file, key_file = get_ca_cert(ota_image_dir)  # 获取证书和密钥requestHandler = https_request_handler()  # 创建请求处理器# 创建HTTP服务器httpd = http.server.HTTPServer((server_ip, server_port), requestHandler)# 配置SSL上下文ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)ssl_context.load_cert_chain(certfile=server_file, keyfile=key_file)# 包装socket为SSL sockethttpd.socket = ssl_context.wrap_socket(httpd.socket, server_side=True)httpd.serve_forever()  # 启动服务器def start_chunked_server(ota_image_dir: str, server_port: int) -> subprocess.Popen:"""启动分块传输的HTTPS服务器(使用openssl s_server)参数:ota_image_dir: OTA镜像目录server_port: 服务器端口返回:subprocess.Popen对象"""server_file, key_file = get_ca_cert(ota_image_dir)# 使用openssl命令启动服务器chunked_server = subprocess.Popen(['openssl', 's_server', '-WWW', '-key', key_file, '-cert', server_file, '-port', str(server_port)])return chunked_server@pytest.mark.esp32
@pytest.mark.ethernet_ota
def test_examples_protocol_native_ota_example(dut: Dut) -> None:"""OTA示例测试用例 - 验证通过HTTPS多次下载完整固件测试步骤:1. 连接AP/以太网2. 通过HTTPS获取OTA镜像3. 使用新OTA镜像重启参数:dut: 被测设备对象"""server_port = 8002  # 服务器端口iterations = 3  # 测试迭代次数bin_name = 'ota_1.bin'  # 要下载的固件文件名# 启动HTTPS服务器(使用多进程)thread1 = multiprocessing.Process(target=start_https_server, args=(dut.app.binary_path, '0.0.0.0', server_port))thread1.daemon = True  # 设置为守护进程thread1.start()try:# 开始测试迭代for _ in range(iterations):# 等待设备启动完成dut.expect('Loaded app from partition at offset', timeout=30)try:# 获取设备IP地址ip_address = dut.expect(r'IPv4 address: (\d+\.\d+\.\d+\.\d+)[^\d]', timeout=30)[1].decode()print('Connected to AP/Ethernet with IP: {}'.format(ip_address))except pexpect.exceptions.TIMEOUT:raise ValueError('ENV_TEST_FAILURE: Cannot connect to AP/Ethernet')# 获取主机IPhost_ip = get_host_ip4_by_dest_ip(ip_address)# 等待OTA任务启动dut.expect('Starting OTA example task', timeout=30)# 构造OTA URL并发送给设备ota_url = 'https://' + host_ip + ':' + str(server_port) + '/firmware/' + bin_nameprint('writing to device: {}'.format(ota_url))dut.write(ota_url)# 等待设备准备重启dut.expect('Prepare to restart system!', timeout=60)finally:# 测试结束,终止服务器进程thread1.terminate()

五、升级Log

  • 下面是使用node服务器远程升级成功的LOG

在这里插入图片描述
在这里插入图片描述

六、疑问

1. 网页https请求也不需要pem文件,为什么esp的OTA升级需要pem文件呢?

场景普通浏览器/系统ESP设备
证书验证方式内置信任的CA根证书库无内置证书库(除非特别配置)
证书来源操作系统或浏览器预装数百个CA根证书必须手动提供可信证书
自签名证书支持会显示警告但可跳过严格验证,无法跳过
  • 为什么ESP需要PEM文件?

    • 身份验证:防止"中间人攻击"
    • PEM文件包含服务器的公钥证书(或签发它的CA证书)
    • ESP用其验证服务器的HTTPS证书是否由可信机构签发
  • 证书文件的作用

    • server.crt / server.pem:服务器公钥证书:包含服务器身份信息和公钥
    • server.key:服务器私钥:永远不共享(仅服务器持有)
    • ca.pem:证书颁发机构(CA)的根证书:(验证服务器证书是否可信)
  • 为什么普通用户不需要?比如:

    • 访问https://google.com时:
    • 浏览器检查Google证书的签发链
    • 发现是由GlobalSign或Google Trust Services签发
    • 系统已预装这些CA根证书,自动完成验证
服务器证书类型ESP设备处理方式
公共CA签发启用证书包功能,无需额外PEM文件
自签名证书必须提供PEM文件(如文档示例)
私有CA签发提供私有CA的根证书(ca.pem)

2. ESP32 HTTPS服务器CA证书配置指南

  • 关键结论:ESP需要PEM文件是因为它没有预装可信CA库,必须通过显式提供证书来建立信任关系。这是嵌入式设备安全通信的必要保障,不同于桌面系统的开箱即用特性。

  • 嵌入式设备因资源限制,需手动配置CA证书建立TLS信任链。与桌面系统不同,ESP32需要显式提供证书实现安全通信。

  1. 启用证书包功能:
idf.py menuconfig
→ Component config → mbedTLS → Certificate Bundle → Enable
  1. 代码中移除cert_pem参数,改用:
.crt_bundle_attach = esp_crt_bundle_attach,
  1. 配置 esp_http_client_config_t 参数:
extern const uint8_t server_cert_pem_start[] asm("_binary_ca_cert_pem_start");
extern const uint8_t server_cert_pem_end[] asm("_binary_ca_cert_pem_end");// 初始化HTTPS客户端配置
esp_http_client_config_t config = {.url = CONFIG_EXAMPLE_FIRMWARE_UPG_URL,.cert_pem = (char *)server_cert_pem_start,.timeout_ms = CONFIG_EXAMPLE_OTA_RECV_TIMEOUT,.keep_alive_enable = true,
};
  1. cmakelist.txt 配置:
# Embed the server root certificate into the final binary
idf_build_get_property(project_dir PROJECT_DIR)
idf_component_register(SRCS "native_ota_example.c"INCLUDE_DIRS "."EMBED_TXTFILES ${project_dir}/server_certs/ca_cert.pem)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/news/914939.shtml
繁体地址,请注明出处:http://hk.pswp.cn/news/914939.shtml
英文地址,请注明出处:http://en.pswp.cn/news/914939.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

byte[]作为接口参数传递的方法

在C#中通过WebService或API传递byte参数&#xff08;如文件、图像等二进制数据&#xff09;时&#xff0c;通常有以下几种实现方式&#xff1a; ‌1. 使用Base64编码&#xff08;推荐REST API&#xff09;‌ 将byte数组转换为Base64字符串传输&#xff0c;适用于JSON格式的API&…

元宇宙与Web3的深度融合:构建沉浸式数字体验的愿景与挑战

一、技术特征与融合基础1. 元宇宙的技术架构&#xff08;2025年&#xff09;空间构建技术&#xff1a;3D建模与渲染&#xff1a;实时渲染引擎&#xff08;如Unity HDRP&#xff09;支持路径追踪光追&#xff0c;AI生成模型&#xff08;NVIDIA Get3D&#xff09;3秒生成3D场景。…

什么是脑裂

脑裂定义&#xff1a; 脑裂是分布式系统中由于网络分区&#xff08;Network Partition&#xff09;导致集群节点被分割成多个独立子集&#xff0c;每个子集认为自己是唯一合法的集群&#xff0c;从而导致数据不一致或系统行为异常的现象。详细工作原理&#xff1a;发生原因&…

川翔云电脑:云端算力新标杆,创作自由无边界

一、旗舰机型&#xff1a;4090Ultra 48G 显存颠覆硬件限制 川翔云电脑最新上线的RTX 4090Ultra 48G 显存机型&#xff0c;采用 NVIDIA Ada Lovelace 架构&#xff0c;单卡显存容量达 48GB GDDR6X&#xff0c;较传统 4090 翻倍&#xff0c;可直接加载 1200 万面数的超复杂模型&a…

贪心算法(排序)

码蹄集OJ-活动安排 #include<bits/stdc.h> using namespace std; struct MOOE {int s,e; }; bool compare(const MOOE&a,const MOOE&b) {return a.e<b.e; } int main( ) {int n;cin>>n;vector<MOOE>a(n);for(int i0;i<n;i){cin>>a[i].…

详解序数回归损失函数ordinal_regression_loss:原理与实现

在医疗 AI 领域&#xff0c;很多分类任务具有有序类别的特性&#xff0c;如疾病严重程度&#xff08;轻度→中度→重度&#xff09;、肿瘤分级&#xff08;G1→G2→G3&#xff09;等。这类任务被称为序数回归&#xff08;Ordinal Regression&#xff09;&#xff0c;需要特殊的…

SQL增查

建完库与建完表后后:1.分别查询student表和score表的所有记录student表&#xff1a;score表:2.查询student表的第2条到5条记录SELECT * FROM student LIMIT 1,4;3.从student表中查询计算机系和英语系的学生的信息SELECT * FROM student-> WHERE department IN (计算机系, 英…

二分答案之最大化最小值

参考资料来源灵神在力扣所发的题单&#xff0c;仅供分享学习笔记和记录&#xff0c;无商业用途。 核心思路&#xff1a;本质上是求最大 应用场景&#xff1a;在满足条件的最小值区间内使最大化 检查函数&#xff1a;保证数据都要大于等于答案 补充&#xff1a;为什么需要满…

OCR 赋能档案数字化:让沉睡的档案 “活” 起来

添加图片注释&#xff0c;不超过 140 字&#xff08;可选&#xff09;企业产品档案包含设计图纸、检测报告、生产记录等&#xff0c;传统数字化仅靠扫描存档&#xff0c;后续检索需人工逐份翻阅&#xff0c;效率极低。​OCR 产品档案解决方案直击痛点&#xff1a;通过智能识别技…

力扣118.杨辉三角

思路1.新建一个vector的vector2.先把空间开出来&#xff0c;然后再把里面的值给一个个修改开空间的手段&#xff1a;new、构造函数、reserve、resize因为我们之后要修改里面的数据&#xff0c;这就意味着我们需要去读取这个数据并修改&#xff0c;如果用reserve的话&#xff0c…

Python 网络爬虫 —— 提交信息到网页

一、模块核心逻辑“提交信息到网页” 是网络交互关键环节&#xff0c;借助 requests 库的 post() 函数&#xff0c;能模拟浏览器向网页发数据&#xff08;如表单、文件 &#xff09;&#xff0c;实现信息上传&#xff0c;让我们能与网页背后的服务器 “沟通”&#xff0c;像改密…

SpringMVC4

一、SpringMVC 注解与项目开发流程1.1注解的生命周期- Target、Retention 等元注解&#xff1a;- Target(ElementType.TYPE) &#xff1a;说明这个注解只能用在类、接口上。- Retention(RetentionPolicy.RUNTIME) &#xff1a;说明注解在运行时保留&#xff0c;能通过反射获取…

数据结构排序算法总结(C语言实现)

以下是常见排序算法的总结及C语言实现&#xff0c;包含时间复杂度、空间复杂度和稳定性分析&#xff1a;1. 冒泡排序 (Bubble Sort)思想&#xff1a;重复比较相邻元素&#xff0c;将较大元素向后移动。 时间复杂度&#xff1a;O(n)&#xff08;最好O(n)&#xff0c;最坏O(n)) 空…

嵌入式学习-PyTorch(2)-day19

很久没有学了&#xff0c;期间打点滴打了一个多星期&#xff0c;太累了&#xff0c;再加上学了一下Python语法基础&#xff0c;再终于开始重新学习pytorchtensorboard 的使用import torch from torch.utils.tensorboard import SummaryWriter writer SummaryWriter("logs…

Prompt Engineering 快速入门+实战案例

资料来源&#xff1a;火山引擎-开发者社区 引言 什么是 prompt A prompt is an input to a Generative AI model, that is used to guide its output. Prompt engineering is the process of writing effective instructions for a model, such that it consistently generat…

「源力觉醒 创作者计划」_文心开源模型(ERNIE-4.5-VL-28B-A3B-PT)使用心得

文章目录背景操作流程开源模型选择算力服务器平台开通部署一个算力服务器登录GPU算力服务器进行模型的部署FastDeploy 快速部署服务安装paddlepaddle-gpu1. 降级冲突的库版本安装fastdeploy直接部署模型&#xff08;此处大约花费15分钟时间&#xff09;放行服务端口供公网访问最…

P10719 [GESP202406 五级] 黑白格

题目传送门 前言&#xff1a;不是这样例有点过分了哈&#xff1a; 这是我没考虑到无解的情况的得分&#xff1a; 这是我考虑了的得分&#xff1a; 总而言之&#xff0c;就是一个Subtask 你没考虑无解的情况&#xff08;除了Subtask #0&#xff09;,就会WA一大片,然后这个Subt…

AWS RDS PostgreSQL可观测性最佳实践

AWS RDS PostgreSQL 介绍AWS RDS PostgreSQL 是亚马逊云服务&#xff08;AWS&#xff09;提供的托管型 PostgreSQL 数据库服务。托管服务&#xff1a;AWS 管理数据库的底层基础设施&#xff0c;包括硬件、操作系统、数据库引擎等&#xff0c;用户无需自行维护。高性能&#xff…

C++——set,map的模拟实现

文章目录前言红黑树的改变set的模拟实现基本框架迭代器插入源码map模拟实现基础框架迭代器插入赋值重载源码测试代码前言 set&#xff0c;map底层使用红黑树这种平衡二叉搜索树来组织元素 &#xff0c;这使得set, map能够提供对数时间复杂度的查找、插入和删除操作。 下面都是基…

LabVIEW液压机智能监控

​基于LabVIEW平台&#xff0c;结合西门子、研华等硬件&#xff0c;构建液压机实时监控系统。通过 OPC 通信技术实现上位机与 PLC 的数据交互&#xff0c;解决传统监控系统数据采集滞后、存储有限、参数调控不便等问题&#xff0c;可精准采集冲压过程中的位置、速度、压力等参数…