0

I'm trying to achieve a following method to update 2 cpu's their firmware;

flowchart

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: debug output

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?

Alan Birtles
  • 32,622
  • 4
  • 31
  • 60
Robin
  • 1,567
  • 3
  • 25
  • 67
  • Something is _radically_ wrong. 1MB over a network link should just take a few seconds. Given all the premade classes that access via http either the server is dreadfully slow or the classes you're using have some [too] high overhead. I'd fix that first. After the master downloads the firmware, at (e.g.) 110,000 baud, the 1MB can be sent in ~100 seconds. – Craig Estey Jan 21 '22 at 21:06
  • 1
    please don't tag c++ questions with c, they're different languages – Alan Birtles Jan 21 '22 at 21:13
  • @CraigEstey I've ran some tests, and I'm getting a throughput of around 624kbps on downloads on my ESP32. The setup is the ETH.h library with an LAN8720 connected the the esp. The code for the eth setup is: `ETH.begin(1, -1, 23, 18, ETH_PHY_LAN8720, ETH_CLOCK_GPIO17_OUT);` And I don't see what is or would be wrong with it? – Robin Jan 22 '22 at 12:07
  • "I don't see what is or would be wrong with it"? Do you know what `Serial1.write("011000010110001101101011")` does? It sent 24-bytes of ASCII code for the string as `{0x30, 0x31, 0x31....}`... It does not send "ack" (which is 0x61, 0x63, 0x6b) as you probably intend to... – hcheung Jan 24 '22 at 06:39
  • @hcheung I'm not really good with C++, I'd like to achieve the functionality described in the question, it currently works (I think) but goes really slow... Like an update every second of 256 bits – Robin Jan 24 '22 at 11:18

0 Answers0