543 lines
13 KiB
C++
543 lines
13 KiB
C++
/*
|
|
esp32 firmware OTA
|
|
Date: December 2018
|
|
Author: Chris Joyce <https://chrisjoyce911/esp32FOTA>
|
|
Purpose: Perform an OTA update from a bin located on a webserver (HTTP Only)
|
|
|
|
Modifications Dec 2019:
|
|
RLJ Added usage of AsyncHTTPrequest to avoid hang issues with flaky internet connections during update poll
|
|
However using AsyncTCP for the actual binary update causes other issues in the callback realm,
|
|
so persisting with the original synchronous update method which blocks all user mode code.
|
|
Modifications Mar 2020:
|
|
RLJ Added FreeRTOS queue to separate callbacks from potential system calls - random reboots in some afterburners...
|
|
*/
|
|
|
|
#include <Arduino.h>
|
|
#include "esp32fota.h"
|
|
#include <WiFi.h>
|
|
#include <HTTPClient.h>
|
|
#include <Update.h>
|
|
#include "../../ArduinoJson/ArduinoJson.h"
|
|
#include "../../asyncHTTPrequest/src/asyncHTTPrequest.h"
|
|
|
|
extern void forceBootInit();
|
|
|
|
#define USE_QUEUE
|
|
|
|
|
|
esp32FOTA::esp32FOTA(String firwmareType, int firwmareVersion, bool isBeta)
|
|
{
|
|
_firwmareType = firwmareType;
|
|
_firwmareVersion = firwmareVersion;
|
|
_bIsBeta = isBeta;
|
|
useDeviceID = false;
|
|
// _endCallback = NULL;
|
|
_queue = xQueueCreate(1, 256);
|
|
}
|
|
|
|
// Utility to extract header value from headers
|
|
String
|
|
esp32FOTA::getHeaderValue(String header, String headerName)
|
|
{
|
|
return header.substring(strlen(headerName.c_str()));
|
|
}
|
|
|
|
// OTA Logic
|
|
void
|
|
esp32FOTA::execOTA()
|
|
{
|
|
|
|
WiFiClient client;
|
|
int contentLength = 0;
|
|
bool isValidContentType = false;
|
|
|
|
Serial.println("Connecting to: " + String(_host));
|
|
// Connect to Webserver
|
|
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");
|
|
|
|
unsigned long timeout = millis();
|
|
while (client.available() == 0)
|
|
{
|
|
if (millis() - timeout > 5000)
|
|
{
|
|
Serial.println("Client Timeout !");
|
|
client.stop();
|
|
return;
|
|
}
|
|
}
|
|
|
|
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 (!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 webserver 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 ( _onComplete != NULL) {
|
|
if(!_onComplete(contentLength)) {
|
|
Serial.println("ESP32FOTA: OnComplete handler returned false");
|
|
Update.abort();
|
|
}
|
|
}
|
|
|
|
if (Update.end())
|
|
{
|
|
Serial.println("OTA done!");
|
|
if (Update.isFinished())
|
|
{
|
|
Serial.println("Update successfully completed. Rebooting.");
|
|
if(_onSuccess != NULL) {
|
|
_onSuccess();
|
|
}
|
|
ESP.restart();
|
|
}
|
|
else
|
|
{
|
|
if(_onFail != NULL) {
|
|
_onFail();
|
|
}
|
|
Serial.println("Update not finished? Something went wrong!");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if(_onFail != NULL) {
|
|
_onFail();
|
|
}
|
|
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();
|
|
}
|
|
}
|
|
|
|
// Synchronous mode update check - may hang on flakey Internet connections
|
|
bool
|
|
esp32FOTA::execHTTPcheck()
|
|
{
|
|
|
|
String useURL;
|
|
|
|
if (useDeviceID)
|
|
{
|
|
// String deviceID = getDeviceID() ;
|
|
useURL = _checkURL + "?id=" + getDeviceID();
|
|
}
|
|
else
|
|
{
|
|
useURL = _checkURL;
|
|
}
|
|
|
|
WiFiClient client;
|
|
_port = 80;
|
|
|
|
Serial.println("Getting HTTP");
|
|
Serial.println(useURL);
|
|
Serial.println("------");
|
|
if ((WiFi.status() == WL_CONNECTED))
|
|
{ //Check the current connection status
|
|
|
|
HTTPClient http;
|
|
|
|
http.begin(useURL); //Specify the URL
|
|
int httpCode = http.GET(); //Make the request
|
|
|
|
if (httpCode == 200)
|
|
{ //Check is a file was returned
|
|
String payload = http.getString();
|
|
|
|
int str_len = payload.length() + 1;
|
|
char* JSONMessage = new char[str_len];
|
|
payload.toCharArray(JSONMessage, str_len);
|
|
|
|
StaticJsonBuffer<300> JSONBuffer; //Memory pool
|
|
JsonObject &parsed = JSONBuffer.parseObject(JSONMessage); //Parse message
|
|
|
|
if (!parsed.success())
|
|
{ //Check for errors in parsing
|
|
delete[] JSONMessage;
|
|
Serial.println("Parsing failed");
|
|
delay(5000);
|
|
http.end(); //Free the resources
|
|
return false;
|
|
}
|
|
|
|
const char *pltype = parsed["type"];
|
|
int plversion = parsed["version"];
|
|
const char *plhost = parsed["host"];
|
|
_port = parsed["port"];
|
|
const char *plbin = parsed["bin"];
|
|
|
|
String jshost(plhost);
|
|
String jsbin(plbin);
|
|
|
|
_host = jshost;
|
|
_bin = jsbin;
|
|
|
|
String fwtype(pltype);
|
|
|
|
delete[] JSONMessage;
|
|
|
|
if(fwtype == _firwmareType) {
|
|
if(_bIsBeta) { // if beta version being used, check for equal version number
|
|
if (plversion >= _firwmareVersion)
|
|
{
|
|
_newVersion = plversion;
|
|
http.end(); //Free the resources
|
|
return true;
|
|
}
|
|
}
|
|
else {
|
|
if (plversion > _firwmareVersion)
|
|
{
|
|
_newVersion = plversion;
|
|
http.end(); //Free the resources
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
_newVersion = 0;
|
|
|
|
// if (plversion > _firwmareVersion && fwtype == _firwmareType)
|
|
// {
|
|
// _newVersion = plversion;
|
|
// return true;
|
|
// }
|
|
// else
|
|
// {
|
|
// _newVersion = 0;
|
|
// return false;
|
|
// }
|
|
|
|
}
|
|
|
|
else
|
|
{
|
|
Serial.println("Error on HTTP request");
|
|
// return false;
|
|
}
|
|
|
|
http.end(); //Free the resources
|
|
}
|
|
return false;
|
|
}
|
|
|
|
String
|
|
esp32FOTA::getDeviceID()
|
|
{
|
|
char deviceid[21];
|
|
uint64_t chipid;
|
|
chipid = ESP.getEfuseMac();
|
|
sprintf(deviceid, "%" PRIu64, chipid);
|
|
String thisID(deviceid);
|
|
return thisID;
|
|
}
|
|
|
|
/**
|
|
* onProgress, set a callback called during upload, passed directly thru to OTA Updater class
|
|
* @access public
|
|
* @param {[type]} void (*func)(size_t, size_t)
|
|
*/
|
|
void
|
|
esp32FOTA::onProgress( std::function<void(size_t, size_t)> func ) {
|
|
Update.onProgress(func);
|
|
}
|
|
|
|
|
|
/**
|
|
* onComplete, set a callback after upload is completed, but not yet verified
|
|
* @access public
|
|
* @param {[type]} void (*func)(void)
|
|
*/
|
|
void
|
|
esp32FOTA::onComplete( std::function<bool(int)> func ) {
|
|
_onComplete = func;
|
|
}
|
|
|
|
/**
|
|
* onSuccess, set a callback after upload is fully completed and verified
|
|
* @access public
|
|
* @param {[type]} void (*func)(void)
|
|
*/
|
|
void
|
|
esp32FOTA::onSuccess( std::function<void()> func ) {
|
|
_onSuccess = func;
|
|
}
|
|
|
|
/**
|
|
* onComplete, set a callback after upload is completed, but not yet verified
|
|
* @access public
|
|
* @param {[type]} void (*func)(void)
|
|
*/
|
|
void
|
|
esp32FOTA::onFail( std::function<void()> func ) {
|
|
_onFail = func;
|
|
}
|
|
|
|
|
|
|
|
|
|
// Callback for when AsyncTCP ready state changes
|
|
// queue data to be processed later in user loop
|
|
void FOTA_PollCallback(void* optParm, asyncHTTPrequest* pRequest, int readyState)
|
|
{
|
|
if(readyState == 4) { // response
|
|
|
|
|
|
#ifdef USE_QUEUE
|
|
esp32FOTA* pFOTA = (esp32FOTA*)optParm;
|
|
pFOTA->queueDLdata(pRequest);
|
|
#else
|
|
String JSONinfo(pRequest->responseText());
|
|
Serial.println(JSONinfo);
|
|
Serial.println();
|
|
esp32FOTA* pFOTA = (esp32FOTA*) optParm;
|
|
if(pFOTA) {
|
|
if(pFOTA->decodeResponse(JSONinfo)) {
|
|
}
|
|
}
|
|
#endif
|
|
|
|
}
|
|
if(readyState == 1) { // connection established
|
|
pRequest->send();
|
|
}
|
|
}
|
|
|
|
void
|
|
esp32FOTA::setCheckURL(const char* host)
|
|
{
|
|
_checkURL = host;
|
|
}
|
|
|
|
void
|
|
esp32FOTA::setupAsync(const char* host)
|
|
{
|
|
#ifdef DEBUG_ASYNC_FOTA
|
|
_versionTest.setDebug(true);
|
|
#endif
|
|
}
|
|
|
|
// Asynchronous update check - performs more reliably with flakey Internet connections
|
|
void
|
|
esp32FOTA::execAsyncHTTPcheck()
|
|
{
|
|
_newVersion = 0;
|
|
if ((WiFi.status() == WL_CONNECTED)) {
|
|
if(_versionTest.readyState() == 0 || _versionTest.readyState() == 4) {
|
|
Serial.println("Querying firmware update server");
|
|
_versionTest.setTimeout(10);
|
|
_versionTest.onReadyStateChange(FOTA_PollCallback, this);
|
|
_versionTest.onBuildHeaders(NULL);
|
|
_versionTest.onData(NULL);
|
|
_versionTest.open("GET", _checkURL.c_str());
|
|
}
|
|
}
|
|
else {
|
|
Serial.println("Firmware update check skipped = no STA");
|
|
}
|
|
}
|
|
|
|
|
|
// wrapper function to allow use of String
|
|
bool
|
|
esp32FOTA::decodeResponse(String payload)
|
|
{
|
|
int str_len = payload.length() + 1;
|
|
char* JSONMessage = new char[str_len];
|
|
payload.toCharArray(JSONMessage, str_len);
|
|
|
|
bool retval = decodeResponse(JSONMessage);
|
|
|
|
delete[] JSONMessage;
|
|
|
|
return retval;
|
|
}
|
|
|
|
|
|
bool
|
|
esp32FOTA::decodeResponse(char* resp)
|
|
{
|
|
StaticJsonBuffer<300> JSONBuffer; //Memory pool
|
|
JsonObject &parsed = JSONBuffer.parseObject(resp); //Parse message
|
|
|
|
if (!parsed.success())
|
|
{ //Check for errors in parsing
|
|
Serial.println("FOTA Parsing failed\r\n");
|
|
return false;
|
|
}
|
|
|
|
// extract from expected JSON fields
|
|
const char *pltype = parsed["type"]; // update type
|
|
int plversion = parsed["version"]; // version number
|
|
const char *plhost = parsed["host"];
|
|
const char *plbin = parsed["bin"]; // filename
|
|
|
|
String fwtype(pltype);
|
|
_host = plhost; // host that holds new firmware
|
|
_port = parsed["port"]; // port to use
|
|
_bin = plbin;
|
|
|
|
|
|
if(fwtype == _firwmareType) {
|
|
if(_bIsBeta) { // if beta version being used, check for equal version number
|
|
if (plversion >= _firwmareVersion)
|
|
{
|
|
_newVersion = plversion;
|
|
return true;
|
|
}
|
|
}
|
|
else {
|
|
if (plversion > _firwmareVersion)
|
|
{
|
|
_newVersion = plversion;
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
_newVersion = 0;
|
|
return false;
|
|
|
|
// if (plversion > _firwmareVersion && fwtype == _firwmareType)
|
|
// {
|
|
// _newVersion = plversion;
|
|
// return true;
|
|
// }
|
|
// else
|
|
// {
|
|
// _newVersion = 0;
|
|
// return false;
|
|
// }
|
|
}
|
|
|
|
void
|
|
esp32FOTA::queueDLdata(asyncHTTPrequest* pRequest)
|
|
{
|
|
sFOTAqueue entry;
|
|
|
|
int len = pRequest->available();
|
|
if(len <= sizeof(sFOTAqueue::data)) {
|
|
entry.len = len;
|
|
pRequest->responseRead(entry.data, len);
|
|
BaseType_t awoken;
|
|
xQueueSendFromISR(_queue, &entry, &awoken);
|
|
}
|
|
}
|
|
|
|
// routine called regularly by the "loop" task - ie not IRQL
|
|
// it is not safe to do system things in the AsyncTCP callbacks!
|
|
void
|
|
esp32FOTA::process()
|
|
{
|
|
sFOTAqueue entry;
|
|
if(xQueueReceive(_queue, &entry, 0)) {
|
|
int16_t len = entry.len;
|
|
|
|
char working[256];
|
|
memcpy(working, entry.data, 255);
|
|
working[len] = 0;
|
|
String JSONinfo(working);
|
|
Serial.println(JSONinfo);
|
|
Serial.println();
|
|
decodeResponse(JSONinfo);
|
|
}
|
|
}
|
|
|