ESP8266开发之旅 网络篇⑯ 无线更新——OTA固件更新

1. 前言

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

  • 需要经常插拔转接线,很容易造成8266串口丢失;
  • 如果是将ESP8266做成产品并交付到客户手上之后应该如何更新产品中的ESP8266固件呢?难道要用户拿到技术中心来更新?如果是这样,这个产品必定属于失败产品。

在这里,就引入我们本篇章需要了解的实用知识 —— OTA功能。
    OTA —— Over the air update of the firmware,也就是无线固件更新,这个可以说是非常炫酷且实用的功能。
    那么OTA的本质是什么?它又是如何工作的呢?
    一般情况下,当我们使用串口线更新8266的固件是通过SerialBootLoader来更新,这个属于开发板内置好的默认方式。
    而OTA因为用到的是WIFI网络,所以我们假设也有一个名为“WIFIOTABootLoader”的东西来处理固件的无线写入更新,但是这个WIFIOTABootLoader需要我们先通过串口线预先写入到ESP8266。换句话说就是,我们得在项目代码中嵌入用于OTA的 WIFIOTABootLoader。
    那么问题来了,WIFIOTABootLoader到底是什么原理呢?
    万变不离其宗,博主第一个想到的就是 WebServer、UDP、mDNS的混合使用,通过mDNS可以解决域名访问问题,WebServer提供web页面供开发者上传固件文件,然后WebServer处理具体的请求,再把文件写入flash中(万幸的是,博主去看底层代码,确实有这样设计的思路)。

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

  • ESP8266开发之旅 网络篇⑩ 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中,然后替代掉旧固件,以达到更新固件的效果。

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

  1. 没有更新固件前,当前固件(current sketch,绿色部分)和 文件系统(spiffs,蓝色部分)位于flash存储空间的不同区域,中间空白的是空闲空间;
  2. 固件更新时,新固件(new sketch,黄色所示)将写入空闲空间,此时flash同时存在这三个对象;
  3. 重启模块后,新固件会覆盖掉旧固件,然后从当前固件的开始地址处开始运行,以达到固件更新的目的。

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

3. ArduinoOTA —— OTA之Arduino IDE更新

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

#include 

    查看 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 THandlerFunction;

/**
 * OTA开始连接回调 fn
 * @param fn 回调函数
 */
void onStart(THandlerFunction fn);

3.3.2 onEnd —— OTA结束回调

函数说明:

/**
 * 回调函数定义
 */
typedef std::function THandlerFunction;

/**
 * OTA结束回调 fn
 * @param fn 回调函数
 */
void onEnd(THandlerFunction fn);

3.3.3 onError —— OTA出错回调

函数说明:

/**
 * 回调函数定义
 * @param ota_error_t 错误原因
 */
typedef std::function 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 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 提取码:g9ds

实验步骤

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

/**
 * 功能描述:OTA之Arduino IDE更新 V1.0版本代码
 *
 */
#include 
#include 
#include 
#include 

//调试定义
#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";//填上wifi账号
const char* password = "xxxxx";//填上wifi密码

