ESP32远程OTA升级:从局域网到公网部署

目录

[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. 本地局域网测试

在部署到公网之前,先在局域网验证整个流程。

  1. 安装Flask

    在你的电脑上(与ESP32同一局域网)安装Python和Flask:

    复制代码
    pip install flask
  2. 启动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}

  3. 准备固件文件

    在Arduino IDE中打开你的项目,选择"项目" -> "导出已编译的二进制文件",生成.bin文件。重命名为firmware_2.0.0.bin,并放置在一个目录中(如C:\ota\firmware)。

  4. 启动HTTP文件服务器

    在固件目录下运行:

    复制代码
    cd C:\ota\firmware
    python -m http.server 8000

    现在可通过http://你的电脑IP:8000/firmware_2.0.0.bin下载固件。

  5. 修改ESP32代码中的check_url

    check_url改为你的电脑IP和端口,例如:

    复制代码
    const char* check_url = "http://192.168.1.100:8080/check_update";

    通过USB烧录到ESP32。

  6. 观察串口监视器

    设备启动后,每30秒检查一次。如果target_versions中配置了该设备的MAC且目标版本高于当前版本,则会触发OTA下载并重启。

  7. 验证

    重启后串口应显示版本号变为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公网测试

  1. 修改ESP32代码中的check_url

    改为公网地址:

    复制代码
    const char* check_url = "http://你的公网IP:8080/check_update";

    如果Flask端口是8081,则相应修改。

  2. 通过USB烧录新程序

    此时设备会向公网服务器请求更新。观察串口输出,应看到下载成功并重启。

  3. 验证

    重启后版本号变为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的网络端口功能,真正实现远程、自动化的固件升级。

相关推荐
cyforkk2 小时前
Spring Boot 3 集成 Swagger 踩坑实录:解决 doc.html 404 与 Unauthorized 拦截
spring boot·后端·html
weixin_430750932 小时前
提升备份效率——网络设备配置
网络·华为·信息与通信·一键备份·提高备份效率
小码哥_常2 小时前
当@RequestBody遇上Request:数据去哪儿了?
后端
yige452 小时前
SpringBoot 集成 Activiti 7 工作流引擎
java·spring boot·后端
DX_水位流量监测2 小时前
德希科技农村供水工程水质在线监测方案
大数据·运维·网络·水质监测·水质传感器·水质厂家·农村供水水质监测方案
G探险者3 小时前
SQL 性能优化实战:一次压测 404 的根因追查与解决
后端
欧云服务器3 小时前
魔方云批量更换ip教程
服务器·网络·tcp/ip
人间打气筒(Ada)3 小时前
如何使用 Go 更好地开发并发程序?
开发语言·后端·golang
honor_zhang3 小时前
Spring Boot集成Websocket服务以及连接时需要注意的问题
spring boot·后端·websocket