一、 OTA功能介绍:
OTA(Over The Air)是一项基于短消息机制,通过互联网wln或局域网wifi实现固件动态下载、删除与更新。通过吸纳网上各种版本的文章,整理出几种常用的ESP8266实现OTA升级固件方案:
- Arduino OTA:使用Arduino IDE提供的OTA功能,可以直接通过Arduino IDE上传固件到ESP8266
- WebServer OTA STA模式:ESP8266运行一个简易的Web服务器,您可以在同一个局域网内通过Web页面上传新固件来更新设备。
- WebServer OTA AP模式:ESP8266运行一个简易的Web服务器,您可以通过链接ESP8266发射的AP信号,然后通过访问Web页面上传新固件来更新设备。
- WebServer OTA STA模式+AP模式:ESP8266运行一个简易的Web服务器,您可以同一局域网或链接AP访问通过Web页面上传新固件来更新设备。
- HTTP OTA:ESP8266从指定的HTTP服务器下载固件并自动进行更新。
二、库依赖
ArduinoOTA库
ESP8266WebServer库
ESP8266WiFi库
FS库
WiFiUdp库
NTPClient库
md5库
三、详细介绍:
- Arduino OTA
依赖ArduinoOTA库,检查Arduino IDE是否安装该库文件,如果未安装,可手动安装,或通过文后下载链接下载。
1) 使用该功能前提是,当前固件开启OTA功能,可以参考以下代码,在现有固件中开启OTA功能。
#include <ArduinoOTA.h>
#include <ESP8266WiFi.h>
#include <Ticker.h>
// 闪烁时间间隔(秒)
const int blinkInterval = 2;
const char* ssid = "SSID";
const char* password = "PASSWORD";
Ticker ticker;
void setup() {
Serial.begin(115200);
Serial.println("");
pinMode(LED_BUILTIN, OUTPUT);
ticker.attach(blinkInterval, tickerCount);
WiFi.begin(ssid, password);
Serial.print("Connecting.");
while ( WiFi.status() != WL_CONNECTED ) {
delay(500);
Serial.print(".");
}
Serial.println("connected");
// 配置OTA
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);
});
ArduinoOTA.onEnd([]() {
Serial.println("\nEnd");
});
ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) {
Serial.printf("Progress: %u%%\r", (progress / (total / 100)));
});
ArduinoOTA.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");
}
});
// OTA设置访问密码
ArduinoOTA.setHostname("ESP8266OTA");
ArduinoOTA.setPassword("1234567890");
ArduinoOTA.begin();
Serial.println("ESP8266 OTA Ready");
Serial.print("ESP8266 Version: ");
Serial.println("1.0");
Serial.print("IP address: ");
Serial.println(WiFi.localIP());
}
// 在Tinker对象控制下,此函数将会定时执行。
void tickerCount(){
digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN));
}
void loop() {
ArduinoOTA.handle(); // 处理OTA更新
}
2)运行效果

3)电脑和开发板要处于相同的网络环境下,配置Arduino IDE,点击工具-》端口,选择网络端口ESP8266OTA,当选择这个后,串口就无法实时展示链接信息了,这了可以通过第三方串口链接工具,链接com3端口,


这里假设将版本号改为2.0
#include <ArduinoOTA.h>
#include <ESP8266WiFi.h>
#include <Ticker.h>
// 闪烁时间间隔(秒)
const int blinkInterval = 2;
const char* ssid = "SSID";
const char* password = "PASSWORD";
Ticker ticker;
void setup() {
Serial.begin(115200);
Serial.println("");
pinMode(LED_BUILTIN, OUTPUT);
ticker.attach(blinkInterval, tickerCount);
WiFi.begin(ssid, password);
Serial.print("Connecting.");
while ( WiFi.status() != WL_CONNECTED ) {
delay(500);
Serial.print(".");
}
Serial.println("connected");
// 配置OTA
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);
});
ArduinoOTA.onEnd([]() {
Serial.println("\nEnd");
});
ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) {
Serial.printf("Progress: %u%%\r", (progress / (total / 100)));
});
ArduinoOTA.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");
}
});
// OTA设置访问密码
ArduinoOTA.setHostname("ESP8266OTA");
ArduinoOTA.setPassword("1234567890"); // 密码必须8位以上
ArduinoOTA.begin();
Serial.println("ESP8266 OTA Ready");
Serial.print("ESP8266 Version: ");
Serial.println("2.0"); //修改版本号为2.0
Serial.print("IP address: ");
Serial.println(WiFi.localIP());
}
// 在Tinker对象控制下,此函数将会定时执行。
void tickerCount(){
digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN));
}
void loop() {
ArduinoOTA.handle(); // 处理OTA更新
}
然后点击 --> 上传,按照图上提示输入密码


