Skip to content

Commit 270da98

Browse files
Merge pull request #77 from thorrak/exp_multi_type_manifest
Allow multiple firmware types in a single JSON manifest
2 parents 0f8c618 + e7ef791 commit 270da98

File tree

3 files changed

+130
-90
lines changed

3 files changed

+130
-90
lines changed

README.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,24 @@ Version information can be either a single number or a semantic version string.
5858
}
5959
```
6060

61+
A single JSON file can provide information on multiple firmware types by combining them together into an array. When this is loaded, the firmware manifest with a type matching the one passed to the esp32FOTA constructor will be selected:
62+
63+
```json
64+
[
65+
{
66+
"type":"esp32-fota-http",
67+
"version":"0.0.2",
68+
"url":"http://192.168.0.100/fota/esp32-fota-http-2.bin"
69+
},
70+
{
71+
"type":"esp32-other-hardware",
72+
"version":"0.0.3",
73+
"url":"http://192.168.0.100/fota/esp32-other-hardware.bin"
74+
}
75+
]
76+
```
77+
78+
6179
#### Firmware types
6280

6381
Types are used to compare with the current loaded firmware, this is used to make sure that when loaded, the device will still do the intended job.

src/esp32fota.cpp

Lines changed: 110 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -317,9 +317,63 @@ void esp32FOTA::execOTA()
317317
}
318318
}
319319

320+
bool esp32FOTA::checkJSONManifest(JsonVariant JSONDocument) {
321+
322+
if(strcmp(JSONDocument["type"].as<const char *>(), _firmwareType.c_str()) != 0) {
323+
log_i("Payload type in manifest %s doesn't match current firmware %s", JSONDocument["type"].as<const char *>(), _firmwareType.c_str() );
324+
log_i("Doesn't match type: %s", _firmwareType.c_str() );
325+
return false; // Move to the next entry in the manifest
326+
}
327+
log_i("Payload type in manifest %s matches current firmware %s", JSONDocument["type"].as<const char *>(), _firmwareType.c_str() );
328+
329+
semver_free(&_payloadVersion);
330+
if(JSONDocument["version"].is<uint16_t>()) {
331+
log_i("JSON version: %d (int)", JSONDocument["version"].as<uint16_t>());
332+
_payloadVersion = semver_t {JSONDocument["version"].as<uint16_t>()};
333+
} else if (JSONDocument["version"].is<const char *>()) {
334+
log_i("JSON version: %s (semver)", JSONDocument["version"].as<const char *>() );
335+
if (semver_parse(JSONDocument["version"].as<const char *>(), &_payloadVersion)) {
336+
log_e( "Invalid semver string received in manifest. Defaulting to 0" );
337+
_payloadVersion = semver_t {0};
338+
}
339+
} else {
340+
log_e( "Invalid semver format received in manifest. Defaulting to 0" );
341+
_payloadVersion = semver_t {0};
342+
}
343+
344+
char version_no[256] = {'\0'};
345+
semver_render(&_payloadVersion, version_no);
346+
log_i("Payload firmware version: %s", version_no );
347+
348+
349+
if(JSONDocument["url"].is<String>()) {
350+
// We were provided a complete URL in the JSON manifest - use it
351+
_firmwareUrl = JSONDocument["url"].as<String>();
352+
if(JSONDocument["host"].is<String>()) // If the manifest provides both, warn the user
353+
log_w("Manifest provides both url and host - Using URL");
354+
} else if (JSONDocument["host"].is<String>() && JSONDocument["port"].is<uint16_t>() && JSONDocument["bin"].is<String>()){
355+
// We were provided host/port/bin format - Build the URL
356+
if( JSONDocument["port"].as<uint16_t>() == 443 || JSONDocument["port"].as<uint16_t>() == 4433 )
357+
_firmwareUrl = String( "https://");
358+
else
359+
_firmwareUrl = String( "http://" );
360+
361+
_firmwareUrl += JSONDocument["host"].as<String>() + ":" + String( JSONDocument["port"].as<uint16_t>() ) + JSONDocument["bin"].as<String>();
362+
363+
} else {
364+
// JSON was malformed - no firmware target was provided
365+
log_e("JSON manifest was missing both 'url' and 'host'/'port'/'bin' keys");
366+
return false;
367+
}
368+
369+
if (semver_compare(_payloadVersion, _firmwareVersion) == 1) {
370+
return true;
371+
}
372+
return false;
373+
}
374+
320375
bool esp32FOTA::execHTTPcheck()
321376
{
322-
323377
String useURL;
324378

325379
if (useDeviceID)
@@ -334,112 +388,78 @@ bool esp32FOTA::execHTTPcheck()
334388

335389
log_i("Getting HTTP: %s",useURL.c_str());
336390
log_i("------");
337-
if ((WiFi.status() == WL_CONNECTED)) { //Check the current connection status
338-
339-
HTTPClient http;
340-
WiFiClientSecure client;
341-
342-
if( useURL.substring( 0, 5 ) == "https" ) {
343-
if (!_allow_insecure_https) {
344-
// If the checkURL is https load the root-CA and connect with that
345-
log_i( "Loading root_ca.pem" );
346-
File root_ca_file = SPIFFS.open( "/root_ca.pem" );
347-
if( !root_ca_file ) {
348-
log_e( "Could not open root_ca.pem" );
349-
return false;
350-
}
351-
{
352-
std::string root_ca = "";
353-
while( root_ca_file.available() ){
354-
root_ca.push_back( root_ca_file.read() );
355-
}
356-
root_ca_file.close();
357-
http.begin( useURL, root_ca.c_str() );
358-
}
359-
} else {
360-
// We're downloading from a secure port, but we don't want to validate the root cert.
361-
client.setInsecure();
362-
http.begin(client, useURL);
363-
}
364-
} else {
365-
http.begin(useURL); //Specify the URL
366-
}
367-
int httpCode = http.GET(); //Make the request
368-
369-
if( httpCode == HTTP_CODE_OK || httpCode == HTTP_CODE_MOVED_PERMANENTLY ) { //Check is a file was returned
370-
371-
String payload = http.getString();
372-
373-
int str_len = payload.length() + 1;
374-
char JSONMessage[str_len];
375-
payload.toCharArray(JSONMessage, str_len);
391+
if ((WiFi.status() != WL_CONNECTED)) { //Check the current connection status
392+
log_w("WiFi not connected - skipping HTTP check");
393+
return false; // WiFi not connected
394+
}
376395

377-
StaticJsonDocument<300> JSONDocument; //Memory pool
378-
DeserializationError err = deserializeJson(JSONDocument, JSONMessage);
396+
HTTPClient http;
397+
WiFiClientSecure client;
379398

380-
if (err) { //Check for errors in parsing
381-
log_e("Parsing failed");
382-
http.end();
399+
if( useURL.substring( 0, 5 ) == "https" ) {
400+
if (!_allow_insecure_https) {
401+
// If the checkURL is https load the root-CA and connect with that
402+
log_i( "Loading root_ca.pem" );
403+
File root_ca_file = SPIFFS.open( "/root_ca.pem" );
404+
if( !root_ca_file ) {
405+
log_e( "Could not open root_ca.pem" );
383406
return false;
384407
}
385-
386-
const char *pltype = JSONDocument["type"];
387-
388-
semver_free(&_payloadVersion);
389-
if(JSONDocument["version"].is<uint16_t>()) {
390-
log_i("JSON version: %d (int)", JSONDocument["version"].as<uint16_t>());
391-
_payloadVersion = semver_t {JSONDocument["version"].as<uint16_t>()};
392-
} else if (JSONDocument["version"].is<const char *>()) {
393-
log_i("JSON version: %s (semver)", JSONDocument["version"].as<const char *>() );
394-
if (semver_parse(JSONDocument["version"].as<const char *>(), &_payloadVersion)) {
395-
log_e( "Invalid semver string received in manifest. Defaulting to 0" );
396-
_payloadVersion = semver_t {0};
408+
{
409+
std::string root_ca = "";
410+
while( root_ca_file.available() ){
411+
root_ca.push_back( root_ca_file.read() );
397412
}
398-
} else {
399-
log_e( "Invalid semver format received in manifest. Defaulting to 0" );
400-
_payloadVersion = semver_t {0};
413+
root_ca_file.close();
414+
http.begin( useURL, root_ca.c_str() );
401415
}
416+
} else {
417+
// We're downloading from a secure port, but we don't want to validate the root cert.
418+
client.setInsecure();
419+
http.begin(client, useURL);
420+
}
421+
} else {
422+
http.begin(useURL); //Specify the URL
423+
}
424+
int httpCode = http.GET(); //Make the request
402425

403-
char version_no[256] = {'\0'};
404-
semver_render(&_payloadVersion, version_no);
405-
log_i("Payload firmware version: %s", version_no );
406-
426+
if( httpCode == HTTP_CODE_OK || httpCode == HTTP_CODE_MOVED_PERMANENTLY ) { //Check is a file was returned
407427

408-
if(JSONDocument["url"].is<String>()) {
409-
// We were provided a complete URL in the JSON manifest - use it
410-
_firmwareUrl = JSONDocument["url"].as<String>();
411-
if(JSONDocument["host"].is<String>()) // If the manifest provides both, warn the user
412-
log_w("Manifest provides both url and host - Using URL");
413-
} else if (JSONDocument["host"].is<String>() && JSONDocument["port"].is<uint16_t>() && JSONDocument["bin"].is<String>()){
414-
// We were provided host/port/bin format - Build the URL
415-
if( JSONDocument["port"].as<uint16_t>() == 443 || JSONDocument["port"].as<uint16_t>() == 4433 )
416-
_firmwareUrl = String( "https://");
417-
else
418-
_firmwareUrl = String( "http://" );
428+
String payload = http.getString();
419429

420-
_firmwareUrl += JSONDocument["host"].as<String>() + ":" + String( JSONDocument["port"].as<uint16_t>() ) + JSONDocument["bin"].as<String>();
430+
int str_len = payload.length() + 1;
431+
char JSONMessage[str_len];
432+
payload.toCharArray(JSONMessage, str_len);
421433

422-
} else {
423-
// JSON was malformed - no firmware target was provided
424-
log_e("JSON manifest was missing both 'url' and 'host'/'port'/'bin' keys");
425-
http.end();
426-
return false;
427-
}
434+
DynamicJsonDocument JSONResult(2048);
435+
DeserializationError err = deserializeJson(JSONResult, JSONMessage);
428436

437+
http.end(); // We're done with HTTP - free the resources
429438

430-
String fwtype(pltype);
439+
if (err) { //Check for errors in parsing
440+
log_e("Parsing failed");
441+
return false;
442+
}
431443

432-
if (semver_compare(_payloadVersion, _firmwareVersion) == 1 && fwtype == _firmwareType) {
433-
http.end();
434-
return true;
444+
if (JSONResult.is<JsonArray>()) {
445+
// We already received an array of multiple firmware types
446+
JsonArray arr = JSONResult.as<JsonArray>();
447+
for (JsonVariant JSONDocument : arr) {
448+
if(checkJSONManifest(JSONDocument)) {
449+
return true;
450+
}
435451
}
436-
} else {
437-
log_e("Error on HTTP request");
452+
} else if (JSONResult.is<JsonObject>()) {
453+
if(checkJSONManifest(JSONResult.as<JsonVariant>()))
454+
return true;
438455
}
439-
http.end(); //Free the resources
456+
457+
return false; // We didn't get a hit against the above, return false
458+
} else {
459+
log_e("Error on HTTP request");
460+
http.end();
440461
return false;
441462
}
442-
return false;
443463
}
444464

445465
String esp32FOTA::getDeviceID()

src/esp32fota.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
#define esp32fota_h
2020

2121
#include <Arduino.h>
22+
#include <ArduinoJson.h>
2223
#include "semver/semver.h"
2324

2425
class esp32FOTA
@@ -46,6 +47,7 @@ class esp32FOTA
4647
String _firmwareUrl;
4748
boolean _check_sig;
4849
boolean _allow_insecure_https;
50+
bool checkJSONManifest(JsonVariant JSONDocument);
4951

5052
};
5153

0 commit comments

Comments
 (0)