欢迎您访问 最编程 本站为您分享编程语言代码,编程技术文章!
您现在的位置是: 首页

物联网】如何实现 ESP32 固件的 OTA 在线升级和更新?

最编程 2024-05-04 12:15:07
...

1、背景

在实际产品开发过程中,在线升级可以远程解决产品软件开发引入的问题,更好地满足用户需求。

2、OTA 简介

OTA(空中)更新是使用 Wi-Fi 连接而不是串行端口将固件加载到 ESP 模块的过程。

2.1、ESP32 的 OTA 升级有三种方式:

Arduino IDE:主要用于软件开发阶段,实现不接线固件烧写
Web Browser:通过 Web 浏览器手动提供应用程序更新模块
HTTP Server:自动使用http服务器 - 针对产品应用
在三种升级情况下,必须通过串行端口完成第一个固件上传。

OTA 进程没有强加的安全性,需要确保开发人员只能从合法/受信任的来源获得更新。更新完成后,模块将重新启动,并执行新的代码。开发人员应确保在模块上运行的应用程序以安全的方式关闭并重新启动。

2.2、保密性 Security
模块必须以无线方式显示,以便通过新的草图进行更新。 这使得模块被强行入侵并加载了其他代码。 为了减少被黑客入侵的可能性,请考虑使用密码保护您的上传,选择某些OTA端口等。

可以提高安全性的 ArduinoOTA 库接口:

void setPort(uint16_t port);
void setHostname(const char* hostname);
void setPassword(const char* password);
void onStart(OTA_CALLBACK(fn));
void onEnd(OTA_CALLBACK(fn));
void onProgress(OTA_CALLBACK_PROGRESS(fn));
void onError(OTA_CALLBACK_ERROR (fn));
已经内置了某些保护功能,不需要开发人员进行任何其他编码。ArduinoOTA和espota.py使用Digest-MD5来验证上传。使用MD5校验和,在ESP端验证传输数据的完整性。

2.2、OTA 升级策略 - 针对 http
ESP32 连接 HTTP 服务器,发送请求 Get 升级固件;每次读取1KB固件数据,写入Flash。

ESP32 SPI Flash 内有与升级相关的(至少)四个分区:OTA data、Factory App、OTA_0、OTA_1。其中 FactoryApp 内存有出厂时的默认固件。

首次进行 OTA 升级时,OTA Demo 向 OTA_0 分区烧录目标固件,并在烧录完成后,更新 OTA data 分区数据并重启。

系统重启时获取 OTA data 分区数据进行计算,决定此后加载 OTA_0 分区的固件执行(而不是默认的 Factory App 分区内的固件),从而实现升级。

同理,若某次升级后 ESP32 已经在执行 OTA_0 内的固件,此时再升级时 OTA Demo 就会向 OTA_1 分区写入目标固件。再次启动后,执行 OTA_1 分区实现升级。以此类推,升级的目标固件始终在 OTA_0、OTA_1 两个分区之间交互烧录,不会影响到出厂时的 Factory App 固件。

微信图片_20230227104327.png

3、OTA 实例解析

3,1、Arduino IDE 方案固件更新

从 Arduino IDE 无线上传模块适用于以下典型场景:

在固件开发过程中,通过串行加载更快的替代方案 - 用于更新少量模块,只有模块在与 Arduino IDE 的计算机相同的网络上可用。

参考实例:

include <WiFi.h>

include <ESPmDNS.h>

include <WiFiUdp.h>

include <ArduinoOTA.h>

const char* ssid = "..........";
const char* password = "..........";

void setup() {
Serial.begin(115200);
Serial.println("Booting");
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
while (WiFi.waitForConnectResult() != WL_CONNECTED) {

Serial.println("Connection Failed! Rebooting...");
delay(5000);
ESP.restart();

}

// Port defaults to 3232
// ArduinoOTA.setPort(3232);

// Hostname defaults to esp3232-[MAC]
// ArduinoOTA.setHostname("myesp32");

// 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;
  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);
})
.onEnd([]() {
  Serial.println("\nEnd");
})
.onProgress([](unsigned int progress, unsigned int total) {
  Serial.printf("Progress: %u%%\r", (progress / (total / 100)));
})
.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");
});

