1. 前言

前面的博文中, 我们编写的固件都是通过 ArduinoIDE 往串口线上的 ESP8266 模块去烧写固件. 这样就会有几个弊端:

需要经常插拔转接线, 很容易造成 8266 串口丢失;

如果是将 ESP8266 做成产品并交付到客户手上之后应该如何更新产品中的 ESP8266 固件呢? 难道要用户拿到技术中心来更新? 如果是这样, 这个产品必定属于失败产品.

在这里, 就引入我们本篇章需要了解的实用知识 -- OTA 功能.

OTA -- Over the air update of the firmware, 也就是无线固件更新, 这个可以说是非常炫酷且实用的功能.

那么 OTA 的本质是什么? 它又是如何工作的呢?

一般情况下, 当我们使用串口线更新 8266 的固件是通过 SerialBootLoader 来更新, 这个属于开发板内置好的默认方式.

而 OTA 因为用到的是 Wi-Fi 网络, 所以我们假设也有一个名为 "WIFIOTABootLoader" 的东西来处理固件的无线写入更新, 但是这个 WIFIOTABootLoader 需要我们先通过串口线预先写入到 ESP8266. 换句话说就是, 我们得在项目代码中嵌入用于 OTA 的 WIFIOTABootLoader.

那么问题来了, WIFIOTABootLoader 到底是什么原理呢?

万变不离其宗, 博主第一个想到的就是 WebServer,UDP,mDNS 的混合使用, 通过 mDNS 可以解决域名访问问题, WebServer 提供 Web 页面供开发者上传固件文件, 然后 WebServer 处理具体的请求, 再把文件写入 flash 中(万幸的是, 博主去看底层代码, 确实有这样设计的思路).

所以, 要想深入理解 OTA, 请先回顾基础知识:

ESP8266 开发之旅 网络篇10 UDP 服务

ESP8266 开发之旅 网络篇 WebServer--ESP8266WebServer 库的使用

ESP8266 开发之旅 网络篇 域名服务 --ESP8266mDNS 库

2. OTA 方式

在 Arduino Core For ESP8266 中, 使用 OTA 功能可以有三种方式:

ArduinoOTA -- OTA 之 Arduino IDE 更新, 也就是无线更新需要利用到 Arduino IDE, 只是不需要通过串口线烧写而已, 这种方式适合开发者;

WebUpdateOTA -- OTA 之 Web 更新, 通过 8266 上配置的 webserver 来选择固件更新, 这种方式适合开发者以及有简单配置经验的消费者;

ServerUpdateOTA -- OTA 之服务器更新, 通过公网服务器, 把固件放在云端服务器上, 下载更新, 这种方式适合零基础的消费者, 无感知更新;

其实不管哪一种方式, 其最终目的:

为了把新固件烧写到 Flash 中, 然后替代掉旧固件, 以达到更新固件的效果.

那么, 我们来看看最终新旧固件是怎么用替换, 请看下图:

没有更新固件前, 当前固件 (current sketch, 绿色部分) 和 文件系统 (spiffs, 蓝色部分) 位于 flash 存储空间的不同区域, 中间空白的是空闲空间;

固件更新时, 新固件 (new sketch, 黄色所示) 将写入空闲空间, 此时 flash 同时存在这三个对象;

重启模块后, 新固件会覆盖掉旧固件, 然后从当前固件的开始地址处开始运行, 以达到固件更新的目的.

接下来, 我们看看这三种方式是怎么用实现了以上三个步骤.

3. ArduinoOTA -- OTA 之 Arduino IDE 更新

为了更好地使用 ArduinoOTA, 先来了解一下 ArduinoOTA 需要用到的库, 然后再具体分析里面的实现原理. 请在代码里面引入以下库:

#include <ArduinoOTA.h>

查看 ArduinoOTA 底层源码, 可以发现引入 UdpContext,ESP8266mDNS,WiFiClient(同时关联 WiFiServer), 也就是说用到了 UDP 服务, TCP 服务以及 mDNS 域名映射, 这个是一个关键点.

在这里, 博主也总结了 ArduinoOTA 库的百度脑图:

总体上, 方法可以细分为 3 大类:

安全策略配置

管理 OTA

固件更新相关

3.1 安全策略配置

一般来说, 使用默认的安全策略配置就好, 但是如果有特殊要求, 也可以自行配置.

3.1.1 setHostname -- 设置主机名

函数说明:

/**
* 设置主机名, 主要用于 mDNS 的域名映射
* @param hostName 主机名
*/
void setHostname(const char *hostname);

注意点:

默认主机名是 esp8266-xxxxx

3.1.2 getHostname -- 获取主机名

函数说明:

/**
* 获取主机名
* @return String 主机名
*/
String getHostname();

3.1.3 setPassword -- 设置访问密码

函数说明:

/**
* 设置访问密码
* @param password 上传密码, 默认为 NULL
*/
void setPassword(const char *password);

源码说明:

void ArduinoOTAClass::setPassword(const char * password) {
if (!_initialized && !_password.length() && password) {
//MD5 编码 建议用这个方法更好
MD5Builder passmd5;
passmd5.begin();
passmd5.add(password);
passmd5.calculate();
_password = passmd5.toString();
}
}

3.1.4 setPasswordHash -- 设置访问密码哈希值

函数说明:

/**
* 设置访问密码哈希值
* @param password 上传密码 Hash 值 MD5(password)
*/
void setPasswordHash(const char *password);

源码说明:

void ArduinoOTAClass::setPasswordHash(const char * password) {
if (!_initialized && !_password.length() && password) {
//md5 编码的 password
_password = password;
}
}

3.1.5 setPort -- 设置 Udp 服务端口

函数说明:

/**
* 设置 Udp 服务端口
* @param port Udp 服务端口
*/
void setPort(uint16_t port);

注意点:

以上代码请在 begin 方法之前调用;

3.2 管理 OTA

3.2.1 begin -- 启动 ArduinoOTA 服务

函数说明:

/**
* 启动 ArduinoOTA 服务
*/
void begin();

源码说明:

