BasicOTA & OTAWebUpdater
一、前言
想象一下你做了一个遥控小车,但是每次更新程序都要和上位机连线。有什么方法不用每次更新都连线呢? 有时单片机放置的位置受到遮挡或其他原因,引出烧录线比较麻烦。好在NanoE支持WiFi功能,我们可以使用无线远程烧录的办法。
本节实验只需要NanoE开发板,无需其他外设模块。我们会带大家尝试通过两种示例程序实现远程固件升级:
BasicOTA
:从Arduino IDE里面直接烧录,先用本地COM端口把BasicOTA烧录进去以后,再次选择端口时可以看到,多了个带有ESP32WIFi的IP地址的网络端口
,通过该串口就能实现无线烧录了。
OTAWebUpdate
:用ESP32WIFi连上路由,开启WebServer。在浏览器里面打开后,先登陆页面,然后把事先编译好的二进制.bin
文件用#include <Update.h>
中的Update.write
写进去。
二、使用BasicOTA方式
(一)准备工作
1. 在Arduino IDE工具中选择开发板。
2. 选择好开发板型号后,打开示例程序BasicOTA,路径如下图。
3. 打开工具栏,开发板参数按下图配置。
4. 准备一个WIFI热点,名称最好为英文。
- 推荐将NanoE和电脑连接至同一WiFi热点。
- 华为与安卓手机需要将热点频段为
2.4GHz
,iPhone需要打开 最大兼容性
功能。这是因为NanoE(ESP32)这样的嵌入式设备通常仅支持 2.4GHz 频段。
(二)程序设计
1. 我们打开示例程序BasicOTA
2. 示例代码如下,注意示例代码需要修改:
- ssid是热点WiFi名称(区分大小写),password是WiFi密码。
- 以下是对BasicOTA示例代码的注释:
#include <WiFi.h> // 引入WiFi库,用于连接WiFi网络
#include <ESPmDNS.h> // 引入mDNS库,用于通过网络发现服务
#include <WiFiUdp.h> // 引入WiFi UDP库,用于UDP通信
#include <ArduinoOTA.h> // 引入ArduinoOTA库,用于实现无线更新(OTA)
// 定义WiFi的SSID和密码
const char* ssid = "xxx"; // WiFi名称
const char* password = "xxxxxxxx"; // WiFi密码
void setup() {
Serial.begin(115200); // 初始化串口通信,波特率为115200
Serial.println("Booting"); // 输出启动信息
WiFi.mode(WIFI_STA); // 设置WiFi工作模式为STA(Station模式)
WiFi.begin(ssid, password); // 连接WiFi
// 检查WiFi连接状态,直到成功连接
while (WiFi.waitForConnectResult() != WL_CONNECTED) {
Serial.println("Connection Failed! Rebooting..."); // 如果连接失败,输出提示信息并重启
delay(5000); // 延时5秒
ESP.restart(); // 重启ESP32
}
// 设置OTA更新的相关配置(以下代码为可选修改项)
// Port defaults to 3232
// ArduinoOTA.setPort(3232); // 默认端口为3232,如果需要自定义则取消注释并设置
// Hostname defaults to esp3232-[MAC]
// ArduinoOTA.setHostname("myesp32"); // 设置OTA的设备主机名,默认为esp3232-[MAC]
// No authentication by default
// ArduinoOTA.setPassword("admin"); // 设置OTA更新的密码(明文)
// Password can be set with it's md5 value as well
// MD5(admin) = 21232f297a57a5a743894a0e4a801fc3
// ArduinoOTA.setPasswordHash("21232f297a57a5a743894a0e4a801fc3"); // 也可以设置密码的MD5哈希值
// 配置ArduinoOTA的回调函数,用于处理OTA更新过程中的事件
ArduinoOTA
.onStart([]() { // 设置OTA开始事件
String type; // 记录更新类型(sketch或filesystem)
// 判断更新是应用代码(U_FLASH)还是文件系统(U_SPIFFS)
if (ArduinoOTA.getCommand() == U_FLASH)
type = "sketch"; // 更新应用代码
else
type = "filesystem"; // 更新文件系统
// 注意:如果是更新文件系统,此处可以调用SPIFFS.end()卸载SPIFFS
Serial.println("Start updating " + type); // 输出更新类型
})
.onEnd([]() { // 设置OTA结束事件
Serial.println("\nEnd"); // 输出结束信息
})
.onProgress([](unsigned int progress, unsigned int total) { // 设置OTA进度事件
Serial.printf("Progress: %u%%\r", (progress / (total / 100))); // 输出更新进度
})
.onError([](ota_error_t error) { // 设置OTA错误事件
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(); // 启动OTA服务
Serial.println("Ready"); // 输出提示信息,表示OTA服务已开启
Serial.print("IP address: "); // 输出设备的IP地址
Serial.println(WiFi.localIP()); // 获取并打印设备的IP地址
}
void loop() {
ArduinoOTA.handle(); // 在主循环中调用ArduinoOTA的handle()处理更新请求
}
3. 编译运行
- 编译运行前,再次检查开发板参数是否按照上文要求配置。
- 点击编译,程序无报错并上传后,查看开发板端界面,会发现多了个带有ESP32WIFi的IP地址的网络端口,之后通过该串口就能实现无线烧录了。
注意:
如果这里没有出现没有新增的网络端口
,请打开工具栏的串口监视器,再次编译上传程序后,检查串口监视器是否输出了IP address
,如下图。
若成功输出IP address
,则说明网络连接成功,但可能被Windows的防火墙拦截了。这种情况,需要进入Windows设置,查看网络属性,将网络配置文件类型切换成专用网络
。再次回到Arduino IDE,应该就能看到新增的网络端口
了。
4. BasicOTA示例方法总结
现在可以将NanoE连接到外接电源(比如充电宝)而非电脑,试着编写一个亮灯小程序并通过网络端口
烧录上传到开发板。以上就是通过BasicOTA示例程序实现无线烧录(固件更新),其优缺点如下:
优点:
- 简单易用:实现代码简洁,适合初学者或只需基本功能的项目。
- 直接对接 Arduino IDE:可以直接通过 Arduino IDE 的 网络端口 上传固件,方便调试和上传。
- 轻量化:代码和占用资源少,适合单片机内存、资源有限的项目。
缺点:
- 功能单一:只支持通过 Arduino IDE 上传程序,不支持文件系统或其他形式的更新。
- 界面限制:操作需要依赖 IDE。
三、使用OTAWebUpdater方式
(一)准备工作
这里准备工作与上文使用BasicOTA方式相同。
(二)程序设计
1. 我们打开示例程序WebUpdater
2. 软件程序设计
WebUpdater
示例代码及其注释如下:
#include <WiFi.h> // 引入WiFi库,用于ESP32连接WiFi网络
#include <WiFiClient.h> // 引入WiFi客户端库,用于网络通信
#include <WebServer.h> // 引入WebServer库,用于搭建HTTP服务器
#include <ESPmDNS.h> // 引入mDNS库,用于通过mDNS协议设置设备名称
#include <Update.h> // 引入Update库,用于OTA(Over The Air)固件更新
// 定义WiFi的相关信息
const char* host = "esp32"; // 设备的mDNS主机名
const char* ssid = "xxx"; // WiFi SSID(网络名称)
const char* password = "xxxx"; // WiFi密码
WebServer server(80); // 创建一个WebServer对象,监听80端口
/*
* 登录页面的HTML代码
*/
const char* loginIndex =
"<form name='loginForm'>" // 表单,用于登录
"<table width='20%' bgcolor='A09F9F' align='center'>" // 表单表格样式
"<tr>"
"<td colspan=2>"
"<center><font size=4><b>ESP32 Login Page</b></font></center>" // 页面标题
"<br>"
"</td>"
"<br>"
"<br>"
"</tr>"
"<tr>"
"<td>Username:</td>" // 用户名输入框
"<td><input type='text' size=25 name='userid'><br></td>"
"</tr>"
"<br>"
"<br>"
"<tr>"
"<td>Password:</td>" // 密码输入框
"<td><input type='Password' size=25 name='pwd'><br></td>"
"<br>"
"<br>"
"</tr>"
"<tr>"
"<td><input type='submit' onclick='check(this.form)' value='Login'></td>" // 登录按钮
"</tr>"
"</table>"
"</form>"
"<script>"
"function check(form)" // JavaScript函数,用于验证用户名和密码
"{"
"if(form.userid.value=='admin' && form.pwd.value=='admin')" // 验证用户名和密码是否为"admin"
"{"
"window.open('/serverIndex')" // 如果验证通过,跳转到服务器主页
"}"
"else"
"{"
" alert('Error Password or Username')" // 验证失败,弹出错误提示
"}"
"}"
"</script>";
/*
* 服务器主页的HTML代码
* 提供一个文件上传表单,用于OTA更新
*/
const char* serverIndex =
"<script src='https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js'></script>" // 引入jQuery库
"<form method='POST' action='#' enctype='multipart/form-data' id='upload_form'>" // 文件上传表单
"<input type='file' name='update'>" // 文件选择按钮
"<input type='submit' value='Update'>" // 提交按钮
"</form>"
"<div id='prg'>progress: 0%</div>" // 显示上传进度
"<script>"
"$('form').submit(function(e){" // 使用jQuery处理表单提交事件
"e.preventDefault();" // 阻止默认表单提交行为
"var form = $('#upload_form')[0];" // 获取表单对象
"var data = new FormData(form);" // 创建FormData对象,用于上传文件
" $.ajax({" // 使用AJAX上传文件
"url: '/update'," // 上传目标URL
"type: 'POST'," // POST请求
"data: data," // 上传的数据
"contentType: false," // 禁用默认的内容类型
"processData:false," // 禁用数据处理
"xhr: function() {" // 自定义XHR对象,用于处理上传进度
"var xhr = new window.XMLHttpRequest();"
"xhr.upload.addEventListener('progress', function(evt) {" // 监听上传进度事件
"if (evt.lengthComputable) {"
"var per = evt.loaded / evt.total;" // 计算进度百分比
"$('#prg').html('progress: ' + Math.round(per*100) + '%');" // 更新进度显示
"}"
"}, false);"
"return xhr;"
"},"
"success:function(d, s) {" // 上传成功回调函数
"console.log('success!')"
"},"
"error: function (a, b, c) {" // 上传失败回调函数
"}"
"});"
"});"
"</script>";
/*
* setup函数
* 初始化WiFi连接、mDNS服务和HTTP服务器
*/
void setup(void) {
Serial.begin(115200); // 初始化串口,用于调试
// 连接WiFi网络
WiFi.begin(ssid, password);
Serial.println("");
// 等待WiFi连接成功
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("");
Serial.print("Connected to ");
Serial.println(ssid); // 输出连接的WiFi名称
Serial.print("IP address: ");
Serial.println(WiFi.localIP()); // 输出设备的IP地址
/* 使用mDNS协议设置主机名 */
if (!MDNS.begin(host)) { // 设置mDNS主机名,例如http://esp32.local
Serial.println("Error setting up MDNS responder!");
while (1) {
delay(1000);
}
}
Serial.println("mDNS responder started");
/* 配置HTTP服务器的路由 */
server.on("/", HTTP_GET, []() { // 配置根路径的GET请求,返回登录页面
server.sendHeader("Connection", "close");
server.send(200, "text/html", loginIndex);
});
server.on("/serverIndex", HTTP_GET, []() { // 配置/serverIndex路径的GET请求,返回服务器主页
server.sendHeader("Connection", "close");
server.send(200, "text/html", serverIndex);
});
/* 处理固件上传 */
server.on("/update", HTTP_POST, []() { // 配置/update路径的POST请求,用于处理上传完成后的响应
server.sendHeader("Connection", "close");
server.send(200, "text/plain", (Update.hasError()) ? "FAIL" : "OK"); // 根据上传结果返回OK或FAIL
ESP.restart(); // 上传完成后重启设备
}, []() { // 配置/update路径的文件上传处理函数
HTTPUpload& upload = server.upload(); // 获取上传对象
if (upload.status == UPLOAD_FILE_START) { // 文件上传开始
Serial.printf("Update: %s\n", upload.filename.c_str()); // 输出上传的文件名
if (!Update.begin(UPDATE_SIZE_UNKNOWN)) { // 开始更新,允许任意大小
Update.printError(Serial); // 如果失败,打印错误信息
}
} else if (upload.status == UPLOAD_FILE_WRITE) { // 文件上传中
if (Update.write(upload.buf, upload.currentSize) != upload.currentSize) { // 写入数据
Update.printError(Serial); // 如果失败,打印错误信息
}
} else if (upload.status == UPLOAD_FILE_END) { // 文件上传结束
if (Update.end(true)) { // 结束更新,验证更新是否成功
Serial.printf("Update Success: %u\nRebooting...\n", upload.totalSize); // 成功更新
} else {
Update.printError(Serial); // 如果失败,打印错误信息
}
}
});
server.begin(); // 启动HTTP服务器
}
/*
* loop函数
* 不断处理客户端请求
*/
void loop(void) {
server.handleClient(); // 处理HTTP客户端请求
delay(1); // 短暂延迟,避免阻塞
}
我们可以通过在loop
函数中添加LED灯闪烁功能,便能直观判断NanoE在网络环境中的状态(失联或工作)。
const int LED = 13;
· · ·
void loop(void) {
server.handleClient();
delay(1);
digitalWrite(LED, HIGH);
delay(500);
digitalWrite(LED, LOW);
delay(500);
}
3. 编译运行
- 编译运行前,再次检查开发板参数是否按照上文要求配置;
- 点击编译,程序无报错并完成上传后,查看串口监视器,随后一直发送
.....
,就是在连接中;
- 连接完成后,会发送自己的IP地址。如果在
loop
函数中添加了LED灯闪烁功能,此时板载的LED灯会闪烁;
- 在此过程中,如果发现一直在发
.....
,可能是程序代码中的ssid
和password
有误。
- 获取IP地址后,在上位机(电脑或手机)中打开浏览器,输入IP地址回车。出现如下界面。
- 输入账号:
admin
,密码:admin
,进入下方界面。
- 至此,我们满足了WebUpdater固件升级的所有前置条件。
4. 修改网页样式
- 大家会发现,示例程序中大部分代码作用是制作登录和上传文件的Web页面,是通过HTML + CSS 实现了一个文件上传的静态页面,允许用户上传新固件文件,并通过 JavaScript 实现实时查看上传进度的功能。
因此,我们可以通过基本的HTML结构和CSS样式设置,让我们的登录界面更加美观一些。
以下是网页美化后的完整示例代码,大家也可以设计自己的网页。
#include <WiFi.h>
#include <WiFiClient.h>
#include <WebServer.h>
#include <ESPmDNS.h>
#include <Update.h>
const char* host = "esp32";
const char* ssid = "xxxx";
const char* password = "xxxxxxxx";
WebServer server(80);
const int LED = 13;
/*
* Login page
*/
const char* loginIndex =
"<style>"
"body {"
" font-family: Arial, sans-serif;"
" margin: 0;"
" padding: 0;"
" background-color: #f2f2f2;"
" display: flex;"
" justify-content: center;"
" align-items: center;"
" height: 100vh;"
"}"
".form-container {"
" background-color: #ffffff;"
" padding: 20px 30px;"
" border-radius: 10px;"
" box-shadow: 0 5px 15px rgba(0,0,0,0.2);"
" text-align: center;"
"}"
".form-container h1 {"
" margin-bottom: 20px;"
" font-size: 24px;"
" color: #333333;"
"}"
".form-container input[type='text'],"
".form-container input[type='password'] {"
" width: 100%;"
" padding: 10px;"
" margin: 10px 0;"
" border: 1px solid #cccccc;"
" border-radius: 5px;"
" box-sizing: border-box;"
"}"
".form-container input[type='submit'] {"
" background-color: #4CAF50;"
" color: white;"
" border: none;"
" padding: 10px 20px;"
" text-transform: uppercase;"
" letter-spacing: 2px;"
" font-weight: bold;"
" cursor: pointer;"
" border-radius: 5px;"
" margin-top: 15px;"
"}"
".form-container input[type='submit']:hover {"
" background-color: #45a049;"
"}"
".error-message {"
" color: red;"
" font-size: 14px;"
" margin-top: 10px;"
" display: none;"
"}"
"</style>"
"<body>"
"<div class='form-container'>"
"<h1>ESP32 Login Page</h1>"
"<form name='loginForm'>"
"<input type='text' placeholder='Enter Username' name='userid'>"
"<input type='password' placeholder='Enter Password' name='pwd'>"
"<input type='submit' onclick='check(this.form)' value='Login'>"
"</form>"
"<div class='error-message' id='error'>Error: Invalid Username or Password.</div>"
"</div>"
"</body>"
"<script>"
"function check(form) {"
" var errorMessage = document.getElementById('error');"
" if (form.userid.value == 'admin' && form.pwd.value == 'admin') {"
" window.open('/serverIndex');"
" } else {"
" errorMessage.style.display = 'block';"
" }"
"}"
"</script>";
/*
* Server Index Page
*/
const char* serverIndex =
"<script src='https://code.jquery.com/jquery-3.2.1.min.js'></script>"
"<form method='POST' action='update' enctype='multipart/form-data' id='upload_form'>"
"<input type='file' name='update'>"
"<input type='submit' value='Update'>"
"</form>"
"<div id='prg'>progress: 0%</div>"
"<script>"
"$('form').submit(function(e){"
"e.preventDefault();"
"var form = $('#upload_form')[0];"
"var data = new FormData(form);"
" $.ajax({"
"url: '/update',"
"type: 'POST',"
"data: data,"
"contentType: false,"
"processData:false,"
"xhr: function() {"
"var xhr = new window.XMLHttpRequest();"
"xhr.upload.addEventListener('progress', function(evt) {"
"if (evt.lengthComputable) {"
"var per = evt.loaded / evt.total;"
"$('#prg').html('progress: ' + Math.round(per*100) + '%');"
"}"
"}, false);"
"return xhr;"
"},"
"success:function(d, s) {"
"console.log('success!')"
"},"
"error: function (a, b, c) {"
"}"
"});"
"});"
"</script>";
/*
* setup function
*/
void setup(void) {
pinMode(LED, OUTPUT);
Serial.begin(115200);
// Connect to WiFi network
WiFi.begin(ssid, password);
Serial.println("");
// Wait for connection
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("");
Serial.print("Connected to ");
Serial.println(ssid);
Serial.print("IP address: ");
Serial.println(WiFi.localIP());
/*use mdns for host name resolution*/
if (!MDNS.begin(host)) { //http://esp32.local
Serial.println("Error setting up MDNS responder!");
while (1) {
delay(1000);
}
}
Serial.println("mDNS responder started");
/*return index page which is stored in serverIndex */
server.on("/", HTTP_GET, []() {
server.sendHeader("Connection", "close");
server.send(200, "text/html", loginIndex);
});
server.on("/serverIndex", HTTP_GET, []() {
server.sendHeader("Connection", "close");
server.send(200, "text/html", serverIndex);
});
/*handling uploading firmware file */
server.on("/update", HTTP_POST, []() {
server.sendHeader("Connection", "close");
server.send(200, "text/plain", (Update.hasError()) ? "FAIL" : "OK");
ESP.restart();
}, []() {
HTTPUpload& upload = server.upload();
if (upload.status == UPLOAD_FILE_START) {
Serial.printf("Update: %s\n", upload.filename.c_str());
if (!Update.begin(UPDATE_SIZE_UNKNOWN)) { //start with max available size
Update.printError(Serial);
}
} else if (upload.status == UPLOAD_FILE_WRITE) {
/* flashing firmware to ESP*/
if (Update.write(upload.buf, upload.currentSize) != upload.currentSize) {
Update.printError(Serial);
}
} else if (upload.status == UPLOAD_FILE_END) {
if (Update.end(true)) { //true to set the size to the current progress
Serial.printf("Update Success: %u\nRebooting...\n", upload.totalSize);
} else {
Update.printError(Serial);
}
}
});
server.begin();
}
void loop(void) {
server.handleClient();
delay(1);
digitalWrite(LED, HIGH);
delay(500);
digitalWrite(LED, LOW);
delay(500);
}
5. Web端烧录测试
- 由于Arduino IDE编译出来的是
.ino
文件,而WebUpdater需要.bin
文件, 下面我们准备个RGB灯光闪烁的程序。
/*
BlinkRGB
*/
//#define RGB_BRIGHTNESS 64 // Change white brightness (max 255)
// the setup function runs once when you press reset or power the board
void setup() {
// No need to initialize the RGB LED
pinMode(RGB_BUILTIN,OUTPUT);
pinMode(LED_R,OUTPUT);
pinMode(LED_G,OUTPUT);
pinMode(LED_B,OUTPUT);
}
// the loop function runs over and over again forever
void loop() {
#ifdef RGB_BUILTIN
digitalWrite(LED_R,LOW); // Red
digitalWrite(LED_G,HIGH); // Green
digitalWrite(LED_B,HIGH); // Blue
delay(1000);
digitalWrite(LED_R,HIGH); // Red
digitalWrite(LED_G,LOW); // Green
digitalWrite(LED_B,HIGH); // Blue
delay(1000);
digitalWrite(LED_R,HIGH); // Red
digitalWrite(LED_G,HIGH); // Green
digitalWrite(LED_B,LOW); // Blue
delay(1000);
#endif
}
- 点击左上角项目中
导出已编译的二进制文件
,编译完成后再点击下方的显示项目文件夹
。
- 按照图中路径,找到刚刚导出已编译的二进制文件,就是图中第一个
.bin
文件。
这里建议先将找到的.bin
文件另存为在自己想放的地方,保存好路径,以免等下找不到。
再次编译上传完整的WebUpdater示例代码,在上位机(电脑或手机)中打开浏览器,输入IP地址回车,完成登录。
在上传页面中,选择刚刚另存为的.bin
文件,然后点击Update。
- 观察串口监视器和NanoE, 若Web端烧录成功,NanoE板载的RGB灯会亮起。
6. 总结
以上就是通过OTAWebUpdater示例程序实现固件更新,其优缺点如下:
优点:
- 功能强大:提供 Web 界面,用户可以通过浏览器上传固件或更新 SPIFFS(文件系统)。
- 跨平台方便:无需依赖 Arduino IDE,适合需要远程部署和更新的场景。
- 用户友好:具有简单、友好的 Web 界面,适合对直接控制设备的需求。
缺点:
- 实现略复杂:代码比 BasicOTA 略多,需要额外设计网络配置和界面。
- 资源占用较多:Web 界面对内存和网络带宽要求较高,不适合资源非常有限的设备。
小结:
如果项目简单,需要最轻量化的 OTA 功能,选择 BasicOTA。
如果需要更灵活的更新方式,同时带有图形界面和跨平台支持,选择 OTAWebUpdater 更合适。