上传成功后,等待几秒中,可以看到开发板已经开始闪烁。使用第三方工具可以看到版本已经更新为2.0

注意:升级过程中原有固件还在运行,所以要确保升级文件大小不超过原有空间,至少按照2倍大小去估算。
- WebServer OTA STA模式
通过STA模式,实现同一局域网访问ESP8266部署的web服务进行更新固件操作。
2.1. 先通过串口烧制以下固件代码:
代码说明:获取网络时间计算当前日期,通过MD5计算session值,用于会话确认。可上传文件到缓存目录中,也可上传升级文件到空闲区域。
#include <Arduino.h>
#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>
#include <FS.h>
#include <c_types.h>
#include <md5.h>
#include <WiFiUdp.h>
#include <NTPClient.h>
ESP8266WebServer server(8080); //创建tcp server
File fsUploadFile; // 建立文件对象用于闪存文件上传
FSInfo fs_info;
struct {
String username;
String hashedPassword;
} user;
md5_context_t context;
const char* ssid = "SSID"; // 连接WiFi名
// 请将您需要连接的WiFi名填入引号中
const char* password = "PASSWORD"; // 连接WiFi密码
// 请将您需要连接的WiFi密码填入引号中
const char* privateKey = "lyc8780"; // 私钥,替换为你的密钥
const char* formattedMarker = "/.formatted";
String update_file_name = "/update.bin";
uint16 data_len = 16; // 数据长度,以字节为单位
WiFiUDP ntpUDP;
//设置NTPClient
#define NTP_OFFSET 60 * 60 * 8 // 时区偏移量
#define NTP_ADDRESS "ntp1.aliyun.com" // NTP服务器
NTPClient timeClient(ntpUDP, NTP_ADDRESS, NTP_OFFSET);
unsigned char Leap_Year_Judge(unsigned short year) {
//判断该年是不是闰年
if ((year % 400) == 0) {
return 1;
} else if ((year % 100) == 0) {
return 0;
} else if ((year % 4) == 0) {
return 1;
} else {
return 0;
}
}
unsigned char last_day_of_mon(unsigned char month, unsigned short year) {
//判断每月天数并在闰年时给2月+1天
const unsigned char day_per_mon[12] = {
31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }; //每个月的天数
if ((month == 0) || (month > 12)) {
return day_per_mon[1] + Leap_Year_Judge(year);
}
if (month != 2) {
return day_per_mon[month - 1]; //非2月直接返回对应月份天数
} else {
return day_per_mon[1] + Leap_Year_Judge(year); //2月则判断该年是不是闰年
}
}
//回复状态码 200 给客户端
void respondOK() {
server.send(200);
}
String createSession() {
String sessionId = "";
int year, year_tmp, month_tmp, day_tmp;
unsigned char month, day;
// 获取当前时间戳
timeClient.update();
unsigned long currentTime = timeClient.getEpochTime(); // 从 NTP 服务器获取当前时间
if (currentTime < 1000000) {
// 检查时间是否有效
Serial.println("Failed to get current time");
return sessionId;
}
int days = currentTime / 86400L; //算出1970-1-1至今天数
for (year_tmp = 1970; days > 0; year_tmp++) {
//从1970开始减每年天数同时年份+1
day_tmp = (365 + Leap_Year_Judge(year_tmp)); //这一年有多少天
if (days >= day_tmp) {
//条件成立,则year_tmp即是这个时间戳值所代表的年数。days剩下的数即为今年过了多少天。
days -= day_tmp;
} else {
break;
}
}
year = year_tmp - 2000; //减去2000是因为时钟芯片仅接受2位数年份
for (month_tmp = 1; month_tmp < 12; month_tmp++) {
//计算今年过了几个月,方法同上
day_tmp = last_day_of_mon(month_tmp, year); //获取每个月的天数
if (days >= day_tmp) {
//条件成立,则month_tmp即是这个时间戳值所代表的月数。days剩下的数即为这个月过了多少天,即日。
days -= day_tmp;
} else {
break;
}
}
month = month_tmp;
day = days + 1;
char timestamp[20];
sprintf(timestamp, "%04d-%02d-%02d", year, month, day);
// 结合私钥和时间戳生成 sessionid
String input = String(timestamp) + String(privateKey);
uint8_t inputBytes[strlen(input.c_str()) + 1];
strcpy((char*)inputBytes, input.c_str());
MD5Init(&context);
MD5Update(&context, inputBytes, strlen(input.c_str()));
byte digest[16];
MD5Final(digest, &context);
// 将哈希值转换为十六进制字符串
char hexDigest[33];
for (int i = 0; i < 16; ++i) {
sprintf(hexDigest + (i * 2), "%02x", digest[i]);
}
hexDigest[32] = 0;
sessionId = String(hexDigest);
return sessionId;
}
//设置需要收集的请求头信息
const char* headerKeys[] = {
"cookie" };
bool validateSession(String sessionID) {
return true;
}
String getCookie() {
// 遍历所有HTTP头部
int headers = server.headers();
for (int i = 0; i < headers; i++) {
String headerName = server.headerName(i);
if (headerName == "cookie") {
return server.header(i);
}
}
return "";
}
bool isLoggedIn() {
String cookie = getCookie();
if (cookie != "") {
// 假设sessionID的有效格式为"sessionID=1234567890"
if (cookie.indexOf("sessionID=") != -1) {
// 提取sessionID的值
size_t sessionIDStart = cookie.indexOf("sessionID=") + 10;
size_t sessionIDEnd = cookie.indexOf(";", sessionIDStart);
String extractedSessionID = cookie.substring(sessionIDStart, sessionIDEnd);
String extractedSessionID_n = createSession();
if (extractedSessionID == extractedSessionID_n) {
return true;
}
}
}
return false;
}
void handleLogin() {
String html = "<!DOCTYPE html><html><head><title>Login Page</title></head><body>";
html += "<h1>Login Required</h1>";
html += "<form method='POST' action='/login'>";
html += "<label>Username: <input type='text' name='username'></label><br>";
html += "<label>Password: <input type='password' name='password'></label><br>";
html += "<button type='submit'>Login</button>";
html += "</form></body></html>";
server.send(200, "text/html", html);
}
void handleLoginPost() {
if (server.method() == HTTP_POST) {
String username = server.arg("username");
String password = server.arg("password");
if (username == user.username && password == user.hashedPassword) {
String sessionID = createSession();
server.sendHeader("Set-Cookie", "sessionID=" + sessionID + "; Path=/");
server.sendHeader("Location", "/");
server.sendHeader("Connection", "close");
// 发送302重定向响应
server.send(302, "text/html", "<html><head><meta http-equiv='refresh' content='0; url=/'></head></body></html>");
delay(500);
Serial.println("User logged in. Session ID: " + sessionID);
} else {
server.send(401, "text/plain", "Invalid username or password");
Serial.println("Login failed: Invalid credentials");
}
} else {
server.send(405, "text/plain", "Method Not Allowed");
Serial.println("Invalid HTTP method");
}
}
void handleRoot() {
if (!isLoggedIn()) {
// server.send(301, "text/plain", "Redirecting to login page");
server.sendHeader("Location", "/login");
server.send(302, "text/html", "<html><head><meta http-equiv='refresh' content='0; url=/login'></head></body></html>");
return;
}
SPIFFS.info(fs_info);
String html = "<!DOCTYPE html><html><head><title>Home Version 1.0</title></head><body>";
html += "<h1>Welcome, " + user.username + "</h1>";
html += "<h4>Used bytes: " + String(fs_info.usedBytes) + "bytes Total bytes: " + String(fs_info.totalBytes) + "bytes</h4>";
html += "<a href='/upload'>Upload Files</a><br>";
html += "<a href='/ota'>OTA Update</a><br>";
html += "<a href='/lists'>File Brower</a>";
html += "</body></html>";
server.send(200, "text/html", html);
}
void handleUpload() {
if (!isLoggedIn()) {
// server.send(301, "text/plain", "Redirecting to login page");
server.sendHeader("Location", "/login");
server.send(302, "text/html", "<html><head><meta http-equiv='refresh' content='0; url=/login'></head></body></html>");
return;
}
String html = "<!DOCTYPE html><html><head><title>Upload Files</title></head><body>";
html += "<h1>Upload Files</h1>";
html += "<button onclick=\"window.location.href='/'\">home</button>";
html += "<form method='POST' enctype='multipart/form-data'>";
html += "<input type='file' name='file'>";
html += "<input type='submit' value = 'Upload'>";
html += "</form>";
html += "<div id='status'></div>";
html += "<script>";
html += "document.querySelector('form').addEventListener('submit', function(e) {";
html += " e.preventDefault();";
html += " var formData = new FormData(this);";
html += " var xhttp = new XMLHttpRequest();";
html += " xhttp.onreadystatechange = function() {";
html += " document.getElementById('status').innerHTML = this.responseText;";
html += " };";
html += " xhttp.open('POST', '/upload', true);";
html += " xhttp.send(formData);";
html += "});";
html += "</script>";
html += "</body></html>";
server.send(200, "text/html", html);
}
void handleOTAUpload() {
if (!isLoggedIn()) {
// server.send(301, "text/plain", "Redirecting to login page");
server.sendHeader("Location", "/login");
server.send(302, "text/html", "<html><head><meta http-equiv='refresh' content='0; url=/login'></head></body></html>");
return;
}
String html = "<!DOCTYPE html><html><head><title>OTA Update</title></head><body>";
html += "<h1>OTA Update</h1>";
html += "<button onclick=\"window.location.href='/'\">home</button>";
html += "<form method='POST' enctype='multipart/form-data'>";
html += "<input type='file' name='file' accept='.bin'>";
html += "<input type='submit' value='Upload and Update'>";
html += "</form>";
html += "<div id='progress-container'>";
html += "<div id='progress-bar'></div>";
html += "<div id='progress-text'>0%</div>";
html += "</div>";
html += "<div id='status'></div>";
html += "<style>";
html += " #progress-container {";
html += " width: 300px;";
html += " height: 20px;";
html += " border: 1px solid #ccc;";
html += " border-radius: 10px;";
html += " margin: 10px 0;";
html += " }";
html += " #progress-bar {";
html += " width: 0%;";
html += " height: 100%;";
html += " background-color: #4CAF50;";
html += " border-radius: 10px;";
html += " transition: width 0.3s ease-in-out;";
html += " }";
html += " #progress-text {";
html += " position: relative;";
html += " top: -10px;";
html += " left: 50%;";
html += " transform: translateX(-50%);";
html += " color: #333;";
html += " }";
html += "<style>";
html += "<script>";
html += "document.querySelector('form').addEventListener('submit', function(e) {";
html += " e.preventDefault();";
html += " var formData = new FormData(this);";
html += " var xhttp = new XMLHttpRequest();";
html += " xhttp.upload.addEventListener('progress', function(e) {";
html += " if (e.lengthComputable) {";
html += " const progress = (e.loaded / e.total) * 100;";
html += " document.getElementById('progress-bar').style.width = progress.toFixed(2) + '%'";
html += " document.getElementById('progress-text').textContent = progress.toFixed(2) + '%'";
html += " }";
html += " });";
html += " xhttp.onreadystatechange = function() {";
html += " document.getElementById('status').innerHTML = this.responseText;";
html += " };";
html += " xhttp.open('POST', '/ota', true);";
html


4437

被折叠的 条评论
为什么被折叠?