void ArduinoOTAClass::begin() {
if (_initialized)
return;
// 配置主机名, 默认 esp8266-xxxx
if (!_hostname.length()) {
char tmp[15];
sprintf(tmp, "esp8266-%06x", ESP.getChipId());
_hostname = tmp;
}
//udp 服务端口号, 默认 8266
if (!_port) {
_port = 8266;
}
if(_udp_ota){
_udp_ota->unref();
_udp_ota = 0;
}
// 启动 UDP 服务
_udp_ota = new UdpContext;
_udp_ota->ref();
if(!_udp_ota->listen(*IP_ADDR_ANY, _port))
return;
// 绑定了回调函数
_udp_ota->onRx(std::bind(&ArduinoOTAClass::_onRx, this));
// 启动 mDNS 服务
MDNS.begin(_hostname.c_str());
if (_password.length()) {
MDNS.enableArduino(_port, true);
} else {
//mDNS 注册 OTA 服务
MDNS.enableArduino(_port);
}
_initialized = true;
_state = OTA_IDLE;
#ifdef OTA_DEBUG
OTA_DEBUG.printf("OTA server at: %s.local:%u\n", _hostname.c_str(), _port);
#endif
}
/**
* 解析收到的 OTA 请求
*/
void ArduinoOTAClass::_onRx(){
if(!_udp_ota->next()) return;
ip_addr_t ota_ip;
if (_state == OTA_IDLE) {
// 查看当前 OTA 命令 可以烧写固件或者烧写 SPIFFS
int cmd = parseInt();
if (cmd != U_FLASH && cmd != U_SPIFFS)
return;
_ota_ip = _udp_ota->getRemoteAddress();
_cmd = cmd;
_ota_port = parseInt();
_ota_udp_port = _udp_ota->getRemotePort();
_size = parseInt();
_udp_ota->read();
_md5 = readStringUntil('\n');
_md5.trim();
if(_md5.length() != 32)
return;
ota_ip.addr = (uint32_t)_ota_ip;
// 验证密码, 需要 IDE 输入密码
if (_password.length()){
MD5Builder nonce_md5;
nonce_md5.begin();
nonce_md5.add(String(micros()));
nonce_md5.calculate();
_nonce = nonce_md5.toString();
char auth_req[38];
sprintf(auth_req, "AUTH %s", _nonce.c_str());
_udp_ota->append((const char *)auth_req, strlen(auth_req));
_udp_ota->send(&ota_ip, _ota_udp_port);
// 切换到验证状态
_state = OTA_WAITAUTH;
return;
} else {
// 切换到更新固件状态
_state = OTA_RUNUPDATE;
}
} else if (_state == OTA_WAITAUTH) {
int cmd = parseInt();
if (cmd != U_AUTH) {
_state = OTA_IDLE;
return;
}
_udp_ota->read();
String cnonce = readStringUntil(' ');
String response = readStringUntil('\n');
if (cnonce.length() != 32 || response.length() != 32) {
_state = OTA_IDLE;
return;
}
String challenge = _password + ":" + String(_nonce) + ":" + cnonce;
MD5Builder _challengemd5;
_challengemd5.begin();
_challengemd5.add(challenge);
_challengemd5.calculate();
String result = _challengemd5.toString();
ota_ip.addr = (uint32_t)_ota_ip;
if(result.equalsConstantTime(response)) {
// 验证通过 切换到更新固件状态 等待固件接收
_state = OTA_RUNUPDATE;
} else {
_udp_ota->append("Authentication Failed", 21);
_udp_ota->send(&ota_ip, _ota_udp_port);
if (_error_callback) _error_callback(OTA_AUTH_ERROR);
_state = OTA_IDLE;
}
}
while(_udp_ota->next()) _udp_ota->flush();
}

可以看出, begin 方法主要是根据配置内容, 启动 mDNS 服务, 默认域名是 esp8266-xxxx, 启动 UDP 服务, 默认端口是 8266, 这个是后面 ArduinoIDE 无线传输固件的根本.

3.2.2 handle -- 处理固件更新

函数说明:

/**
* 处理固件更新, 这个方法需要在 loop 方法中不断检测调用
*/
void handle();

源码说明:

void ArduinoOTAClass::handle() {
if (_state == OTA_RUNUPDATE) {
// 处理固件传输更新
_runUpdate();
_state = OTA_IDLE;
}
}
/**
* 处理固件传输更新
*/
void ArduinoOTAClass::_runUpdate() {
ip_addr_t ota_ip;
ota_ip.addr = (uint32_t)_ota_ip;
// 查看 Update 是否启动成功, Update 类主要用于跟 flash 打交道, 用于更新固件或者 SPIFFS, 下面博主会说明一下
if (!Update.begin(_size, _cmd)) {
#ifdef OTA_DEBUG
OTA_DEBUG.println("Update Begin Error");
#endif
if (_error_callback) {
_error_callback(OTA_BEGIN_ERROR);
}
StreamString ss;
Update.printError(ss);
_udp_ota->append("ERR:", 5);
_udp_ota->append(ss.c_str(), ss.length());
_udp_ota->send(&ota_ip, _ota_udp_port);
delay(100);
_udp_ota->listen(*IP_ADDR_ANY, _port);
_state = OTA_IDLE;
return;
}
_udp_ota->append("OK", 2);
_udp_ota->send(&ota_ip, _ota_udp_port);
delay(100);
Update.setMD5(_md5.c_str());
// 停止 UDP 服务
WiFiUDP::stopAll();
WiFiClient::stopAll();
// 执行 OTA 开始回调
if (_start_callback) {
_start_callback();
}
if (_progress_callback) {
_progress_callback(0, _size);
}
// 连接到 IDE 建立的服务地址
WiFiClient client;
if (!client.connect(_ota_ip, _ota_port)) {
#ifdef OTA_DEBUG
OTA_DEBUG.printf("Connect Failed\n");
#endif
_udp_ota->listen(*IP_ADDR_ANY, _port);
if (_error_callback) {
_error_callback(OTA_CONNECT_ERROR);
}
_state = OTA_IDLE;
}
uint32_t written, total = 0;
while (!Update.isFinished() && client.connected()) {
int waited = 1000;
// 接收固件内容
while (!client.available() && waited--)
delay(1);
if (!waited){
#ifdef OTA_DEBUG
OTA_DEBUG.printf("Receive Failed\n");
#endif
_udp_ota->listen(*IP_ADDR_ANY, _port);
if (_error_callback) {
_error_callback(OTA_RECEIVE_ERROR);
}
_state = OTA_IDLE;
}
// 把固件内容写入 flash
written = Update.write(client);
if (written> 0) {
client.print(written, DEC);
total += written;
// 回调调用进度
if(_progress_callback) {
_progress_callback(total, _size);
}
}
}
// 更新结束
if (Update.end()) {
// 回调接收成功
client.print("OK");
client.stop();
delay(10);
#ifdef OTA_DEBUG
OTA_DEBUG.printf("Update Success\n");
#endif
//OTA 结束回调
if (_end_callback) {
_end_callback();
}
// 自动重启
if(_rebootOnSuccess){
#ifdef OTA_DEBUG
OTA_DEBUG.printf("Rebooting...\n");
#endif
//let serial/network finish tasks that might be given in _end_callback
delay(100);
// 重启命令
ESP.restart();
}
} else {
_udp_ota->listen(*IP_ADDR_ANY, _port);
if (_error_callback) {
_error_callback(OTA_END_ERROR);
}
Update.printError(client);
#ifdef OTA_DEBUG
Update.printError(OTA_DEBUG);
#endif
_state = OTA_IDLE;
}
}

接下来, 看看 Update 类, 这是一个写 Flash 存储空间的重要类, 重点看几个方法:

Update.begin 源码说明

