【ESP开发板】篇七、arduino+L0L1n ESP8266实现OTA功能(ArduinoOTA、WebServerOTA_WIFI_STA/WIFI_AP/WIFI_AP_STA、HTTPOT)

一、 OTA功能介绍:

  OTA(Over The Air)是一项基于短消息机制,通过互联网wln或局域网wifi实现固件动态下载、删除与更新。通过吸纳网上各种版本的文章,整理出几种常用的ESP8266实现OTA升级固件方案:

  1. Arduino OTA:使用Arduino IDE提供的OTA功能,可以直接通过Arduino IDE上传固件到ESP8266
  2. WebServer OTA STA模式:ESP8266运行一个简易的Web服务器,您可以在同一个局域网内通过Web页面上传新固件来更新设备。
  3. WebServer OTA AP模式:ESP8266运行一个简易的Web服务器,您可以通过链接ESP8266发射的AP信号,然后通过访问Web页面上传新固件来更新设备。
  4. WebServer OTA STA模式+AP模式:ESP8266运行一个简易的Web服务器,您可以同一局域网或链接AP访问通过Web页面上传新固件来更新设备。
  5. HTTP OTA:ESP8266从指定的HTTP服务器下载固件并自动进行更新。

二、库依赖

ArduinoOTA库
ESP8266WebServer库
ESP8266WiFi库
FS库
WiFiUdp库
NTPClient库
md5库

三、详细介绍:

  1. 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倍大小去估算。

  1. 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 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值