ArduinoOTA.begin();

Serial.println("Ready");
Serial.print("IP address: ");
Serial.println(WiFi.localIP());
}

void loop() {
ArduinoOTA.handle();
}
3,2、Web Browser 方案固件更新

该方案使用场景:

直接从 Arduino IDE 加载是不方便或不可能的

用户无法从外部更新服务器公开 OTA 的模块

在设置更新服务器不可行时,将部署后的更新提供给少量模块

参考实例:

include <WiFi.h>

include <WiFiClient.h>

include <WebServer.h>

include <ESPmDNS.h>

include <Update.h>

const char* host = "esp32";
const char* ssid = "xxx";
const char* password = "xxxx";

WebServer server(80);

/*

  • Login page

*/

const char* loginIndex =
"

"
"<table width='20%' bgcolor='A09F9F' align='center'>"
    "<tr>"
        "<td colspan=2>"
            "

ESP32 Login Page

" "<br>" "</td>" "<br>" "<br>" "</tr>" "<td>Username:</td>" "<td><input type='text' size=25 name='userid'><br></td>" "</tr>" "<br>" "<br>" "<tr>" "<td>Password:</td>" "<td><input type='Password' size=25 name='pwd'><br></td>" "<br>" "<br>" "</tr>" "<tr>" "<td><input type='submit' onclick='check(this.form)' value='Login'></td>" "</tr>" "</table>"

"

"
"";

/*

  • Server Index Page

*/

const char* serverIndex =
""
"

"
""
    "<input type='submit' value='Update'>"
"</form>"

"

progress: 0%
"
"";

/*

  • setup function

*/
void setup(void) {
Serial.begin(115200);

// Connect to WiFi network
WiFi.begin(ssid, password);
Serial.println("");

// Wait for connection
while (WiFi.status() != WL_CONNECTED) {

delay(500);
Serial.print(".");

}
Serial.println("");
Serial.print("Connected to ");
Serial.println(ssid);
Serial.print("IP address: ");
Serial.println(WiFi.localIP());

/use mdns for host name resolution/
if (!MDNS.begin(host)) { //http://esp32.local

Serial.println("Error setting up MDNS responder!");
while (1) {
  delay(1000);
}

}
Serial.println("mDNS responder started");
/return index page which is stored in serverIndex /
server.on("/", HTTP_GET, []() {

server.sendHeader("Connection", "close");
server.send(200, "text/html", loginIndex);

});
server.on("/serverIndex", HTTP_GET, []() {

server.sendHeader("Connection", "close");
server.send(200, "text/html", serverIndex);

});
/handling uploading firmware file /
server.on("/update", HTTP_POST, []() {

server.sendHeader("Connection", "close");
server.send(200, "text/plain", (Update.hasError()) ? "FAIL" : "OK");
ESP.restart();

}, []() {

HTTPUpload& upload = server.upload();
if (upload.status == UPLOAD_FILE_START) {
  Serial.printf("Update: %s\n", upload.filename.c_str());
  if (!Update.begin(UPDATE_SIZE_UNKNOWN)) { //start with max available size
    Update.printError(Serial);
  }
} else if (upload.status == UPLOAD_FILE_WRITE) {
  /* flashing firmware to ESP*/
  if (Update.write(upload.buf, upload.currentSize) != upload.currentSize) {
    Update.printError(Serial);
  }
} else if (upload.status == UPLOAD_FILE_END) {
  if (Update.end(true)) { //true to set the size to the current progress
    Serial.printf("Update Success: %u\nRebooting...\n", upload.totalSize);
  } else {
    Update.printError(Serial);
  }
}

});
server.begin();
}

void loop(void) {
server.handleClient();
delay(1);
}
3.3、HTTP 服务器实现更新

ESPhttpUpdate 类可以检查更新并从 HTTP Web 服务器下载二进制文件。可以从网络或 Internet 上的每个 IP 或域名地址下载更新,主要应用于远程服务器更新升级。

参考实例:

/**
AWS S3 OTA Update
Date: 14th June 2017
Author: Arvind Ravulavaru https://github.com/arvindr21
Purpose: Perform an OTA update from a bin located in Amazon S3 (HTTP Only)

Upload:
Step 1 : Download the sample bin file from the examples folder
Step 2 : Upload it to your Amazon S3 account, in a bucket of your choice
Step 3 : Once uploaded, inside S3, select the bin file >> More (button on top of the file list) >> Make Public
Step 4 : You S3 URL => http://bucket-name.s3.ap-south-1.amazonaws.com/sketch-name.ino.bin
Step 5 : Build the above URL and fire it either in your browser or curl it curl -I -v http://bucket-name.ap-south-1.amazonaws.com/sketch-name.ino.bin to validate the same
Step 6: Plug in your SSID, Password, S3 Host and Bin file below

Build & upload
Step 1 : Menu > Sketch > Export Compiled Library. The bin file will be saved in the sketch folder (Menu > Sketch > Show Sketch folder)
Step 2 : Upload bin to S3 and continue the above process

// Check the bottom of this sketch for sample serial monitor log, during and after successful OTA Update
*/

include <WiFi.h>

include <Update.h>

WiFiClient client;

// Variables to validate
// response from S3
int contentLength = 0;
bool isValidContentType = false;

// Your SSID and PSWD that the chip needs
// to connect to
const char* SSID = "YOUR-SSID";
const char* PSWD = "YOUR-SSID-PSWD";

// S3 Bucket Config
String host = "bucket-name.s3.ap-south-1.amazonaws.com"; // Host => bucket-name.s3.region.amazonaws.com
int port = 80; // Non https. For HTTPS 443. As of today, HTTPS doesn't work.
String bin = "/sketch-name.ino.bin"; // bin file name with a slash in front.

// Utility to extract header value from headers
String getHeaderValue(String header, String headerName) {
return header.substring(strlen(headerName.c_str()));
}

// OTA Logic
void execOTA() {
Serial.println("Connecting to: " + String(host));
// Connect to S3
if (client.connect(host.c_str(), port)) {

// Connection Succeed.
// Fecthing the bin
Serial.println("Fetching Bin: " + String(bin));

// Get the contents of the bin file
client.print(String("GET ") + bin + " HTTP/1.1\r\n" +
             "Host: " + host + "\r\n" +
             "Cache-Control: no-cache\r\n" +
             "Connection: close\r\n\r\n");

// Check what is being sent
//    Serial.print(String("GET ") + bin + " HTTP/1.1\r\n" +
//                 "Host: " + host + "\r\n" +
//                 "Cache-Control: no-cache\r\n" +
//                 "Connection: close\r\n\r\n");

unsigned long timeout = millis();
while (client.available() == 0) {
  if (millis() - timeout > 5000) {
    Serial.println("Client Timeout !");
    client.stop();
    return;
  }
}
// Once the response is available,
// check stuff

/*
   Response Structure
    HTTP/1.1 200 OK
    x-amz-id-2: NVKxnU1aIQMmpGKhSwpCBh8y2JPbak18QLIfE+OiUDOos+7UftZKjtCFqrwsGOZRN5Zee0jpTd0=
    x-amz-request-id: 2D56B47560B764EC
    Date: Wed, 14 Jun 2017 03:33:59 GMT
    Last-Modified: Fri, 02 Jun 2017 14:50:11 GMT
    ETag: "d2afebbaaebc38cd669ce36727152af9"
    Accept-Ranges: bytes
    Content-Type: application/octet-stream
    Content-Length: 357280
    Server: AmazonS3
                               
    {{BIN FILE CONTENTS}}

*/
while (client.available()) {
  // read line till /n
  String line = client.readStringUntil('\n');
  // remove space, to check if the line is end of headers
  line.trim();

  // if the the line is empty,
  // this is end of headers
  // break the while and feed the
  // remaining `client` to the
  // Update.writeStream();
  if (!line.length()) {
    //headers ended
    break; // and get the OTA started
  }

  // Check if the HTTP Response is 200
  // else break and Exit Update
  if (line.startsWith("HTTP/1.1")) {
    if (line.indexOf("200") < 0) {
      Serial.println("Got a non 200 status code from server. Exiting OTA Update.");
      break;
    }
  }

  // extract headers here
  // Start with content length
  if (line.startsWith("Content-Length: ")) {
    contentLength = atoi((getHeaderValue(line, "Content-Length: ")).c_str());
    Serial.println("Got " + String(contentLength) + " bytes from server");
  }

  // Next, the content type
  if (line.startsWith("Content-Type: ")) {
    String contentType = getHeaderValue(line, "Content-Type: ");
    Serial.println("Got " + contentType + " payload.");
    if (contentType == "application/octet-stream") {
      isValidContentType = true;
    }
  }
}

} else {

// Connect to S3 failed
// May be try?
// Probably a choppy network?
Serial.println("Connection to " + String(host) + " failed. Please check your setup");
// retry??
// execOTA();

}

// Check what is the contentLength and if content type is application/octet-stream
Serial.println("contentLength : " + String(contentLength) + ", isValidContentType : " + String(isValidContentType));

// check contentLength and content type
if (contentLength && isValidContentType) {

// Check if there is enough to OTA Update
bool canBegin = Update.begin(contentLength);

// If yes, begin
if (canBegin) {
  Serial.println("Begin OTA. This may take 2 - 5 mins to complete. Things might be quite for a while.. Patience!");
  // No activity would appear on the Serial monitor
  // So be patient. This may take 2 - 5mins to complete
  size_t written = Update.writeStream(client);

  if (written == contentLength) {
    Serial.println("Written : " + String(written) + " successfully");
  } else {
    Serial.println("Written only : " + String(written) + "/" + String(contentLength) + ". Retry?" );
    // retry??
    // execOTA();
  }

  if (Update.end()) {
    Serial.println("OTA done!");
    if (Update.isFinished()) {
      Serial.println("Update successfully completed. Rebooting.");
      ESP.restart();
    } else {
      Serial.println("Update not finished? Something went wrong!");
    }
  } else {
    Serial.println("Error Occurred. Error #: " + String(Update.getError()));
  }
} else {
  // not enough space to begin OTA
  // Understand the partitions and
  // space availability
  Serial.println("Not enough space to begin OTA");
  client.flush();
}

} else {

Serial.println("There was no content in the response");
client.flush();

}
}

void setup() {
//Begin Serial
Serial.begin(115200);
delay(10);

Serial.println("Connecting to " + String(SSID));

// Connect to provided SSID and PSWD
WiFi.begin(SSID, PSWD);

// Wait for connection to establish
while (WiFi.status() != WL_CONNECTED) {

Serial.print("."); // Keep the serial monitor lit!
delay(500);

}

// Connection Succeed
Serial.println("");
Serial.println("Connected to " + String(SSID));

// Execute OTA Update
execOTA();
}

void loop() {
// chill
}

/*

  • Serial Monitor log for this sketch
  • If the OTA succeeded, it would load the preference sketch, with a small modification. i.e.
  • Print OTA Update succeeded!! This is an example sketch : Preferences > StartCounter
  • And then keeps on restarting every 10 seconds, updating the preferences
  •  rst:0x10 (RTCWDT_RTC_RESET),boot:0x13 (SPI_FAST_FLASH_BOOT)
     configsip: 0, SPIWP:0x00
     clk_drv:0x00,q_drv:0x00,d_drv:0x00,cs0_drv:0x00,hd_drv:0x00,wp_drv:0x00
     mode:DIO, clock div:1
     load:0x3fff0008,len:8
     load:0x3fff0010,len:160
     load:0x40078000,len:10632
     load:0x40080000,len:252
     entry 0x40080034
     Connecting to SSID
     ......
     Connected to SSID
     Connecting to: bucket-name.s3.ap-south-1.amazonaws.com
     Fetching Bin: /StartCounter.ino.bin
     Got application/octet-stream payload.
     Got 357280 bytes from server
     contentLength : 357280, isValidContentType : 1
     Begin OTA. This may take 2 - 5 mins to complete. Things might be quite for a while.. Patience!
     Written : 357280 successfully
     OTA done!
     Update successfully completed. Rebooting.
     ets Jun  8 2016 00:22:57
     
     rst:0x10 (RTCWDT_RTC_RESET),boot:0x13 (SPI_FAST_FLASH_BOOT)
     configsip: 0, SPIWP:0x00
     clk_drv:0x00,q_drv:0x00,d_drv:0x00,cs0_drv:0x00,hd_drv:0x00,wp_drv:0x00
     mode:DIO, clock div:1
     load:0x3fff0008,len:8
     load:0x3fff0010,len:160
     load:0x40078000,len:10632
     load:0x40080000,len:252
     entry 0x40080034
     
     OTA Update succeeded!! This is an example sketch : Preferences > StartCounter
     Current counter value: 1
     Restarting in 10 seconds...
     E (102534) wifi: esp_wifi_stop 802 wifi is not init
     ets Jun  8 2016 00:22:57
     
     rst:0x10 (RTCWDT_RTC_RESET),boot:0x13 (SPI_FAST_FLASH_BOOT)
     configsip: 0, SPIWP:0x00
     clk_drv:0x00,q_drv:0x00,d_drv:0x00,cs0_drv:0x00,hd_drv:0x00,wp_drv:0x00
     mode:DIO, clock div:1
     load:0x3fff0008,len:8
     load:0x3fff0010,len:160
     load:0x40078000,len:10632
     load:0x40080000,len:252
     entry 0x40080034
     
     OTA Update succeeded!! This is an example sketch : Preferences > StartCounter
     Current counter value: 2
     Restarting in 10 seconds...
    
     ....

从《天道》的角度谈谈产品规划
原创2023-02-24 21:05·产品人卫朋
今天主要借用《天道》中丁元英的商业案例来谈谈产品规划这个话题。

《天道》这部被众人追捧的影视剧来源于豆豆的成名作《遥远的救世主》。

如果没有全局做过产品或者市场的规划,而且是初次接触这部剧。

你就会惊叹于主人公的组局、布局,以及成局的能力。

从互联网拥簇的评论声中,也可见一斑。

剧中的丁元英甚至都有一种被神化的趋势。

而随着个人知识和阅历的增加,再加上每年也都要做产品规划。

也逐渐对这部剧或者这本书有了一些新的认识。

究其本质,这是一种战略性的思维,也是一种规划的能力。

更是一种市场与内部能力的匹配过程。

笔者之前也分享过这块的内容,也看到了一些质疑。

怎么能用虚拟的案例做讲解呢?

其实这么做的原因主要有两点考虑:

首先,这部剧中的商业案例的整体逻辑是自洽的,而且也符合当时的商业环境。

其次,整部剧将整个商业案例完整地呈现了出来,也包括其中很多的决策细节。

这就要比分析现实案例直观得多,也更加有指导意义。

再回到产品规划这个话题上来。

产品规划从本质上来说是一种推演能力,也就是根据第一性原则推演产品从0到1、从1到100的一个过程。

如果说一款产品是一个点的话,那产品规划便是通过构造一种系统能力以达成企业最终的商业目的。

第一性原理是埃隆·马斯克非常推崇的一种思维模型。

通常来说,企业愿景对应的便是企业的第一性原则。

围绕第一性原则可以激发资源优势、制定细分市场目标,最终实现企业目标。

下面以影视剧中丁元英操盘的格律诗音响项目为例,谈谈产品规划。

格律诗音响公司的企业愿景是实现王庙村生产力和市场的对接,最终实现农户脱贫。

这是企业的愿景,同时也是丁元英承诺要给红颜知己芮小丹创造的神话。

启动一个项目或产品,资源和人力配置是你首先要考虑的。

企业在不同的发展周期,对人的要求是有很大差异的。

丁元英在分析完这些人的本质之后,并没有把自己的全套计划完整地告诉原始这些人。

而是通过市场的变化来淘汰掉一部分人。

因为这部分人现在不淘汰掉,在以后的市场变化中,可能会给公司带来毁灭性的灾难。

下面就先梳理一下其中的关键人物:

丁元英作为格律诗音响项目的唯一操盘手,全局规划了整个项目。

他的优势是自己在欧洲的人脉和战略规划能力,以及在欧阳雪等人心中的影响力。

同时,作为发烧级音乐玩家,他对音箱的独特见解也为他们打造差异化的产品起到了关键助力作用。

差异化的意思是相比于竞争对手,你的独特优势或者护城河,没有这个前提,整个策略也就无从谈起,这为他们赢得了时间上的先机。

在音响这个市场,竞品已经很成功了,而且他们提供的价值点已经被用户接受。

如果按照他们的价值点去做产品,你就永远只能跟在他们身后。

这时候就需要找一个跟他们不一样的价值点,做差异化。

欧阳雪这个人呢,做事很踏实,很讲义气,不贪心。优势是人脉、资金和社会地位。

这个人的价值在于她对格律诗的绝对控股,这样就可以确保关键决策权的归属。

由于每个人的认知水平的限制,很多时候不同个体看到的终局是有极大差异的,这个时候你就需要考虑如何增加成事的确定性。

如果开公司的话,股权的分配问题是你优先要考虑的。

不赚钱的时候,大家还都能力出一孔。一旦公司有起色,每个人就开始有自己的诉求,不确定性也就随之而来。

肖亚文见过世面,知道公司怎么运行,知道商务谈判和商务合作的事情,是很精明的职场人物。

而冯世杰和叶晓明想成就一番事业,但没有机会,能够脚踏实地的做事情,但眼光欠缺。刘冰是小人物,唯利是图,关键时刻不能顶上,迟早会被淘汰。

叶晓明,冯世杰,刘冰这三个人的优势就是懂音乐,会组装,可以作为高级技术工。

同时,这三人和王庙村农民有一定的关系,可以作为连接的纽带,核心竞争力是技术和人脉。

乐圣公司的掌舵人是林雨峰(竞争对手),但太过刚硬,只知道进攻,不懂防守,考虑问题存在漏洞。

这就有点类似竞争分析了,通过分析竞争对手的漏洞,找到破局点,制定商业竞争策略。

接下来就需要统一思想了:

想要以小博大,达成乐圣跟王庙村合作的目的,就必须把优势发挥到最大效果。

这才有几次股东开会,召集农民兄弟一起开会等,就是为了统一思想。

市场的生存竞争非常残酷,胜负往往就在毫厘之间,微弱的优势都可能成为关键一环,你比他多一口气,你就是赢家。

最后,丁元英就把这些人的优势资源整合起来,按照需要组建公司,精心规划。

详细案例分析可以参阅笔者之前的文章。

卫朋

人人都是产品经理受邀专栏作家,**** 嵌入式领域新星创作者、资深技术博主。2020 年 8 月开始写产品相关内容,截至目前,人人都是产品经理单渠道阅读 56 万+,鸟哥笔记单渠道阅读200 万+,**** 单渠道阅读 210 万+,51CTO单渠道阅读 180 万+。

卫朋入围2021/2022年人人都是产品经理平台年度作者,光环国际学习社区首批原创者、知识合作伙伴,商业新知 2021 年度产品十佳创作者,腾讯调研云2022年达人榜第三名。

文章被人人都是产品经理、****、华为云、运营派、产品壹佰、鸟哥笔记、光环国际、商业新知、腾讯调研云等头部垂直类媒体转载。文章见仁见智,各位看官可策略性选择对于自己有用的部分。

推荐阅读