void setup() {
  DebugBegin(115200);
  DebugPrintln("Booting Sketch....");
  DebugPrintln(CodeVersion);
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
  while (WiFi.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(WiFi.localIP());
}

void loop() {
  ArduinoOTA.handle();
}

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

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

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

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

/**
 * 功能描述:OTA之Arduino IDE更新 V1.1版本代码
 *
 */
#include 
#include 
#include 
#include 

//调试定义
#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";//填上wifi账号
const char* password = "xxxx";//填上wifi密码

void setup() {
  DebugBegin(115200);
  DebugPrintln("Booting Sketch....");
  DebugPrintln(CodeVersion);
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
  while (WiFi.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(WiFi.localIP());
}

void loop() {
  ArduinoOTA.handle();
}

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

更新完毕,重启8266

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

  • 连接WIFI
  • 配置 ArduinoOTA 对象的事件函数
  • 启动 ArduinoOTA 服务 ArduinoOTA.begin()
  • 在 loop() 函数将处理权交由 ArduinoOTA.handle()

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

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

4. WebUpdateOTA —— OTA之web更新

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

  1. 用ESP8266先建立一个Web服务器然后提供一个web更新界面,需要使用到库 ESP8266HTTPUpdateServer
  2. 通过Arduino将源文件编译为*.bin的二进制文件;
  3. 通过mDNS功能在浏览器中访问ESP8266的服务器页面,默认服务地址为:http://esp8266.local/update;
  4. 通过Web界面将本地编译好的*.bin二进制固件文件上传到ESP8266中;
  5. 上传完成编译文件后ESP8266将固件写入Flash中

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

#include 
#include 
#include 

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

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

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 
#include 
#include 
#include 
#include 

//调试定义
#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";//填上wifi账号
const char* password = "xxx";//填上wifi密码

ESP8266WebServer httpServer(80);
ESP8266HTTPUpdateServer httpUpdater;

void setup(void) {
  DebugBegin(115200);
  DebugPrintln("Booting Sketch...");
  DebugPrintln(CodeVersion);
  WiFi.mode(WIFI_AP_STA);
  WiFi.begin(ssid, password);

  while (WiFi.waitForConnectResult() != WL_CONNECTED) {
    WiFi.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的更新页面地址:

然后在浏览器里面打开该地址,会看到下面的界面:
ESP8266开发之旅 网络篇⑯ 无线更新——OTA固件更新_第1张图片

接下来,开始更新代码。
在首选项设置里面的“显示详细输出”选项中选中"编译"

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

/*
 * 功能描述:OTA之web更新 V1.1版本代码
 */

#include 
#include 
#include 
#include 
#include 

//调试定义
#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";//填上wifi账号
const char* password = "xxx";//填上wifi密码

ESP8266WebServer httpServer(80);
ESP8266HTTPUpdateServer httpUpdater;

void setup(void) {
  DebugBegin(115200);
  DebugPrintln("Booting Sketch...");
  DebugPrintln(CodeVersion);
  WiFi.mode(WIFI_AP_STA);
  WiFi.begin(ssid, password);

  while (WiFi.waitForConnectResult() != WL_CONNECTED) {
    WiFi.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中去:

ESP8266开发之旅 网络篇⑯ 无线更新——OTA固件更新_第2张图片

更新结束。

实验结果

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

实验总结

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

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 =
"\r\n\
 \r\n\
 \r\n\
 \r\n\
 ESP8266 WebOTA\r\n\
 \r\n\
 \r\n\
 \r\n\
 
\r\n\
\r\n\ \r\n\
ESP8266 WebOTA更新
\r\n\
\r\n\
\r\n\
\r\n\ \r\n\ 点击选择新固件\r\n\ \r\n\
\r\n\ \r\n\
\r\n\
Copyright © 2019 By单片机菜鸟
\r\n\
\r\n\ \r\n\ ";

当然好心的博主肯定不需要你们自己写,下载下来放到你们的8266库目录吧 —— ESP8266CustomHTTPUpdateServer

注意:

  • 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 
#include 
#include 
#include 
#include 

//调试定义
#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";//填上wifi账号
const char* password = "xxxx";//填上wifi密码

ESP8266WebServer httpServer(80);
ESP8266CustomHTTPUpdateServer httpUpdater;

void setup(void) {
  DebugBegin(115200);
  DebugPrintln("Booting Sketch...");
  DebugPrintln(CodeVersion);
  WiFi.mode(WIFI_AP_STA);
  WiFi.begin(ssid, password);

  while (WiFi.waitForConnectResult() != WL_CONNECTED) {
    WiFi.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 
#include 
#include 
#include 
#include 

//调试定义
#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";//填上wifi账号
const char* password = "6206908you11011010";//填上wifi密码

ESP8266WebServer httpServer(80);
ESP8266CustomHTTPUpdateServer httpUpdater;

void setup(void) {
  DebugBegin(115200);
  DebugPrintln("Booting Sketch...");
  DebugPrintln(CodeVersion);
  WiFi.mode(WIFI_AP_STA);
  WiFi.begin(ssid, password);

  while (WiFi.waitForConnectResult() != WL_CONNECTED) {
    WiFi.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 库,请在代码中引入以下头文件:

#include 
#include 

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

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

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 

#include 
#include 

#include 
#include 

//调试定义
#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);
  WiFi.mode(WIFI_STA);
  //这里填上wifi账号 SSID 和 密码 PASSWORD
  WiFiMulti.addAP("SSID", "PASSWORD");
}

void loop() {
  // wait for WiFi 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。

转载于:https://www.cnblogs.com/danpianjicainiao/p/11051039.html

你可能感兴趣的:(ESP8266开发之旅 网络篇⑯ 无线更新——OTA固件更新)