bool UpdaterClass::begin(size_t size, int command) {
....... // 省略前面细节
if (command == U_FLASH) {
// 以下代码就是确认烧写位置, 烧写位置在我们文章开头说到的空闲空间, 处于当前程序区和 SPIFFS 之间
//size of current sketch rounded to a sector
uint32_t currentSketchSize = (ESP.getSketchSize() + FLASH_SECTOR_SIZE - 1) & (~(FLASH_SECTOR_SIZE - 1));
//address of the end of the space available for sketch and update
//_SPIFFS_start SPIFFS 开始地址
uint32_t updateEndAddress = (uint32_t)&_SPIFFS_start - 0x40200000;
//size of the update rounded to a sector
uint32_t roundedSize = (size + FLASH_SECTOR_SIZE - 1) & (~(FLASH_SECTOR_SIZE - 1));
//address where we will start writing the update
updateStartAddress = (updateEndAddress> roundedSize)? (updateEndAddress - roundedSize) : 0;
.....// 省略细节
}
else if (command == U_SPIFFS) {
// 如果是烧写 SPIFFS
updateStartAddress = (uint32_t)&_SPIFFS_start - 0x40200000;
}
else {
// 不支持其他命令
// unknown command
#ifdef DEBUG_UPDATER
DEBUG_UPDATER.println(F("[begin] Unknown update command."));
#endif
return false;
}
//initialize 记录更新位置
_startAddress = updateStartAddress;
_currentAddress = _startAddress;
....... 省略细节}

Update.end 源码说明

bool UpdaterClass::end(bool evenIfRemaining){
..... // 省略前面细节
if (_command == U_FLASH) {
// 设置重启后 copy 新固件覆盖旧固件
eboot_command ebcmd;
ebcmd.action = ACTION_COPY_RAW;
ebcmd.args[0] = _startAddress;
ebcmd.args[1] = 0x00000;
ebcmd.args[2] = _size;
eboot_command_write(&ebcmd);
#ifdef DEBUG_UPDATER
DEBUG_UPDATER.printf("Staged: address:0x%08X, size:0x%08X\n", _startAddress, _size);
}
else if (_command == U_SPIFFS) {
DEBUG_UPDATER.printf("SPIFFS: address:0x%08X, size:0x%08X\n", _startAddress, _size);
#endif
}
_reset();
return true;
}

3.2.3 setRebootOnSuccess -- 设置固件更新完毕是否自动重启

函数说明:

/**
* 设置固件更新完毕是否自动重启
* @param reboot 是否自动重启, 默认 true
*/
void setRebootOnSuccess(bool reboot);

注意点:

这个函数可以设置成 true, 让 8266 可以自动重启;

3.3 固件更新相关

3.3.1 onStart -- OTA 开始连接回调

函数说明:

/**
* 回调函数定义
*/
typedef std::function<void(void)> THandlerFunction;
/**
* OTA 开始连接回调 fn
* @param fn 回调函数
*/
void onStart(THandlerFunction fn);
3.3.2 onEnd -- OTA 结束回调

函数说明:

/**
* 回调函数定义
*/
typedef std::function<void(void)> THandlerFunction;
/**
* OTA 结束回调 fn
* @param fn 回调函数
*/
void onEnd(THandlerFunction fn);

3.3.3 onError -- OTA 出错回调

函数说明:

/**
* 回调函数定义
* @param ota_error_t 错误原因
*/
typedef std::function<void(ota_error_t)> THandlerFunction_Error;
/**
* OTA 出错回调 fn
* @param fn 回调函数
*/
void onError(THandlerFunction_Error fn);

错误原因定义如下:

typedef enum {
OTA_AUTH_ERROR,// 验证失败
OTA_BEGIN_ERROR,//update 开启失败
OTA_CONNECT_ERROR,// 网络连接失败
OTA_RECEIVE_ERROR,// 接收固件失败
OTA_END_ERROR// 结束失败
} ota_error_t;

3.3.4 onProgress -- OTA 接收固件进度

函数说明:

/**
* 回调函数定义
* @param 固件当前数据大小
* @param 固件总大小
*/
typedef std::function<void(unsigned int, unsigned int)> THandlerFunction_Progress;
/**
* OTA 接收固件进度 回调 fn
* @param fn 回调函数
*/
void onProgress(THandlerFunction_Progress fn);

3.4 实例

实验说明:

OTA 之 Arduino IDE 更新, 需要利用到 ArduinoOTA 库. 也就意味着我们需要首先往 8266 烧写支持 ArduinoOTA 的代码, 然后 ArduinoIDE 会通过 UDP 通信连接到 8266 建立的 UDP 服务, 通过 UDP 服务校验相应信息, 校验通过后 8266 连接 ArduinoIDE 建立的 Http 服务, 传输新固件.

注意:

ArduinoOTA 需要 Python 环境支持, 需要读者先安装.

实验准备:

NodeMcu 开发板

Python 2.7(不安装不支持的 Python 3.5,Windows 用户应选择 "将 python.exe 添加到路径"(见下文 - 默认情况下未选择此选项))python 2.7 https://pan/s/1eLJtHlDmF9PKpkPgEqTtNg 提取码: g9ds

实验步骤:

演示更新功能, 需要区分新旧代码. 先往 NodeMcu 烧写 V1.0 版本代码:

/**
* 功能描述: OTA 之 Arduino IDE 更新 V1.0 版本代码
*
*/
#include <ESP8266WiFi.h>
#include <ESP8266mDNS.h>
#include <WiFiUdp.h>
#include <ArduinoOTA.h>
// 调试定义
#define DebugBegin(baud_rate) Serial.begin(baud_rate)
#define DebugPrintln(message) Serial.println(message)
#define DebugPrint(message) Serial.print(message)
#define DebugPrintF(...) Serial.printf( __VA_ARGS__ )
#define CodeVersion "CodeVersion V1.0"
const char* ssid = "xxxx";// 填上 Wi-Fi 账号
const char* password = "xxxxx";// 填上 Wi-Fi 密码
void setup() {
DebugBegin(115200);
DebugPrintln("Booting Sketch....");
DebugPrintln(CodeVersion);
Wi-Fi.mode(WIFI_STA);
Wi-Fi.begin(ssid, password);
while (Wi-Fi.waitForConnectResult() != WL_CONNECTED) {
DebugPrintln("Connection Failed! Rebooting...");
delay(5000);
// 重启 ESP8266 模块
ESP.restart();
}
// Port defaults to 8266
// ArduinoOTA.setPort(8266);
// Hostname defaults to esp8266-[ChipID]
// ArduinoOTA.setHostname("myesp8266");
// No authentication by default
// ArduinoOTA.setPassword("admin");
// Password can be set with it's md5 value as well
// MD5(admin) = 21232f297a57a5a743894a0e4a801fc3
// ArduinoOTA.setPasswordHash("21232f297a57a5a743894a0e4a801fc3");
ArduinoOTA.onStart([]() {
String type;
// 判断一下 OTA 内容
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()
DebugPrintln("Start updating" + type);
});
ArduinoOTA.onEnd([]() {
DebugPrintln("\nEnd");
});
ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) {
DebugPrintF("Progress: %u%%\r", (progress / (total / 100)));
});
ArduinoOTA.onError([](ota_error_t error) {
DebugPrintF("Error[%u]:", error);
if (error == OTA_AUTH_ERROR) {
DebugPrintln("Auth Failed");
} else if (error == OTA_BEGIN_ERROR) {
DebugPrintln("Begin Failed");
} else if (error == OTA_CONNECT_ERROR) {
DebugPrintln("Connect Failed");
} else if (error == OTA_RECEIVE_ERROR) {
DebugPrintln("Receive Failed");
} else if (error == OTA_END_ERROR) {
DebugPrintln("End Failed");
}
});
ArduinoOTA.begin();
DebugPrintln("Ready");
DebugPrint("IP address:");
DebugPrintln(Wi-Fi.localIP());
}
void loop() {
ArduinoOTA.handle();
}

烧写成功后, 打开串口监视器会看到下图内容:

注意: 烧写成功后, 关闭 ArduinoIDE 然后重新打开(目的是为了和 ESP8266 建立无线通信).

然后在工具菜单的端口项中你会发现多了一个 "esp8266-xxxxx" 的菜单项, 选中它.

接下来, 请往 NodeMcu 烧写 V1.1 版本代码(跟上面代码一样, 就是改变了版本号):

/**
* 功能描述: OTA 之 Arduino IDE 更新 V1.1 版本代码
*
*/
#include <ESP8266WiFi.h>
#include <ESP8266mDNS.h>
#include <WiFiUdp.h>
#include <ArduinoOTA.h>
// 调试定义
#define DebugBegin(baud_rate) Serial.begin(baud_rate)
#define DebugPrintln(message) Serial.println(message)
#define DebugPrint(message) Serial.print(message)
#define DebugPrintF(...) Serial.printf( __VA_ARGS__ )
#define CodeVersion "CodeVersion V1.1"
const char* ssid = "xxxx";// 填上 Wi-Fi 账号
const char* password = "xxxx";// 填上 Wi-Fi 密码
void setup() {
DebugBegin(115200);
DebugPrintln("Booting Sketch....");
DebugPrintln(CodeVersion);
Wi-Fi.mode(WIFI_STA);
Wi-Fi.begin(ssid, password);
while (Wi-Fi.waitForConnectResult() != WL_CONNECTED) {
DebugPrintln("Connection Failed! Rebooting...");
delay(5000);
// 重启 ESP8266 模块
ESP.restart();
}
// Port defaults to 8266
// ArduinoOTA.setPort(8266);
// Hostname defaults to esp8266-[ChipID]
// ArduinoOTA.setHostname("myesp8266");
// No authentication by default
// ArduinoOTA.setPassword("admin");
// Password can be set with it's md5 value as well
// MD5(admin) = 21232f297a57a5a743894a0e4a801fc3
// ArduinoOTA.setPasswordHash("21232f297a57a5a743894a0e4a801fc3");
ArduinoOTA.onStart([]() {
String type;
// 判断一下 OTA 内容
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()
DebugPrintln("Start updating" + type);
});
ArduinoOTA.onEnd([]() {
DebugPrintln("\nEnd");
});
ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) {
DebugPrintF("Progress: %u%%\r", (progress / (total / 100)));
});
ArduinoOTA.onError([](ota_error_t error) {
DebugPrintF("Error[%u]:", error);
if (error == OTA_AUTH_ERROR) {
DebugPrintln("Auth Failed");
} else if (error == OTA_BEGIN_ERROR) {
DebugPrintln("Begin Failed");
} else if (error == OTA_CONNECT_ERROR) {
DebugPrintln("Connect Failed");
} else if (error == OTA_RECEIVE_ERROR) {
DebugPrintln("Receive Failed");
} else if (error == OTA_END_ERROR) {
DebugPrintln("End Failed");
}
});
ArduinoOTA.begin();
DebugPrintln("Ready");
DebugPrint("IP address:");
DebugPrintln(Wi-Fi.localIP());
}
void loop() {
ArduinoOTA.handle();
}

编译点击上传, 会出现以下页面:

更新完毕, 重启 8266

实验总结:

OTA 之 Arduino IDE 更新实现逻辑非常简单, 主要包括几方面:

连接 Wi-Fi

配置 ArduinoOTA 对象的事件函数

启动 ArduinoOTA 服务 ArduinoOTA.begin()

在 loop() 函数将处理权交由 ArduinoOTA.handle()

为了区分正常工作模式以及更新模式, 我们可以设置个标志位来区分(标志位通过其他手段修改, 比如按钮, 软件控制).

void loop() {
if (flag ==0 ) {
// 正常工作状态的代码
} else {
ArduinoOTA.handle();
}
}

4. WebUpdateOTA -- OTA 之 Web 更新

OTA 之 Web 更新, 通过 8266 上配置的 webserver 来选择固件更新, 这种方式适合开发者以及有简单配置经验的消费者, 其操作过程如下:

用 ESP8266 先建立一个 Web 服务器然后提供一个 Web 更新界面, 需要使用到库 ESP8266HTTPUpdateServer;

通过 Arduino 将源文件编译为 *.bin 的二进制文件;

通过 mDNS 功能在浏览器中访问 ESP8266 的服务器页面, 默认服务地址为: http://esp8266.local/update;

通过 Web 界面将本地编译好的 *.bin 二进制固件文件上传到 ESP8266 中;

上传完成编译文件后 ESP8266 将固件写入 Flash 中

OTA 之 Web 更新, 请加上以下头文件:

#include <ESP8266WebServer.h>
#include <ESP8266mDNS.h>
#include <ESP8266HTTPUpdateServer.h>

接下来, 先上一个博主总结的百度脑图:

方法只有两个, 非常简单.

4.1 updateCredentials -- 验证用户信息

函数说明:

/**
* 校验用户信息
* @param username 用户名称
* @param password 用户密码
*/
void updateCredentials(const char * username, const char * password)

4.2 setup -- 配置 WebOTA

函数说明:

/**
* 配置 WebOTA
* @param ESP8266WebServer 需要绑定的 webserver
*/
void setup(ESP8266WebServer *server){
setup(server, NULL, NULL);
}
/**
* 配置 WebOTA
* @param ESP8266WebServer 需要绑定的 webserver
* @param path 注册 uri
*/
void setup(ESP8266WebServer *server, const char * path){
setup(server, path, NULL, NULL);
}
/**
* 配置 WebOTA
* @param ESP8266WebServer 需要绑定的 webserver
* @param username 用户名称
* @param password 用户密码
*/
void setup(ESP8266WebServer *server, const char * username, const char * password){
setup(server, "/update", username, password);
}
/**
* 配置 WebOTA
* @param ESP8266WebServer 需要绑定的 webserver
* @param username 用户名称
* @param password 用户密码
* @param path 注册 uri (默认是 "/update")
*/
void setup(ESP8266WebServer *server, const char * path, const char * username, const char * password);

来分析一下 setup 源码:

/**
* 配置 WebOTA
* @param ESP8266WebServer 需要绑定的 webserver
* @param username 用户名称
* @param password 用户密码
* @param path 注册 uri (默认是 "/update")
*/
void ESP8266HTTPUpdateServer::setup(ESP8266WebServer *server, const char * path, const char * username, const char * password)
{
_server = server;
_username = (char *)username;
_password = (char *)password;
// 注册 webserver 的响应回调函数
_server->on(path, HTTP_GET, [&](){
// 校验用户信息 通过就发送更新页面
if(_username != NULL && _password != NULL && !_server->authenticate(_username, _password))
return _server->requestAuthentication();
_server->send_P(200, PSTR("text/html"), serverIndex);
});
// 注册 webserver 的响应回调函数 处理文件上传 文件结束
_server->on(path, HTTP_POST, [&](){
// 文件上传完毕回调
if(!_authenticated)
return _server->requestAuthentication();
if (Update.hasError()) {
_server->send(200, F("text/html"), String(F("Update error:")) + _updaterError);
} else {
_server->client().setNoDelay(true);
_server->send_P(200, PSTR("text/html"), successResponse);
delay(100);
// 断开 http 连接
_server->client().stop();
// 重启 ESP8266
ESP.restart();
}
},[&](){
// 通过 Update 对象处理文件上传, 关于 update 对象请看上面的讲解.
HTTPUpload& upload = _server->upload();
// 固件上传开始
if(upload.status == UPLOAD_FILE_START){
_updaterError = String();
if (_serial_output)
Serial.setDebugOutput(true);
_authenticated = (_username == NULL || _password == NULL || _server->authenticate(_username, _password));
if(!_authenticated){
if (_serial_output)
Serial.printf("Unauthenticated Update\n");
return;
}
WiFiUDP::stopAll();
if (_serial_output)
Serial.printf("Update: %s\n", upload.filename.c_str());
uint32_t maxSketchSpace = (ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000;
if(!Update.begin(maxSketchSpace)){//start with max available size
_setUpdaterError();
}
} else if(_authenticated && upload.status == UPLOAD_FILE_WRITE && !_updaterError.length()){
// 固件正在写入
if (_serial_output) Serial.printf(".");
if(Update.write(upload.buf, upload.currentSize) != upload.currentSize){
_setUpdaterError();
}
} else if(_authenticated && upload.status == UPLOAD_FILE_END && !_updaterError.length()){
// 固件正在写入结束
if(Update.end(true)){ //true to set the size to the current progress
if (_serial_output) Serial.printf("Update Success: %u\nRebooting...\n", upload.totalSize);
} else {
_setUpdaterError();
}
if (_serial_output) Serial.setDebugOutput(false);
} else if(_authenticated && upload.status == UPLOAD_FILE_ABORTED){
Update.end();
if (_serial_output) Serial.println("Update was aborted");
}
delay(0);
});
}

整体上来说, 博主比较建议这种方法, 简单快捷, 巧妙利用了 webserver.

4.3 实例

4.3.1 系统自带 OTA 之 Web 更新()

实验说明:

演示 ESP8266 OTA 之 Web 更新, 通过建立的 webserver 来上传新固件以达到更新目的.

实验准备:

NodeMcu 开发板

实验源码:

先往 ESP8266 烧写 V1.0 版本代码, 如下:

/*
* 功能描述: OTA 之 Web 更新 V1.0 版本代码
*/
#include <ESP8266WiFi.h>
#include <WiFiClient.h>
#include <ESP8266WebServer.h>
#include <ESP8266mDNS.h>
#include <ESP8266HTTPUpdateServer.h>
// 调试定义
#define DebugBegin(baud_rate) Serial.begin(baud_rate)
#define DebugPrintln(message) Serial.println(message)
#define DebugPrint(message) Serial.print(message)
#define DebugPrintF(...) Serial.printf( __VA_ARGS__ )
#define CodeVersion "CodeVersion V1.0"
const char* host = "esp8266-webupdate";
const char* ssid = "xxx";// 填上 Wi-Fi 账号
const char* password = "xxx";// 填上 Wi-Fi 密码
ESP8266WebServer httpServer(80);
ESP8266HTTPUpdateServer httpUpdater;
void setup(void) {
DebugBegin(115200);
DebugPrintln("Booting Sketch...");
DebugPrintln(CodeVersion);
Wi-Fi.mode(WIFI_AP_STA);
Wi-Fi.begin(ssid, password);
while (Wi-Fi.waitForConnectResult() != WL_CONNECTED) {
Wi-Fi.begin(ssid, password);
DebugPrintln("WiFi failed, retrying.");
}
// 启动 mdns 服务
MDNS.begin(host);
// 配置 webserver 为更新 server
httpUpdater.setup(&httpServer);
httpServer.begin();
MDNS.addService("http", "tcp", 80);
DebugPrintF("HTTPUpdateServer ready! Open http://%s.local/update in your browser\n", host);
}
void loop(void) {
httpServer.handleClient();
MDNS.update();
}

然后在串口调试器就可以看到 OTA 的更新页面地址:

然后在浏览器里面打开该地址, 会看到下面的界面:

接下来, 开始更新代码.

在首选项设置里面的 "显示详细输出" 选项中选中 "编译"

然后修改代码为 V1.1 版本, 如下:

/*
* 功能描述: OTA 之 Web 更新 V1.1 版本代码
*/
#include <ESP8266WiFi.h>
#include <WiFiClient.h>
#include <ESP8266WebServer.h>
#include <ESP8266mDNS.h>
#include <ESP8266HTTPUpdateServer.h>
// 调试定义
#define DebugBegin(baud_rate) Serial.begin(baud_rate)
#define DebugPrintln(message) Serial.println(message)
#define DebugPrint(message) Serial.print(message)
#define DebugPrintF(...) Serial.printf( __VA_ARGS__ )
#define CodeVersion "CodeVersion V1.1"
const char* host = "esp8266-webupdate";
const char* ssid = "xxx";// 填上 Wi-Fi 账号
const char* password = "xxx";// 填上 Wi-Fi 密码
ESP8266WebServer httpServer(80);
ESP8266HTTPUpdateServer httpUpdater;
void setup(void) {
DebugBegin(115200);
DebugPrintln("Booting Sketch...");
DebugPrintln(CodeVersion);
Wi-Fi.mode(WIFI_AP_STA);
Wi-Fi.begin(ssid, password);
while (Wi-Fi.waitForConnectResult() != WL_CONNECTED) {
Wi-Fi.begin(ssid, password);
DebugPrintln("WiFi failed, retrying.");
}
// 启动 mdns 服务
MDNS.begin(host);
// 配置 webserver 为更新 server
httpUpdater.setup(&httpServer);
httpServer.begin();
MDNS.addService("http", "tcp", 80);
DebugPrintF("HTTPUpdateServer ready! Open http://%s.local/update in your browser\n", host);
}
void loop(void) {
httpServer.handleClient();
MDNS.update();
}

编译该代码, 然后找到新固件的本地地址,

回到浏览器点击 "Choose file" 按钮然后选择该新固件就可以上传到 ESP8266 中去:

更新结束.

实验结果:

可以看到下面的打印结果:

实验总结:

这个更新界面做得有点丑, 可以提供给开发人员用, 尽量还是不要给消费者用.

4.3.2 自定义 OTA 之 Web 更新

实验说明:

在上面的系统自带 OTA 之 Web 更新实例中, 由于是系统自带的更新页面, 还是有点丑. 对于开发人员来说, 这个页面我表示接受不了了.

自定义页面有两种方式:

直接修改 ESP8266HTTPUpdateServer 里面 Web 页面, 读者可以把 ESP8266HTTPUpdateServer.cpp 文件里面的 serverIndex 改成下面博主提供的 serverIndex, 这里暂且不讲;

基于 ESP8266HTTPUpdateServer 库去自定义新库, 我们暂且命名为 ESP8266CustomHTTPUpdateServer, 博主建议并讲解这种方式;

ESP8266CustomHTTPUpdateServer 库的实现步骤:

请找到 ESP8266 核心库目录, 然后在 libraries 目录下拷贝 ESP8266HTTPUpdateServer 目录, 重命名为 ESP8266CustomHTTPUpdateServer

修改 ESP8266CustomHTTPUpdateServer 里面的类名, 把 ESP8266HTTPUpdateServer 统一改成 ESP8266CustomHTTPUpdateServer;

把 ESP8266CustomHTTPUpdateServer.cpp 文件里面的 serverIndex 改成以下内容:

static const char serverIndex[] PROGMEM ="<!DOCTYPE HTML>\r\n\
<HTML lang=\"en\">\r\n\
<head>\r\n\
<meta charset=\"UTF-8\">\r\n\
<title>ESP8266 WebOTA</title>\r\n\
<style>\r\n\
body{text-align: center;height: 100%;}\r\n\
div,input{padding:5px;font-size:12px;}\r\n\
input{width:95%;margin-top: 5px;}\r\n\
button{padding:5px;border:0;border-radius:20px;background-color:#1fa3ec;color:#fff;line-height:30px;font-size:16px;width:100%;margin-top: 40px;}\r\n\
.m-user-icon{width: 100px;height: 100px;border-radius: 50px}\r\n\
.m-user-name{font-size: 18px;font-weight: bold;margin-top: 10px;}\r\n\
.fileupload{position: relative;width:150px;height:25px;border:1px solid #66B3FF;border-radius: 4px;box-shadow: 1px 1px 5px #66B3FF;line-height: 25px;overflow: hidden;color: #66B3FF;;left: 50%;transform: translateX(-50%);text-overflow:ellipsis;white-space:nowrap}\r\n\
.fileupload input{position: absolute;width:150px;height:25px;top: 0;left: 50%;transform: translateX(-50%);opacity: 0;filter: alpha(opacity=0);-ms-filter: 'alpha(opacity=0)';}\r\n\
</style>\r\n\
</head>\r\n\
<body>\r\n\
<div style=\"text-align:center;display:inline-block;min-width:260px;margin-top: 80px;\">\r\n\
<div class=\"m-user\">\r\n\
<img class= \"m-user-icon\" src=\"data:image/PNG;base64,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\">\r\n\
<div class=\"m-user-name\">ESP8266 WebOTA 更新</div>\r\n\
</div>\r\n\
<form method='POST' action=''enctype='multipart/form-data'>\r\n\
<div class=\"fileupload\">\r\n\
<script>\r\n\
function getFilename(){\r\n\
let filename=document.getElementById(\"file\").value;\r\n\
if(filename===undefined||filename===\"\"){\r\n\
document.getElementById(\"filename\").innerHTML=\"点击此处上传文件 \";\r\n\
} else{\r\n\
let fn=filename.substring(filename.lastIndexOf(\"\\\")+1);\r\n\
document.getElementById(\"filename\").innerHTML=fn; \r\n\
}\r\n\
}\r\n\
</script>\r\n\
<span id=\"filename\">点击选择新固件</span>\r\n\
<input type=\"file\" name=\"file\" id=\"file\" onchange=\"getFilename()\"/>\r\n\
</div>\r\n\
<button type='submit'>确定更新</button>\r\n\
</form>\r\n\
<div style=\";margin-top: 10px;\">Copyright © 2019 By<a href='https://blog.csdn.net/wubo_fly'>单片机菜鸟</a></div>\r\n\
</div>\r\n\
</body>\r\n\
</HTML>";

当然好心的博主肯定不需要你们自己写, 下载下来放到你们的 8266 库目录吧 -- ESP8266CustomHTTPUpdateServer https://github.com/tingyouwu/8266libs

注意:

ESP8266CustomHTTPUpdateServer 库用法跟 ESP8266HTTPUpdateServer 库是一样的, 博主只是基于 ESP8266HTTPUpdateServer 修改 Web 页面而已, 其他一概不改动.

博主在 ArduinoIDE 1.8.5 版本和 esp8266 2.4.2 版本加入这个库, 编译不过. 后改用 ArduinoIDE 1.8.9 版本以及 esp8266 2.5.0 版本可以编译通过, 猜测是底层编译器不一样, 请读者注意一下.

实验准备:

需要大家有一定的 Web 基础 --HTML+CSS+JS

NodeMcu 开发板

实验步骤

先往 ESP8266 烧写 V1.0 版本代码, 如下:

/*
* 功能描述: 自定义 OTA 之 Web 更新 V1.0 版本代码
*/
#include <ESP8266WiFi.h>
#include <WiFiClient.h>
#include <ESP8266WebServer.h>
#include <ESP8266mDNS.h>
#include <ESP8266CustomHTTPUpdateServer.h>
// 调试定义
#define DebugBegin(baud_rate) Serial.begin(baud_rate)
#define DebugPrintln(message) Serial.println(message)
#define DebugPrint(message) Serial.print(message)
#define DebugPrintF(...) Serial.printf( __VA_ARGS__ )
#define CodeVersion "CodeVersion V1.0"
const char* host = "esp8266-webupdate";
const char* ssid = "xxxx";// 填上 Wi-Fi 账号
const char* password = "xxxx";// 填上 Wi-Fi 密码
ESP8266WebServer httpServer(80);
ESP8266CustomHTTPUpdateServer httpUpdater;
void setup(void) {
DebugBegin(115200);
DebugPrintln("Booting Sketch...");
DebugPrintln(CodeVersion);
Wi-Fi.mode(WIFI_AP_STA);
Wi-Fi.begin(ssid, password);
while (Wi-Fi.waitForConnectResult() != WL_CONNECTED) {
Wi-Fi.begin(ssid, password);
DebugPrintln("WiFi failed, retrying.");
}
// 启动 mdns 服务
MDNS.begin(host);
// 配置 webserver 为更新 server
httpUpdater.setup(&httpServer);
httpServer.begin();
MDNS.addService("http", "tcp", 80);
DebugPrintF("HTTPUpdateServer ready! Open http://%s.local/update in your browser\n", host);
}
void loop(void) {
httpServer.handleClient();
MDNS.update();
}

可以看到串口打印信息

然后可以在电脑浏览器访问 http://esp8266-webupdate.local/update

接着修改代码为 V1.1 版本, 如下:

/*
* 功能描述: 自定义 OTA 之 Web 更新 V1.1 版本代码
*/
#include <ESP8266WiFi.h>
#include <WiFiClient.h>
#include <ESP8266WebServer.h>
#include <ESP8266mDNS.h>
#include <ESP8266CustomHTTPUpdateServer.h>
// 调试定义
#define DebugBegin(baud_rate) Serial.begin(baud_rate)
#define DebugPrintln(message) Serial.println(message)
#define DebugPrint(message) Serial.print(message)
#define DebugPrintF(...) Serial.printf( __VA_ARGS__ )
#define CodeVersion "CodeVersion V1.1"
const char* host = "esp8266-webupdate";
const char* ssid = "TP-LINK_5344";// 填上 Wi-Fi 账号
const char* password = "6206908you11011010";// 填上 Wi-Fi 密码
ESP8266WebServer httpServer(80);
ESP8266CustomHTTPUpdateServer httpUpdater;
void setup(void) {
DebugBegin(115200);
DebugPrintln("Booting Sketch...");
DebugPrintln(CodeVersion);
Wi-Fi.mode(WIFI_AP_STA);
Wi-Fi.begin(ssid, password);
while (Wi-Fi.waitForConnectResult() != WL_CONNECTED) {
Wi-Fi.begin(ssid, password);
DebugPrintln("WiFi failed, retrying.");
}
// 启动 mdns 服务
MDNS.begin(host);
// 配置 webserver 为更新 server
httpUpdater.setup(&httpServer);
httpServer.begin();
MDNS.addService("http", "tcp", 80);
DebugPrintF("HTTPUpdateServer ready! Open http://%s.local/update in your browser\n", host);
}
void loop(void) {
httpServer.handleClient();
MDNS.update();
}

编译代码, 注意最终生成 bin 文件存储位置

选择该 bin 文件, 更新完毕, 可以看到串口打印信息:

5. ServerUpdateOTA -- OTA 之服务器更新

OTA 之服务器更新, 通过公网服务器, 把固件放在云端服务器上, 下载更新, 这种方式适合零基础的消费者, 无感知更新;

不过由于博主暂时没有自主开发服务器程序的能力, 所以这里暂时只讨论需用用到的库, 原理本质上都是一样的.

ServerUpdateOTA 需要用到 ESP8266httpUpdate 库, 请在代码中引入以下头文件:


  1. #include <ESP8266HTTPClient.h>
  2. #include <ESP8266httpUpdate.h>

接下来, 先上一个博主总结的百度脑图:

方法只有两个, 非常简单.

5.1 update -- 更新固件

函数说明:

/**
* 更新固件(http)
* @param url 固件下载地址
* @param currentVersion 固件当前版本
* @return t_httpUpdate_return 更新状态
*/
t_httpUpdate_return update(const String& url, const String& currentVersion = "");
/**
* 更新固件(https)
* @param url 固件下载地址
* @param currentVersion 固件当前版本
* @param httpsFingerprint https 相关信息
* @return t_httpUpdate_return 更新状态
*/
t_httpUpdate_return update(const String& url, const String& currentVersion,const String& httpsFingerprint);
/**
* 更新固件(https)
* @param url 固件下载地址
* @param currentVersion 固件当前版本
* @param httpsFingerprint https 相关信息
* @return t_httpUpdate_return 更新状态
*/
t_httpUpdate_return update(const String& url, const String& currentVersion,const uint8_t httpsFingerprint[20]); // BearSSL
/**
* 更新固件(http)
* @param host 主机
* @param port 端口
* @param uri uri 地址
* @param currentVersion 固件当前版本
* @return t_httpUpdate_return 更新状态
*/
t_httpUpdate_return update(const String& host, uint16_t port, const String& uri = "/",const String& currentVersion = "");
/**
* 更新固件(https)
* @param host 主机
* @param port 端口
* @param uri uri 地址
* @param currentVersion 固件当前版本
* @param httpsFingerprint https 相关信息
* @return t_httpUpdate_return 更新状态
*/
t_httpUpdate_return update(const String& host, uint16_t port, const String& url,const String& currentVersion, const String& httpsFingerprint);
/**
* 更新固件(https)
* @param host 主机
* @param port 端口
* @param uri uri 地址
* @param currentVersion 固件当前版本
* @param httpsFingerprint https 相关信息
* @return t_httpUpdate_return 更新状态
*/
t_httpUpdate_return update(const String& host, uint16_t port, const String& url,const String& currentVersion, const uint8_t httpsFingerprint[20]); // BearSSL

t_httpUpdate_return 定义如下:

enum HTTPUpdateResult {
HTTP_UPDATE_FAILED,// 更新失败
HTTP_UPDATE_NO_UPDATES,// 未开始更新
HTTP_UPDATE_OK// 更新完毕
};

5.2 rebootOnUpdate -- 是否自动重启

函数说明:

/**
* 设置是否自动重启
* @param reboot true 表示自动重启, 默认 false
*/
void rebootOnUpdate(bool reboot);

5.3 updateSpiffs -- 更新 SPIFFS

函数说明:

/**
* 更新固件(http)
* @param url 固件下载地址
* @param currentVersion 固件当前版本
* @return t_httpUpdate_return 更新状态
*/
t_httpUpdate_return updateSpiffs(const String& url, const String& currentVersion = "");
/**
* 更新固件(http)
* @param url 固件下载地址
* @param currentVersion 固件当前版本
* @return t_httpUpdate_return 更新状态
*/
t_httpUpdate_return updateSpiffs(const String& url, const String& currentVersion, const String& httpsFingerprint);
/**
* 更新固件(http)
* @param url 固件下载地址
* @param currentVersion 固件当前版本
* @return t_httpUpdate_return 更新状态
*/
t_httpUpdate_return updateSpiffs(const String& url, const String& currentVersion, const uint8_t httpsFingerprint[20]); // BearSSL

5.4 实例

博主没有具体的服务器(原理都是非常相似的, 把服务器上面的新固件下载下来, 然后更新), 所以这里只是给一个通用的代码:

/**
* 功能描述: OTA 之服务器更新
*/
#include <Arduino.h>
#include <ESP8266WiFi.h>
#include <ESP8266WiFiMulti.h>
#include <ESP8266HTTPClient.h>
#include <ESP8266httpUpdate.h>
// 调试定义
#define DebugBegin(baud_rate) Serial.begin(baud_rate)
#define DebugPrintln(message) Serial.println(message)
#define DebugPrint(message) Serial.print(message)
#define DebugPrintF(...) Serial.printf( __VA_ARGS__ )
ESP8266WiFiMulti WiFiMulti;
void setup() {
DebugBegin(115200);
Wi-Fi.mode(WIFI_STA);
// 这里填上 Wi-Fi 账号 SSID 和 密码 PASSWORD
WiFiMulti.addAP("SSID", "PASSWORD");
}
void loop() {
// wait for Wi-Fi connection
if ((WiFiMulti.run() == WL_CONNECTED)) {
// 填上服务器地址
t_httpUpdate_return ret = ESPhttpUpdate.update("http://server/file.bin");
//t_httpUpdate_return ret = ESPhttpUpdate.update("https://server/file.bin", "","fingerprint");
switch (ret) {
case HTTP_UPDATE_FAILED:
DebugPrintF("HTTP_UPDATE_FAILD Error (%d): %s", ESPhttpUpdate.getLastError(), ESPhttpUpdate.getLastErrorString().c_str());
break;
case HTTP_UPDATE_NO_UPDATES:
DebugPrintln("HTTP_UPDATE_NO_UPDATES");
break;
case HTTP_UPDATE_OK:
DebugPrintln("HTTP_UPDATE_OK");
break;
}
}
}

等博主后面学习了服务器开发, 再补回来吧.

6. 总结

在 Arduino Core For ESP8266 中, 使用 OTA 功能可以有三种方式:

ArduinoOTA -- OTA 之 Arduino IDE 更新, 也就是无线更新需要利用到 Arduino IDE, 只是不需要通过串口线烧写而已, 这种方式适合开发者;

WebUpdateOTA -- OTA 之 Web 更新, 通过 8266 上配置的 webserver 来选择固件更新, 这种方式适合开发者以及有简单配置经验的消费者;

ServerUpdateOTA -- OTA 之服务器更新, 通过公网服务器, 把固件放在云端服务器上, 下载更新, 这种方式适合零基础的消费者, 无感知更新;

至于使用哪一种, 看具体需求.

其实不管哪一种方式, 其最终目的:

为了把新固件烧写到 Flash 中, 然后替代掉旧固件, 以达到更新固件的效果.

注意, OTA 更新也可以更新 SPIFFS.

ESP8266 开发之旅 网络篇 无线更新 --OTA 固件更新相关推荐

  1. ESP8266开发之旅 网络篇⑭ web配网

    文章目录 1. 前言 2. Web配网(AP配网) 2.1 自定义AP配网 2.2 WiFiManager 3. 总结 授人以鱼不如授人以渔,目的不是为了教会你具体项目开发,而是学会学习的能力.希望大 ...

  2. ESP8266开发之旅 网络篇③ Soft-AP——ESP8266WiFiAP库的使用

    授人以鱼不如授人以渔,目的不是为了教会你具体项目开发,而是学会学习的能力.希望大家分享给你周边需要的朋友或者同学,说不定大神成长之路有博哥的奠基石... 共同学习成长QQ群 622368884,不喜勿 ...

  3. ESP8266开发之旅 网络篇⑥ ESP8266WiFiGeneric——基础库

    授人以鱼不如授人以渔,目的不是为了教会你具体项目开发,而是学会学习的能力.希望大家分享给你周边需要的朋友或者同学,说不定大神成长之路有博哥的奠基石... 共同学习成长QQ群 622368884,不喜勿 ...

  4. ESP8266开发之旅 网络篇⑲ WebSocket Server——全双工通信

    文章目录 1.前言 2. WebSocket协议 2.1 客户端发起WS请求 2.2 服务器响应WS请求 2.3 WS数据交互协议 3. arduinoWebSockets -- ESP8266 We ...

  5. ESP8266开发之旅 网络篇⑦ TCP Server TCP Client

    授人以鱼不如授人以渔,目的不是为了教会你具体项目开发,而是学会学习的能力.希望大家分享给你周边需要的朋友或者同学,说不定大神成长之路有博哥的奠基石... 共同学习成长QQ群 622368884,不喜勿 ...

  6. ESP8266开发之旅 网络篇⑧ SmartConfig——一键配网

    授人以鱼不如授人以渔,目的不是为了教会你具体项目开发,而是学会学习的能力.希望大家分享给你周边需要的朋友或者同学,说不定大神成长之路有博哥的奠基石... 共同学习成长QQ群 622368884,不喜勿 ...

  7. ESP8266开发之旅 网络篇⑪ WebServer——ESP8266WebServer库的使用

    授人以鱼不如授人以渔,目的不是为了教会你具体项目开发,而是学会学习的能力.希望大家分享给你周边需要的朋友或者同学,说不定大神成长之路有博哥的奠基石... 共同学习成长QQ群 622368884,不喜勿 ...

  8. ESP8266开发之旅 应用篇⑭ 局域网应用 ——炫酷RGB彩灯(WebSocket实现)

    文章目录 1.前言 2.技术原理 3.ESP8266 源码 4. APP 授人以鱼不如授人以渔,目的不是为了教会你具体项目开发,而是学会学习的能力.希望大家分享给你周边需要的朋友或者同学,说不定大神成 ...

  9. ESP8266开发之旅 应用篇⑧Arduino版本 WiFi杀手

    文章目录 1. 前言 授人以鱼不如授人以渔,目的不是为了教会你具体项目开发,而是学会学习的能力.希望大家分享给你周边需要的朋友或者同学,说不定大神成长之路有博哥的奠基石... 共同学习成长QQ群 62 ...

  10. ESP8266开发之旅 进阶篇⑨ 深入了解 802.11 无线协议(非常重要)

    文章目录 1. 前言 2. WLAN拓扑结构 2.1 IBSS -- 无AP 2.2 BSS -- 有AP 2.3 ESS -- 有AP 和 DS 3. 802.11 协议 3.1 802.11 帧 ...

最新文章

  1. .Net平台Winform两个ComboBox控件绑定同一个数据源
  2. 理解关于java反射中类的域及修饰符
  3. python中list的意思_list在python中是什么意思
  4. 今明两场直播丨openGauss和MogDB的优化分享;为什么学习 PostgreSQL 是当下不二之选...
  5. python中形参可以使用中文定义嘛_python中函数的参数分类
  6. Ubuntu 11.10 make menuconfig 失败的解决方法
  7. 2018 ACM 国际大学生程序设计竞赛上海大都会赛重现赛 C Thinking Bear magic
  8. 西北工业大学生态环境学院张文宇课题组博士后招聘启事
  9. 如何关闭迅雷频繁自动弹出更新到新版本的提示
  10. 今日,立秋。秋季养生篇。
  11. PlayStation@4功能介绍及测试应用
  12. input函数使用及运算符
  13. 虚拟机无法上网的原因
  14. java 怎么让图片运动,小编给你传授java怎么实现键盘控制图片移动
  15. 人力资源外包是什么?转型灵活用工系统,解决服务痛点
  16. sap客户信贷_FD32维护客户信贷数据
  17. 比 Xshell 还好用的 SSH 客户端神器,MobaXterm 太爱了!
  18. NVisual-自动化网络拓扑
  19. 关于用c++写心理测试是有分支就行的事(这次是哈利波特分院帽)
  20. (ACWing217)绿豆蛙的归宿(数学期望)

热门文章

  1. 勾股定理计算机语言,勾股定理公式计算器
  2. BGP联邦和反射器实验
  3. linux 命令 unicode,linux下中文转unicode
  4. Win7 远程桌面连接不上
  5. 麒麟子带你快速进入Cocos Creator的3D世界
  6. 深度学习中的多任务学习介绍
  7. Cocos Creator 国旗头像生成器,源码奉上!
  8. 一个非常naive的分数阶微积分介绍
  9. 英文字体识别在线识别_如何查找和识别字体
  10. 一元三次方程的解法史