1. 项目概述
esp32HttpJsonOTA是一个面向 ESP32 平台的轻量级、纯 HTTP 协议驱动的固件(Firmware)与文件系统(SPIFFS)空中升级(OTA)库。其核心设计目标是在不依赖服务端脚本(如 PHP/Python 后端)的前提下,通过标准 HTTP 协议完成版本校验与二进制镜像下载更新。该库完全基于 ESP-IDF 或 Arduino-ESP32 框架构建,兼容官方 WiFi 和 OTA API,适用于生产环境中的远程设备维护与功能迭代。
与传统 OTA 方案(如 ESP-IDF 的esp_https_ota或 Arduino 的ArduinoOTA)不同,esp32HttpJsonOTA采用“客户端主动拉取 + JSON 元数据驱动”的模式:设备启动后,首先向预设 URL 发起 HTTP GET 请求获取 JSON 配置文件;解析其中的版本号、目标主机、端口及二进制路径;比对本地当前版本;若存在更高版本,则发起第二次 HTTP GET 下载.bin文件并执行烧录。整个流程无需 TLS 加密(仅支持 HTTP),极大降低了服务端部署门槛——可直接使用静态 Web 服务器(Apache/Nginx)、Google Cloud Storage(GCS)、AWS S3、GitHub Pages 或任何支持 HTTP 文件直链的存储服务。
该项目由 Franck RONDOT 于 2020 年 3 月发布,是在 Chris Joyce 开发的esp32FOTA基础上深度重构的分支。主要增强点包括:
- 双模 OTA 支持:同时支持 Firmware(应用程序主镜像)与 SPIFFS(SPI Flash 文件系统)独立升级;
- JSON 版本控制机制:通过结构化元数据实现语义化版本管理;
- 设备标识集成能力:支持基于设备唯一 ID(如 MAC 地址哈希)的差异化更新策略;
- 零服务端逻辑依赖:所有决策逻辑均在 ESP32 端完成,服务端仅需提供静态 JSON 与 BIN 文件。
该方案特别适用于资源受限、无 HTTPS 证书管理能力、或需快速搭建 OTA 基础设施的嵌入式场景,例如工业传感器节点、农业物联网终端、教育开发套件等。
2. 核心架构与工作原理
2.1 整体流程图解
OTA 流程分为两个关键阶段:HTTP 版本检查(execHTTPcheck)与固件/文件系统烧录(execOTA)。二者严格解耦,允许开发者按需组合调用:
[设备启动] ↓ [WiFi 连接建立] ↓ [调用 execHTTPcheck()] ├─→ 构造 HTTP GET 请求 → JSON URL(如 http://update.example.com/fw.json) ├─→ 解析响应体为 JSON 对象 ├─→ 提取 'version' 字段并与本地版本(构造时传入或运行时设置)比对 └─→ 返回布尔值:true 表示存在新版本,false 表示已是最新 ↓(仅当返回 true 时执行) [调用 execOTA()] ├─→ 构造 HTTP GET 请求 → bin URL(如 http://storage.googleapis.com/bucket/fw.bin) ├─→ 流式接收 HTTP 响应体(Chunked Transfer Encoding 兼容) ├─→ 将数据块写入 OTA 分区(Firmware)或 SPIFFS 分区(SPIFFS) └─→ 校验 CRC32 / 写入完成标志 → 触发重启(Firmware)或重挂载(SPIFFS)此流程规避了 HTTPS 握手开销与证书管理复杂度,但要求开发者自行保障传输通道安全性(如通过私有网络、IP 白名单、URL 签名等方式)。
2.2 JSON 配置文件规范
JSON 文件是 OTA 的“指挥中枢”,必须严格遵循以下 Schema。服务端只需确保该文件可通过 HTTP GET 访问,内容为 UTF-8 编码纯文本。
| 字段名 | 类型 | 必填 | 说明 |
|---|---|---|---|
name | string | 是 | 设备应用名称,用于日志与调试标识,不参与版本逻辑判断 |
type | string | 是 | 更新类型,仅接受'FIRMWARE'或'SPIFFS',区分烧录目标分区 |
version | number | 是 | 当前远端版本号,整数类型。设备端将此值与本地版本比较决定是否更新 |
host | string | 是 | 目标 bin 文件所在 HTTP 服务器域名/IP,如storage.googleapis.com |
port | number | 是 | HTTP 端口,通常为80(HTTP)或443(HTTPS,但本库不支持) |
bin | string | 是 | bin 文件在服务器上的相对路径,如/bucket/fw-esp32.bin |
Firmware JSON 示例:
{ "name": "ESP32APPFR", "type": "FIRMWARE", "version": 1, "host": "storage.googleapis.com", "port": 80, "bin": "/update/esp32ehjo/fw-esp32ehjo.bin" }SPIFFS JSON 示例:
{ "name": "ESP32APPFR", "type": "SPIFFS", "version": 1, "host": "storage.googleapis.com", "port": 80, "bin": "/update/esp32ehjo/fs-esp32ehjo.bin" }⚠️关键注意事项:
- Google Cloud Storage 等对象存储服务默认 MIME 类型为
application/json(JSON)和text/plain(BIN),但 ESP32 HTTP 客户端对非application/octet-stream类型的二进制响应可能解析异常。务必在 GCS 控制台中将.bin文件的 Content-Type 设置为application/octet-stream。- 若使用带 Web 应用防火墙(WAF)的 CDN 或子域名(如 Cloudflare),部分 WAF 会拦截包含
ESP32、Arduino等 UA 字符串的请求。建议在测试阶段使用curl --head <JSON_URL>验证可访问性,必要时临时禁用 WAF 或自定义 User-Agent。
2.3 版本比对与状态管理
版本比对是 OTA 的决策核心。库提供两种版本管理方式:
编译期固定版本(推荐用于 Firmware)
在创建esp32HttpJsonOTA实例时传入OTA_VER宏定义值:#define OTA_VER 1 esp32HttpJsonOTA majFW(OTA_NAME, "FIRMWARE", OTA_VER, FWOTA_JSONURL);此方式下,
execHTTPcheck()直接将 JSON 中的version与构造时传入的OTA_VER比较。适用于主程序版本与硬件绑定紧密的场景。运行时动态版本(必需用于 SPIFFS)
SPIFFS 内容(如网页、配置文件)常需独立于固件更新。此时需在运行时读取 SPIFFS 中的版本文件(如/ver):int verFS() { File myFile = SPIFFS.open("/ver", "r"); if (myFile) { String verStr = myFile.readString(); myFile.close(); return verStr.toInt(); // 返回当前 SPIFFS 版本号 } return 1; // 默认版本 } // 在检查前设置 majFS.setVer(verFS()); bool needUpdate = majFS.execHTTPcheck();setVer()方法将运行时获取的版本号注入实例,供后续比对使用。
3. API 接口详解与源码逻辑
3.1 主要类与构造函数
esp32HttpJsonOTA是一个 C++ 类,封装了全部 OTA 功能。其构造函数签名如下:
esp32HttpJsonOTA(const char* name, const char* type, uint32_t currentVersion, const char* jsonUrl);| 参数 | 类型 | 说明 |
|---|---|---|
name | const char* | 设备/应用名称,仅用于日志输出,无业务逻辑作用 |
type | const char* | 更新类型字符串,必须为"FIRMWARE"或"SPIFFS" |
currentVersion | uint32_t | 当前本地版本号,用于与 JSON 中version比较 |
jsonUrl | const char* | JSON 配置文件的完整 HTTP URL(含协议、域名、路径) |
构造过程关键操作:
- 复制
name、type到内部缓冲区(长度限制为 32 字节); - 存储
currentVersion至成员变量m_currentVersion; - 解析
jsonUrl,提取协议(强制http://)、主机名、端口(默认 80)、路径; - 初始化内部状态机(
m_state = STATE_IDLE)。
3.2 核心成员函数解析
bool execHTTPcheck()
功能:执行 HTTP 版本检查,返回是否需要更新。
源码逻辑(简化):
bool esp32HttpJsonOTA::execHTTPcheck() { // 1. 创建 HTTP 客户端,连接 jsonUrl 主机 http_client_config_t config = {.url = m_jsonUrl}; esp_http_client_handle_t client = esp_http_client_init(&config); esp_http_client_open(client, 0); // GET 请求 // 2. 读取响应头,验证状态码 200 int status_code = esp_http_client_get_status_code(client); if (status_code != 200) { /* 错误处理 */ } // 3. 流式读取响应体至缓冲区(最大 1024 字节) char buffer[1024]; int len = esp_http_client_read(client, buffer, sizeof(buffer)-1); buffer[len] = '\0'; // 4. 使用 cJSON 解析 JSON cJSON *root = cJSON_Parse(buffer); if (!root) { /* JSON 解析失败 */ } // 5. 提取 version 字段 cJSON *verObj = cJSON_GetObjectItemCaseSensitive(root, "version"); uint32_t remoteVersion = (verObj && cJSON_IsNumber(verObj)) ? verObj->valueint : 0; // 6. 比较版本:remoteVersion > m_currentVersion bool needUpdate = (remoteVersion > m_currentVersion); cJSON_Delete(root); esp_http_client_cleanup(client); return needUpdate; }关键点:
- 使用 ESP-IDF 官方
esp_http_client组件,稳定可靠; - JSON 解析依赖
cJSON库(需在sdkconfig中启用CONFIG_CJSON_ENABLE); - 未做 JSON Schema 校验,若字段缺失则使用默认值(如
version=0),导致比对失败。
void execOTA()
功能:执行实际的二进制下载与烧录。
源码逻辑(分 Firmware/SPIFFS):
Firmware 分支:
// 1. 获取 OTA 分区信息 const esp_partition_t* update_partition = esp_ota_get_next_update_partition(NULL); // 2. 初始化 OTA 句柄 esp_ota_handle_t ota_handle; esp_ota_begin(update_partition, OTA_SIZE_UNKNOWN, &ota_handle); // 3. 构造 bin URL 并发起 HTTP GET char binUrl[256]; snprintf(binUrl, sizeof(binUrl), "http://%s:%d%s", m_host, m_port, m_binPath); // 4. 流式读取 HTTP 响应,写入 OTA 分区 while ((len = esp_http_client_read(client, buffer, sizeof(buffer))) > 0) { esp_ota_write(ota_handle, (const void*)buffer, len); } // 5. 结束 OTA,校验,重启 esp_ota_end(ota_handle); esp_ota_set_boot_partition(update_partition); esp_restart();SPIFFS 分支:
// 1. 打开 SPIFFS 文件(覆盖模式) File file = SPIFFS.open(m_binPath, "w"); // 注意:此处 m_binPath 是目标文件名,如 "/fs.bin" // 2. 流式写入 HTTP 响应体 while ((len = esp_http_client_read(client, buffer, sizeof(buffer))) > 0) { file.write((uint8_t*)buffer, len); } file.close(); // 3. 重挂载 SPIFFS(可选,确保新文件可见) SPIFFS.end(); SPIFFS.begin(true); // 格式化并重新挂载关键点:
- Firmware 更新后强制重启,由 Bootloader 加载新分区;
- SPIFFS 更新后不自动重启,但需调用
SPIFFS.end()/begin()以使新文件生效; - 无内置 CRC 校验,依赖 HTTP 层的 TCP 校验和,生产环境建议在 JSON 中增加
checksum字段并自行验证。
void setVer(uint32_t version)与void forceUpdate(const char* ip, uint16_t port, const char* binPath, const char* type)
setVer():覆盖构造时传入的m_currentVersion,用于运行时动态版本管理(如 SPIFFS)。forceUpdate():绕过 JSON 检查,直接指定 IP、端口、bin 路径进行强制更新。适用于内网调试或紧急修复,例如:void forceUpd() { majFW.forceUpdate("192.168.0.100", 80, "/firmware.bin", "FIRMWARE"); }
void useDeviceID = true
启用设备唯一 ID 模式。库会自动获取 ESP32 的 MAC 地址(esp_wifi_get_mac(WIFI_IF_STA, mac)),并将其哈希(如 MD5 前 8 字节)作为设备标识。此时,JSON URL 可动态拼接为http://server.com/update/<device_id>.json,实现千人千面的差异化更新策略。
4. 工程实践与代码示例
4.1 完整 Arduino 示例解析
以下为 README 中示例的逐行工程化解读:
#include <Arduino.h> #include <WiFi.h> #include <esp_log.h> #include <SPIFFS.h> #include "esp32HttpJsonOTA.h" #define SSID "WIFISSID" #define PASSWORD "WIFIPASSWORD" #define OTA_NAME "ESP32APPFR" #define OTA_VER 1 #define FWOTA_JSONURL "http://update.website.com/update/esp32ehjo/fw-esp32ehjo.json" #define FSOTA_JSONURL "http://update.website.com/update/esp32ehjo/fs-esp32ehjo.json" // 创建两个 OTA 实例:一个管固件,一个管文件系统 esp32HttpJsonOTA majFW(OTA_NAME, "FIRMWARE", OTA_VER, FWOTA_JSONURL); esp32HttpJsonOTA majFS(OTA_NAME, "SPIFFS", OTA_VER, FSOTA_JSONURL); static const char* TAG = "EHJO"; // 【SPIFFS 版本读取函数】 int verFS() { // 1. 初始化 SPIFFS if (!SPIFFS.begin(true)) { // true=格式化(首次运行) ESP_LOGE(TAG, "Mount error of SPIFFS..."); return 0; } // 2. 打开版本文件 /ver(纯文本,内容为数字) File myFile = SPIFFS.open("/ver", "r"); if (myFile) { String ver = myFile.readString(); // 读取全部内容 myFile.close(); ESP_LOGD(TAG, "SPIFFS version : %d", ver.toInt()); return ver.toInt(); } else { ESP_LOGW(TAG, "No /ver file found, assume version 1"); return 1; } } // 【固件更新检查】 bool newVerFW() { bool maj = majFW.execHTTPcheck(); // 发起 JSON 请求并比对 ESP_LOGD(TAG, "Sketch update available : %s", maj ? "Yes" : "No"); return maj; } // 【执行固件更新】 void updateFW() { majFW.execOTA(); // 下载并烧录,完成后自动重启 } // 【SPIFFS 更新检查】 bool newVerFS() { majFS.setVer(verFS()); // 设置当前 SPIFFS 版本 bool maj = majFS.execHTTPcheck(); ESP_LOGD(TAG, "SPIFFS update available : %s", maj ? "Yes" : "No"); return maj; } // 【执行 SPIFFS 更新】 void updateFS() { majFS.execOTA(); // 下载 bin 到 SPIFFS,需手动重挂载 } // 【WiFi 连接函数】 void setup_wifi() { WiFi.begin(SSID, PASSWORD); while (WiFi.status() != WL_CONNECTED) { Serial.print("."); delay(500); } ESP_LOGI(TAG, "IP address : %s", WiFi.localIP().toString().c_str()); } // 【主函数】 void setup() { esp_log_level_set("*", ESP_LOG_VERBOSE); // 全局日志级别 Serial.begin(115200); setup_wifi(); // 启动时检查更新(顺序:先固件,再文件系统) if (newVerFW()) { ESP_LOGI(TAG, "Sketch update available !"); updateFW(); // 此处会重启,后续代码不执行 } if (newVerFS()) { ESP_LOGI(TAG, "SPIFFS update available !"); updateFS(); // SPIFFS 更新后需重挂载才能生效 SPIFFS.end(); SPIFFS.begin(true); } } void loop() { delay(100); }工程要点总结:
SPIFFS.begin(true)在首次运行时格式化分区,生产环境应移除此参数,避免意外擦除用户数据;- 固件更新 (
updateFW()) 后设备立即重启,因此newVerFS()检查必须放在updateFW()之后,否则重启会中断流程; - SPIFFS 更新后必须调用
SPIFFS.end()/begin(),否则新文件不可见; - 日志级别设为
ESP_LOG_VERBOSE便于调试,量产时建议降为ESP_LOG_INFO以节省 Flash 空间。
4.2 生产环境增强建议
1. 添加 OTA 状态持久化
避免每次启动都检查更新,可将上次成功更新的版本号写入 NVS(Non-Volatile Storage):
#include "nvs_flash.h" #include "nvs.h" void saveCurrentVersion(const char* key, uint32_t version) { nvs_handle_t my_handle; nvs_open("ota", NVS_READWRITE, &my_handle); nvs_set_u32(my_handle, key, version); nvs_commit(my_handle); nvs_close(my_handle); } uint32_t loadCurrentVersion(const char* key, uint32_t defaultVer) { nvs_handle_t my_handle; uint32_t ver; nvs_open("ota", NVS_READONLY, &my_handle); esp_err_t err = nvs_get_u32(my_handle, key, &ver); nvs_close(my_handle); return (err == ESP_OK) ? ver : defaultVer; }2. 实现断点续传(HTTP Range)
对于大固件(>1MB),网络中断可能导致更新失败。可扩展execOTA()支持Range请求头,记录已下载字节数。
3. 集成 FreeRTOS 任务
将 OTA 封装为独立任务,避免阻塞loop():
void otaTask(void* pvParameters) { while(1) { if (newVerFW()) updateFW(); vTaskDelay(60000 / portTICK_PERIOD_MS); // 每分钟检查一次 } } xTaskCreate(otaTask, "ota_task", 8192, NULL, 5, NULL);5. 常见问题与故障排查
5.1 HTTP 连接失败(execHTTPcheck()返回 false)
| 现象 | 可能原因 | 排查步骤 |
|---|---|---|
HTTP request failed: ESP_ERR_HTTP_CONNECT_FAILURE | DNS 解析失败 | ping update.website.com检查域名可达性;确认WiFi.status() == WL_CONNECTED |
HTTP request failed: ESP_ERR_HTTP_INVALID_SERVER_CERT | 误用 HTTPS URL | 确认 JSON URL 以http://开头,非https:// |
HTTP request failed: ESP_ERR_HTTP_NO_CONTENT | 服务器返回空响应 | curl -v <JSON_URL>查看响应头与体;检查 GCS 的Content-Type是否为application/json |
5.2 版本比对始终为 false
- 检查 JSON 文件中
version字段是否为纯数字(无引号),如"version": 1✅,"version": "1"❌; - 确认
OTA_VER宏定义值与 JSON 中version严格一致(整数比较); - 若使用
setVer(),在execHTTPcheck()前添加ESP_LOGI(TAG, "Current ver: %d, Remote ver: %d", majFS.m_currentVersion, remoteVersion)日志。
5.3 SPIFFS 更新后文件不可见
- 确认
updateFS()执行后调用了SPIFFS.end()/begin(); - 检查
m_binPath在execOTA()中是否被正确解析为 SPIFFS 内部路径(如/fs.bin),而非服务器路径; - 使用
SPIFFS.open("/", "r")列出根目录,确认新文件存在。
5.4 Google Cloud Storage 配置指南
- 创建存储桶(Bucket),设置为“公共读取”;
- 上传
fw.json和fw.bin; - 在 GCS 控制台中,选中
fw.bin→ “编辑” → “Content-Type” → 修改为application/octet-stream; - 获取公开 URL:
https://storage.googleapis.com/<bucket-name>/fw.json; - 注意:GCS 的
https://URL 在本库中不可用,需替换为http://并指定端口80,即http://storage.googleapis.com:80/<bucket-name>/fw.json。
6. 安全性与生产部署考量
esp32HttpJsonOTA的 HTTP-only 设计在简化部署的同时,引入了明确的安全边界:
- 无传输加密:所有通信明文传输,禁止在公网暴露敏感设备。生产环境必须部署于受控网络(如企业内网、VPC、IoT 专用 APN);
- 无身份认证:JSON 与 BIN 文件为公开资源,任何获知 URL 的设备均可下载。可通过以下方式加固:
- URL 签名:服务端生成带时效性签名的 URL(如
?expires=1735689600&signature=abc123),设备端在构造 URL 时拼接; - IP 白名单:Web 服务器(Nginx/Apache)配置
allow 192.168.1.0/24; deny all;; - 设备 ID 绑定:启用
useDeviceID,服务端根据设备 ID 提供专属 JSON,避免全局更新风暴;
- URL 签名:服务端生成带时效性签名的 URL(如
- 无固件签名验证:库不校验 BIN 文件完整性。强烈建议在 JSON 中增加
sha256字段,并在execOTA()下载完成后调用mbedtls_sha256()计算并比对。
一个健壮的生产级 OTA 流程应为:
WiFi 连接 → 设备 ID 上报 → 获取签名 JSON → 校验 JSON 签名 → 解析 bin URL → 下载 bin → 校验 bin SHA256 → 烧录 → 重启。esp32HttpJsonOTA提供了其中 3 个环节(JSON 获取、BIN 下载、烧录)的坚实基础,其余环节需开发者根据安全等级需求补充。