一、背景
ESP32 智能家居项目涉及多个传感器的数据采集和云端通信,代码迭代非常频繁。每次更新固件都要用 USB 线连接开发板,拔插、选端口、烧录,实在太麻烦了。
于是决定把 OTA(Over-The-Air,空中升级)功能加上,实现无线固件更新。
按理说 Arduino 的 OTA 功能很成熟,几行代码就能搞定。但实际上,我在这条路上踩了好几个坑,花了两天才彻底打通。这篇文章记录整个排查过程,希望帮你省点时间。
二、环境说明
| 项目 | 配置 |
|---|---|
| 开发板 | TinkerNode NB-IoT |
| 核心库版本 | 1.0.8(基于 ESP32 2.0.x) |
| IDE | Arduino 1.8.19 |
| OTA 库 | ArduinoOTA(核心库自带) |
| 电脑系统 | Windows 11 |
三、具体问题
使用IDE自带的示例程序进行验证
cpp
#include <WiFi.h>
#include <ESPmDNS.h>
#include <WiFiUdp.h>
#include <ArduinoOTA.h>
const char* ssid = "..........";
const char* password = "..........";
void setup() {
Serial.begin(115200);
Serial.println("Booting");
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
while (WiFi.waitForConnectResult() != WL_CONNECTED) {
Serial.println("Connection Failed! Rebooting...");
delay(5000);
ESP.restart();
}
ArduinoOTA
.onStart([]() {
String type;
if (ArduinoOTA.getCommand() == U_FLASH)
type = "sketch";
else // U_SPIFFS
type = "filesystem";
// NOTE: if updating SPIFFS this would be the place to unmount SPIFFS using SPIFFS.end()
Serial.println("Start updating " + type);
})
.onEnd([]() {
Serial.println("\nEnd");
})
.onProgress([](unsigned int progress, unsigned int total) {
Serial.printf("Progress: %u%%\r", (progress / (total / 100)));
})
.onError([](ota_error_t error) {
Serial.printf("Error[%u]: ", error);
if (error == OTA_AUTH_ERROR) Serial.println("Auth Failed");
else if (error == OTA_BEGIN_ERROR) Serial.println("Begin Failed");
else if (error == OTA_CONNECT_ERROR) Serial.println("Connect Failed");
else if (error == OTA_RECEIVE_ERROR) Serial.println("Receive Failed");
else if (error == OTA_END_ERROR) Serial.println("End Failed");
});
ArduinoOTA.begin();
Serial.println("Ready");
Serial.print("IP address: ");
Serial.println(WiFi.localIP());
}
void loop() {
ArduinoOTA.handle();
}
第一步: USB烧录OTA示例代码,一切正常。
第二步: 拔掉USB,Arduino IDE切换为网络端口(xxx.xxx.x.xxx)。
第三步: 修改代码(比如加个版本号打印),点击上传。
预期结果: 上传成功
实际结果: 
就卡在这里,反复试了十几次,同一行报错。能看到设备,能认证通过,但就是传不上去。
四、问题排查
把常规方向都排查了一遍:
-
代码没问题(用的官方示例)
-
网络没问题(能 ping 通,能发现设备)
-
防火墙关了
-
端口没被占用
都没问题,开始抓包用 Wireshark 监听 OTA 通信过程:
红色方框为电脑端,绿色方框为开发板端。从截图可以清楚看到:UDP发现阶段正常 ,ESP32成功广播了自己的存在;但进入TCP握手阶段后,电脑没有回复ESP32发来的SYN包。
通俗点说:ESP32主动发起连接请求(三次握手),但电脑那边像没听见一样,完全不搭理。更诡异的是,我多次抓包后发现:TCP传输时ESP32用的是随机端口,而不是固定端口。这意味着无法判断电脑端是否在对应端口上正常监听,排查一度陷入僵局。
上ESP32 OTA官方文档查资料时发现,官方推荐用的工具是espota.py,而Arduino IDE默认调用的OTA上传工具是TinkerNode核心库自带的espota.exe。这两个工具名字相似,但前者是Python脚本,后者是独立可执行文件。尝试在命令行手动调用espota.py上传固件.bin文件------一次成功 。再去Wireshark里看,这次UDP发现和TCP握手都正常了。看来问题就出在espota.exe这个工具上。

五、密码认证失败
第一次调试时没设密码,直接用了默认无密码模式。项目正式用肯定要加密码,在代码里添加:
cpp
ArduinoOTA.setPassword("123456");
然后重新上传,结果密码明明是对的,但就是验证不过去,一直显示密码错误。WiFi连接、IP地址也都检查过,没问题。同样的环境,不加密码就通,加了密码就失败------问题只能出在密码认证这个环节。查看 ArduinoOTA 源码(ArduinoOTA.cpp)发现:setPassword() 底层把明文密码转成了MD5哈希值存储------也就是说,开发板端存的是密码的MD5,而不是密码本身。而espota.py默认走的是新安全协议,与旧版核心库的MD5协议不兼容。两边对不上,自然验证失败。
cpp
ArduinoOTAClass& ArduinoOTAClass::setPassword(const char * password) {
if (!_initialized && !_password.length() && password) {
MD5Builder passmd5;
passmd5.begin();
passmd5.add(password);
passmd5.calculate();
_password = passmd5.toString(); // 存的是 MD5 哈希值!
}
return *this;
}
解决方案: 查了下espota.py的参数说明,发现有一个-m参数可以强制走MD5协议,加了 -m 参数后密码认证通过,上传成功。
六、一劳永逸
每次都敲命令行太麻烦,直接修改 Arduino IDE 的配置文件。
找到 platform.txt:C:\Users\你的用户名\...\Arduino15\packages\TinkerNode_NB-IoT\hardware\esp32\1.0.8\platform.txt(不同安装路径可能不同,按实际情况来)
修改 upload.network_pattern 这一行,原本是:tools.esptool_py.upload.network_pattern={network_cmd} -i "{serial.port}" -p "{network.port}" "--auth={network.password}" -f "{build.path}/{build.project_name}.bin"
替换为:tools.esptool_py.upload.network_pattern=C:\Users\86136\AppData\Local\Programs\Python\Python312\python.exe "{runtime.platform.path}/tools/espota.py" -i "{serial.port}" -p "{network.port}" --auth="{network.password}" -f "{build.path}/{build.project_name}.bin" -m
保存后重启 Arduino IDE,点击上传,直接走网络端口,一键无线升级。
七、总结
踩坑清单
| 问题 | 原因 | 解决 |
|---|---|---|
| OTA 上传无响应 | espota.exe TCP 服务端启动失败 |
换用 espota.py |
| 密码认证失败 | 新工具默认用新协议,核心库用 MD5 | 加 -m 强制 MD5 |
| 每次都敲命令行麻烦 | IDE 配置未修改 | 修改 platform.txt |
相关资源: