I'm trying to achieve a following method to update 2 cpu's their firmware;
As you can see, the master has internet, but the slave does not. Which means that I need some sort of "over the air" update mechanism. For the master side, this works, but could be faster in my opinion. For the slave, I have a semi-working solution, but it's painfully slow and stops after a time (never succeeded the update on slave side).
So the main idea was on the ESP to make use of the SPIFFS
to fetch the binary to, and then read from there the binary to the slave.
Currently it takes 15-35 minutes to download a 1MB firmware binary from an S3 endpoint (over https), which is in my opinion way to slow, but I could live with that, if all the rest works perfectly fine and the updates always succeed.
My current code for the master is the following:
#include <WiFi.h>
#include <AsyncTCP.h>
#include <ETH.h>
#include <HTTPClient.h>
#include <WiFiClientSecure.h>
#include <ArduinoJson.h>
#include "FS.h"
#include "SPIFFS.h"
#include <Update.h>
HTTPClient http;
void setup() {
Serial.begin(115200); // debug
Serial1.begin(921600, SERIAL_8N1, RX_PIN, TX_PIN); // to communicate with the slave
ETH.begin(ETH_ADDR, ETH_POWER_PIN, ETH_MDC_PIN, ETH_MDIO_PIN, ETH_TYPE, ETH_CLK_MODE);
SPIFFS.begin(true);
}
void doUpdate() {
http.begin("https://example.com/path/to/get/bin");
http.addHeader("Content-Type", "application/json");
http.GET();
StaticJsonDocument<192> doc;
DeserializationError error = deserializeJson(doc, http.getString());
if (error) {
Serial.print("deserializeJson() failed: ");
Serial.println(error.c_str());
}
http.end();
String host = doc["host"].as<String>();
String binary_path = doc["binary"].as<String>();
String url = "https://" + host + binary_path;
// If the file exists, we need to remove it first
if (SPIFFS.exists("/newFirmware.bin")) {
SPIFFS.remove("/newFirmware.bin");
Serial.println("Removed file");
}
File f = SPIFFS.open("/newFirmware.bin", "w");
if (f) {
http.begin(url);
int httpCode = http.GET();
if (httpCode > 0) {
if (httpCode == HTTP_CODE_OK) {
Serial.println("Downloading firmware..."); // this stage takes 15-35 (or more) minutes, for 1MB....
http.writeToStream(&f);
}
} else {
Serial.printf("[HTTP] GET... failed, error: %s\n", http.errorToString(httpCode).c_str());
}
f.close();
} else {
Serial.println("Could not open file");
return;
}
http.end();
// checks here if it's master or slave (hidden here)
// send to slave is below
// make payload that the slave will be able to decode and get ready to start the update
StaticJsonDocument<48> doc;
doc.clear(); // release the memory
String jsonDocument;
File readFile = SPIFFS.open("/newFirmware.bin");
if (!readFile){
Serial.println("Failed to open file for reading");
return;
}
doc["action"] = "FETCH_UPDATE";
serializeJson(doc, jsonDocument);
Serial1.write(jsonDocument.c_str());
Serial1.write("\n");
// wait for slave confirmation to start sending the data
while(true) {
// read a buffer and a size
uint8_t buf[1025] = {};
size_t _len = 256;
Serial1.readBytes(buf, _len);
// 011000010110001101101011 -> "ack"
if (strcmp((char*)buf, "011000010110001101101011") == 0) {
Serial.println("Received ack from slave");
break;
}
}
int len = 0;
int total = 0;
uint8_t buf[1025] = {};
size_t _len = 256;
do {
len = readFile.read(buf, _len); // get the bytes that need to be sent
Serial.printf("Serial1 write size: %d, %d\n", Serial1.write(buf, len), total);
total += len;
Serial1.write(buf, len); // writes to the slave
// wait for ack from slave
while (true) {
// read the slave the whole time.
uint8_t buf[1025] = {};
size_t _len = 256;
Serial1.readBytes(buf, _len);
// ack
if (strcmp((char*)buf, "011000010110001101101011") == 0) {
Serial.println("Received ack from slave, sending next update");
break;
}
// todo: add timeout (e.g. 5 seconds no answer, stop the process)
}
} while (len > 0);
Serial.println("Finished sending the file");
readFile.close();
}
void loop() {
// code to listen for new mqtt messages will be here, including the 'trigger' for OTA
}
That's about it. I continiously "ask" from the slave if the update has been written, to ensure data intigrity.
Now, for the slave part it's almost the same process, the code is the following;
#include <ArduinoJson.h>
#include <Update.h>
String jsonCommand = ""; // global command on receive
bool inUpdateMode = false;
static bool finished = false; // if the update is finished or not
static int lastTime = 0; // Last time an update came in
static int previousTotal = 0; // Last time an update came in
void setup() {
Serial.begin(115200); // debug
Serial1.begin(921600, SERIAL_8N1, RX_PIN, TX_PIN); // to communicate with the master
}
void loop() {
// Check if there is something being sent over the UART
while (Serial1.available() && !inUpdateMode) {
// read from the serial
char c = Serial1.read();
if (c != '\n') {
jsonCommand.concat(c); // append the character to the string
} else {
// process the jsonCommand and reset it
processReceivedCommand();
jsonCommand = ""; // reset the command
}
}
static int n = 0;
while (Serial1.available() && !finished) {
uint8_t buf[264] = {};
n = Serial1.read(buf, 256);
static int total = 0;
total += n;
int x = Update.write(buf, n);
Serial.printf("data size: %d/%d, total: %d\n", x, n, total);
// check if the current total = previous total + buffer
if (total == previousTotal + 256) {
previousTotal = total;
// send new ack
Serial1.write("011000010110001101101011");
}
lastTime = millis();
}
if (lastTime != 0 && millis() - lastTime > 5000) {
Serial.println("No updates were sent in the past 5 seconds, assuming the update is finished.");
// If there were no updates in the past 5 seconds, we can assume that the update is done.
finished = true;
if (!Update.end(true)) {
Serial.printf("Update error: %d\n", Update.getError());
Update.printError(Serial);
while(1) delay(1000);
} else {
Serial.println("SUCCESS");
//ESP.restart();
}
}
vTaskDelay(1);
}
void processReceivedCommand() {
StaticJsonDocument<512> doc;
doc.clear(); // release the memory
DeserializationError error = deserializeJson(doc, jsonCommand);
if (error) {
Serial.println("Json decode error");
}
String action;
action = doc["action"].as<String>();
if (action == "FETCH_UPDATE") {
Serial.println("Going into update mode");
if (!Update.begin()) {
Serial.println("Cannot start the update");
Update.printError(Serial);
return;
}
Update.onProgress([](size_t progress, size_t len) {
Serial.printf("update progress: %d, %d\n", progress, len);
});
inUpdateMode = true;
Serial.println("Sending ACK to master");
Serial1.write("011000010110001101101011"); // Trigger the start sending on the master side
}
}
That mostly sums up the whole "communication between devices".
Problem is, I can't have the slave connected to wifi or some sort. All the internet communications needs to hapen via the master. there cannot be any human interaction on the hardware or software to update an device.
Here is some debugging output on the left slace, on the right, master:
Ths update started on 17:04:26
and suddenly ended on 17:11:30
, which means that after 7 minutes, the transmission just stopped working and the slave waits for that to hapen in order to see the transmission as 'completed'. The last OTA update callback I got was update progress: 102400, 1310720
and tat correspondents with the effective "total" in my code.
What am I missing here? What can I do better to speed things up (by hopefully a lot) and how can I achieve a robust OTA with master->slave updates capabilities?