目录
[1. 引言](#1. 引言)
[2. 整体架构](#2. 整体架构)
[3. 准备工作](#3. 准备工作)
[4. 设备端代码](#4. 设备端代码)
[5. 服务端代码(Flask)](#5. 服务端代码(Flask))
[6. 本地局域网测试](#6. 本地局域网测试)
[7. 部署到公网服务器(腾讯云轻量服务器为例)](#7. 部署到公网服务器(腾讯云轻量服务器为例))
[7.1 环境准备](#7.1 环境准备)
[7.2 上传服务端代码](#7.2 上传服务端代码)
[7.3 创建固件目录并上传固件](#7.3 创建固件目录并上传固件)
[7.4 启动Flask服务(后台运行)](#7.4 启动Flask服务(后台运行))
[7.5 启动HTTP文件服务器(后台运行)](#7.5 启动HTTP文件服务器(后台运行))
[7.6 配置防火墙](#7.6 配置防火墙)
[7.7 测试公网访问](#7.7 测试公网访问)
[8. ESP32公网测试](#8. ESP32公网测试)
[9. 常见问题与解决](#9. 常见问题与解决)
[10. 后续扩展与安全建议](#10. 后续扩展与安全建议)
[11. 总结](#11. 总结)
1. 引言
在物联网开发中,设备固件更新是一项基本且重要的需求。通过远程OTA(Over‑The‑Air)升级,我们可以无需物理接触设备,就能修复漏洞、增加功能,极大提升维护效率。
本文将以ESP32为核心,带你从零实现一套基于HTTP的远程升级方案。你将学会:
-
在局域网内完成OTA功能验证
-
将服务部署到公网云服务器(以腾讯云轻量服务器为例)
-
实现指定设备的精准升级
最终效果:ESP32上电后自动连接WiFi,定期向你的服务器查询是否有新固件;有则自动下载、烧录并重启,整个过程无需人工干预。
2. 整体架构
-
ESP32设备:运行用户程序,其中包含OTA检查逻辑。设备通过WiFi连接互联网,定期向服务端发送HTTP GET请求,携带设备ID(MAC地址)和当前版本号。
-
Flask服务端:接收设备请求,根据设备ID查询其"目标版本",若目标版本高于当前版本,则返回固件下载URL。
-
HTTP文件服务器:提供固件文件(.bin)的下载服务,通常与Flask运行在同一台服务器上,使用另一个端口。
数据流 :
设备 → Flask(检查更新) → 返回固件URL → 设备 → 文件服务器(下载固件) → 设备烧录 → 重启。
3. 准备工作
硬件
-
ESP32开发板(如ESP32‑DevKitC)一块
-
USB数据线(用于首次烧录和调试)
-
电脑(Windows / macOS / Linux)
-
公网服务器(本文以腾讯云轻量应用服务器 Ubuntu 22.04 为例)
软件
-
Arduino IDE(已安装ESP32开发板支持包)
-
Python 3.x(服务器端)
-
库:ArduinoJson(设备端)、Flask(服务端)
4. 设备端代码
创建一个新的Arduino项目,完整代码如下。注意替换WiFi凭据和服务器地址(本地测试时先用电脑局域网IP)。
#include <WiFi.h>
#include <HTTPClient.h>
#include <Update.h>
#include <ArduinoJson.h>
// ========== 配置区 ==========
const char* ssid = "你的WiFi名称";
const char* password = "你的WiFi密码";
// 设备唯一标识(使用MAC地址)
String device_id = WiFi.macAddress();
// 当前固件版本(语义化版本,如 "1.0.0")
String current_version = "1.0.0";
// 检查更新的API地址(局域网测试时填电脑IP,公网填服务器公网IP)
const char* check_url = "http://192.168.1.100:8080/check_update";
// 检查间隔(毫秒),测试时可设为30秒,生产环境建议1小时以上
const unsigned long check_interval = 30000;
// ==========================
void setup() {
Serial.begin(115200);
WiFi.begin(ssid, password);
Serial.print("Connecting to WiFi");
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("\nWiFi connected");
Serial.print("IP address: ");
Serial.println(WiFi.localIP());
Serial.print("Device ID: ");
Serial.println(device_id);
}
void loop() {
static unsigned long last_check = 0;
if (millis() - last_check > check_interval) {
last_check = millis();
checkForUpdate();
}
// 你的其他业务代码(不要用长时间delay阻塞)
}
void checkForUpdate() {
HTTPClient http;
String url = String(check_url) + "?device_id=" + device_id + "&version=" + current_version;
Serial.print("Checking update: ");
Serial.println(url);
http.begin(url);
int httpCode = http.GET();
if (httpCode == 200) {
String payload = http.getString();
Serial.println("Response: " + payload);
// 解析JSON
DynamicJsonDocument doc(1024);
DeserializationError error = deserializeJson(doc, payload);
if (!error) {
bool update = doc["update"];
if (update) {
String firmware_url = doc["url"].as<String>();
Serial.println("Firmware URL: " + firmware_url);
performOTA(firmware_url);
} else {
Serial.println("No update available.");
}
} else {
Serial.println("JSON parse failed");
}
} else {
Serial.printf("HTTP GET failed, error: %s\n", http.errorToString(httpCode).c_str());
}
http.end();
}
void performOTA(String url) {
HTTPClient http;
http.setTimeout(10000); // 10秒超时
http.begin(url);
int httpCode = http.GET();
if (httpCode != 200) {
Serial.printf("Download failed, HTTP code: %d\n", httpCode);
http.end();
return;
}
int contentLength = http.getSize();
if (contentLength <= 0) {
Serial.println("Invalid content length");
http.end();
return;
}
Serial.printf("Starting OTA, size: %d bytes\n", contentLength);
if (!Update.begin(contentLength)) {
Serial.println("Not enough space to begin OTA");
http.end();
return;
}
size_t written = Update.writeStream(http.getStream());
if (written == contentLength) {
Serial.println("Written : " + String(written) + " bytes successfully");
if (Update.end()) {
Serial.println("OTA done, restarting...");
ESP.restart();
} else {
Serial.println("Error in Update.end()");
}
} else {
Serial.println("Written only : " + String(written) + "/" + String(contentLength) + ". Retry?");
}
http.end();
}
关键点
-
设备ID :使用
WiFi.macAddress(),确保每台设备唯一。 -
版本管理 :当前版本
current_version与服务器目标版本比较决定是否更新。 -
JSON解析 :使用
ArduinoJson库,避免字符串匹配的脆弱性。 -
OTA下载 :
Update库负责将固件写入Flash,支持失败自动回滚。
5. 服务端代码(Flask)
在服务器(或电脑)上创建文件ota_server.py,内容如下:
from flask import Flask, request, jsonify
app = Flask(__name__)
# 模拟数据库:设备ID -> 目标版本号
target_versions = {
"AA:BB:CC:DD:EE:FFC": "2.0.0", # 示例MAC,请替换为你的设备MAC
}
# 固件下载的基础URL(公网服务器IP+端口)
FIRMWARE_BASE_URL = "http://你的公网IP:8000/"
@app.route('/check_update')
def check_update():
device_id = request.args.get('device_id')
current_ver = request.args.get('version')
print(f"Device: {device_id}, current: {current_ver}")
target = target_versions.get(device_id)
if target and target != current_ver:
firmware_url = FIRMWARE_BASE_URL + "firmware_" + target + ".bin"
return jsonify({"update": True, "url": firmware_url})
else:
return jsonify({"update": False})
if __name__ == '__main__':
app.run(host='0.0.0.0', port=8080)
说明
-
端口:示例使用8080,若被占用可改为其他(如8081),记得在防火墙开放相应端口。
-
固件URL :按版本号拼接,例如
firmware_2.0.0.bin,文件需提前放置于文件服务器目录。 -
目标版本 :通过修改
target_versions字典,可精准控制哪些设备升级到哪个版本。
6. 本地局域网测试
在部署到公网之前,先在局域网验证整个流程。
-
安装Flask
在你的电脑上(与ESP32同一局域网)安装Python和Flask:
pip install flask -
启动Flask服务
运行
ota_server.py:python ota_server.py服务默认监听
http://0.0.0.0:8080,可用浏览器访问http://你的电脑IP:8080/check_update?device_id=test&version=1.0测试,应返回{"update":false}。 -
准备固件文件
在Arduino IDE中打开你的项目,选择"项目" -> "导出已编译的二进制文件",生成
.bin文件。重命名为firmware_2.0.0.bin,并放置在一个目录中(如C:\ota\firmware)。 -
启动HTTP文件服务器
在固件目录下运行:
cd C:\ota\firmware python -m http.server 8000现在可通过
http://你的电脑IP:8000/firmware_2.0.0.bin下载固件。 -
修改ESP32代码中的
check_url将
check_url改为你的电脑IP和端口,例如:const char* check_url = "http://192.168.1.100:8080/check_update";通过USB烧录到ESP32。
-
观察串口监视器
设备启动后,每30秒检查一次。如果
target_versions中配置了该设备的MAC且目标版本高于当前版本,则会触发OTA下载并重启。 -
验证
重启后串口应显示版本号变为
2.0.0,且不再触发更新。
7. 部署到公网服务器(腾讯云轻量服务器为例)
7.1 环境准备
-
登录服务器(SSH),更新软件包:
sudo apt update && sudo apt upgrade -y -
安装Python3和pip(系统通常已自带),以及Flask:
sudo apt install python3-flask -y
7.2 上传服务端代码
将本地测试成功的ota_server.py上传到服务器,例如/home/ubuntu/ota/。
注意修改FIRMWARE_BASE_URL为你的公网IP和文件服务器端口(如8000)。
7.3 创建固件目录并上传固件
mkdir -p /home/ubuntu/ota/firmware
将firmware_2.0.0.bin上传到该目录,并确保文件名正确。
7.4 启动Flask服务(后台运行)
cd /home/ubuntu/ota
nohup python3 ota_server.py > ota.log 2>&1 &
查看日志:tail -f ota.log,确认服务正常启动。
7.5 启动HTTP文件服务器(后台运行)
cd /home/ubuntu/ota/firmware
nohup python3 -m http.server 8000 > http.log 2>&1 &
7.6 配置防火墙
-
腾讯云控制台:进入实例的"防火墙"页面,添加入站规则:
-
协议:TCP,端口:8080(Flask端口),策略:允许
-
协议:TCP,端口:8000(文件服务端口),策略:允许
-
-
服务器内部防火墙(如果启用ufw):
sudo ufw allow 8080/tcp sudo ufw allow 8000/tcp
7.7 测试公网访问
在本地浏览器访问:
-
http://你的公网IP:8080/check_update?device_id=test&version=1.0→ 应返回JSON -
http://你的公网IP:8000/firmware_2.0.0.bin→ 应下载固件
若返回404,请检查文件路径和端口防火墙。
8. ESP32公网测试
-
修改ESP32代码中的
check_url改为公网地址:
const char* check_url = "http://你的公网IP:8080/check_update";如果Flask端口是8081,则相应修改。
-
通过USB烧录新程序
此时设备会向公网服务器请求更新。观察串口输出,应看到下载成功并重启。
-
验证
重启后版本号变为
2.0.0,且不再触发更新。
9. 常见问题与解决
| 现象 | 可能原因 | 解决方法 |
|---|---|---|
| 设备无法连接WiFi | SSID/密码错误 | 检查代码中的WiFi凭据 |
| 检查更新时无响应或超时 | 服务器IP/端口错误,或防火墙阻挡 | 确认公网IP正确,防火墙已开放端口 |
| 收到JSON但显示"No update available." | JSON解析失败或版本比较错误 | 使用ArduinoJson正确解析;检查target_versions配置 |
下载固件时Written only : 0 |
ESP32与服务器网络不通,或文件服务器端口被防火墙拦截 | 确认防火墙开放8000端口,ESP32能ping通服务器 |
| 下载后设备反复重启 | 新固件有bug或未包含OTA代码 | 回退旧固件,检查新固件是否包含OTA检查逻辑 |
| 公网访问404 | 文件服务器未运行或路径错误 | 确认http.server在固件目录运行,文件名称正确 |
10. 后续扩展与安全建议
-
HTTPS:生产环境应使用HTTPS,防止固件被篡改。可配置Nginx反向代理+Let's Encrypt免费证书。
-
身份验证:在Flask接口中加入token参数,避免恶意请求。
-
数据库 :将
target_versions存储在SQLite/MySQL中,便于管理大量设备。 -
灰度发布:可按设备分组或百分比逐步推送新版本。
-
轮询间隔:根据业务调整,避免服务器压力过大。
-
版本回滚:保留旧版本固件,若新版本异常,可将目标版本改回旧版,设备下次轮询时回滚。
11. 总结
通过本文,你已经掌握了从零构建ESP32 HTTP OTA系统的完整方法。从本地局域网测试到公网部署,我们实现了:
-
设备主动轮询更新
-
服务端精准控制指定设备
-
固件文件独立存储与下载
这套方案可以灵活扩展到成百上千台设备,且无需依赖Arduino IDE的网络端口功能,真正实现远程、自动化的固件升级。