Minimal OTA with Blynk.Air using Tinygsm

After countless banging my head against the wall and running post to pillar to send OTA .bin files over GSM to my device, I have figured out a solution for the same, it has a few prerequisites, this is based on the minimal OTA code that was shared by @Ibrahim_Roshdy over here Blynk.Air OTA minimal code

I am using tinygsm with a sim800l and on an ESP32

edit, added a few missing function calls
there may be a few which aren’t used which i may have added during testing, please feel free to point it out

#include <TinyGsmClient.h>
#include <BlynkSimpleTinyGSM.h>
#include<FS.h>
#include "FS.h"
#include "SPIFFS.h"

// Increase RX buffer
#define TINY_GSM_RX_BUFFER 1023

// other include functions and global variables here

void printProgressBar(int progress) {
  Serial.print("\rProgress: [");
  for (int i = 0; i < 50; ++i) {
    Serial.print(i < (progress / 2) ? "=" : " ");
  }
  Serial.print("] ");
  Serial.print(progress);
  Serial.print("%");
}
void appendFile(fs::FS &fs, const char *path, const char *message)
{
    Serial.printf("Appending to file: %s\n", path);

    File file = fs.open(path, FILE_APPEND);
    if (!file)
    {
        Serial.println("Failed to open file for appending");
        return;
    }
    if (file.print(message))
    {
        Serial.println("APOK");
    }
    else
    {
        Serial.println("APX");
    }
}

void readFile(fs::FS &fs, const char *path)
{
    Serial.printf("Reading file: %s\n", path);

    File file = fs.open(path);
    if (!file || file.isDirectory())
    {
        Serial.println("Failed to open file for reading");
        return;
    }

    Serial.print("Read from file: ");
    while (file.available())
    {
        Serial.write(file.read());
        delayMicroseconds(100);
    }
}

void writeFile(fs::FS &fs, const char *path, const char *message)
{
    Serial.printf("Writing file: %s\n", path);

    File file = fs.open(path, FILE_WRITE);
    if (!file)
    {
        Serial.println("Failed to open file for writing");
        return;
    }
    if (file.print(message))
    {
        Serial.println("File written");
    }
    else
    {
        Serial.println("Write failed");
    }
}

void listDir(fs::FS &fs, const char *dirname, uint8_t levels)
{
    Serial.printf("Listing directory: %s\n", dirname);

    File root = fs.open(dirname);
    if (!root)
    {
        Serial.println("Failed to open directory");
        return;
    }
    if (!root.isDirectory())
    {
        Serial.println("Not a directory");
        return;
    }

    File file = root.openNextFile();
    while (file)
    {
        if (file.isDirectory())
        {
            Serial.print("  DIR : ");
            Serial.println(file.name());
            if (levels)
            {
                listDir(fs, file.name(), levels - 1);
            }
        }
        else
        {
            Serial.print("  FILE: ");
            Serial.print(file.name());
            Serial.print("  SIZE: ");
            Serial.println(file.size());
        }
        file = root.openNextFile();
    }
}

void deleteFile(fs::FS &fs, const char *path)
{
    Serial.printf("Deleting file: %s\n", path);
    if (fs.remove(path))
    {
        Serial.println("File deleted");
    }
    else
    {
        Serial.println("Delete failed");
    }
}



void performUpdate(Stream &updateSource, size_t updateSize)
{
    if (Update.begin(updateSize))
    {
        size_t written = Update.writeStream(updateSource);
        if (written == updateSize)
        {
            Serial.println("Writes : " + String(written) + " successfully");
        }
        else
        {
            Serial.println("Written only : " + String(written) + "/" + String(updateSize) + ". Retry?");
        }
        if (Update.end())
        {
            Serial.println("OTA finished!");
            if (Update.isFinished())
            {
                Serial.println("Restart ESP device!");
                ESP.restart();
            }
            else
            {
                Serial.println("OTA not fiished");
            }
        }
        else
        {
            Serial.println("Error occured #: " + String(Update.getError()));
        }
    }
    else
    {
        Serial.println("Cannot beggin update");
    }
}

void updateFromFS()
{
    File updateBin = SPIFFS.open("/update.bin");
    if (updateBin)
    {
        if (updateBin.isDirectory())
        {
            Serial.println("Directory error");
            updateBin.close();
            return;
        }

        size_t updateSize = updateBin.size();

        if (updateSize > 0)
        {
            Serial.println("Starting update");
            performUpdate(updateBin, updateSize);
        }
        else
        {
            Serial.println("Error, archivo vacío");
        }

        updateBin.close();

        // whe finished remove the binary from sd card to indicate end of the process
        //fs.remove("/update.bin");
    }
    else
    {
        Serial.println("no such binary");
    }
}



void printPercent(uint32_t readLength, uint32_t contentLength)
{
    // If we know the total length
    if (contentLength != -1)
    {
        Serial.print("\r ");
        Serial.print((100.0 * readLength) / contentLength);
        Serial.print('%');
    }
    else
    {
        Serial.println(readLength);
    }
}


bool parseURL(const String& url, String& server, String& resource, int& port) {
    // Check if the URL starts with "http://"
    if (!url.startsWith("http://")) {
        Serial.println("Invalid URL format");
        return false;
    }

    // Remove the "http://" part
    String urlWithoutProtocol = url.substring(7);

    // Find the position of the first "/" after "http://"
    int slashPos = urlWithoutProtocol.indexOf('/');

    // Extract the server part
    server = urlWithoutProtocol.substring(0, slashPos);

    // Extract the resource part
    resource = urlWithoutProtocol.substring(slashPos);

    // Find the position of ":" to check for a custom port
    int colonPos = server.indexOf(':');

    if (colonPos != -1) {
        // Custom port specified
        port = server.substring(colonPos + 1).toInt();
        server = server.substring(0, colonPos);
    } else {
        // Use default port 80
        port = 80;
    }

    return true;
}
String overTheAirURL;
BLYNK_WRITE(InternalPinOTA) {
    overTheAirURL = param.asString();

    String serverOTABlynk;
    String resourceOTABlynk;
    int portOTABlynk;
Blynk.disconnect();
    if (!parseURL(overTheAirURL, serverOTABlynk, resourceOTABlynk, portOTABlynk)) {
        Serial.println("Failed to parse URL");
        return;
    }

    TinyGsmClient client(modem);

    Serial.print("Connecting to ");
    Serial.print(serverOTABlynk);
    Serial.print(" on port ");
    Serial.println(portOTABlynk);

    if (!client.connect(serverOTABlynk.c_str(), portOTABlynk)) {
        Serial.println("Connection failed");
        Blynk.connect();
        return;
    }

    // Make a HTTP GET request
    client.print(String("GET ") + resourceOTABlynk + " HTTP/1.1\r\n" +
                 "Host: " + serverOTABlynk + "\r\n" +  // Use only the server part
                 "Cache-Control: no-cache\r\n" +
                 "Connection: close\r\n\r\n");

   long timeout = millis();

    // Wait for the server to respond
    while (!client.available()) {
        if (millis() - timeout > 25000L) {
            Serial.println(">>> Client Timeout !");
            client.stop();
            Blynk.connect();
            return;
        }
    }

    // Reading response header
    uint32_t contentLength = 0;

    while (client.available()) {
        String line = client.readStringUntil('\n');
        line.trim();
        Serial.println(line);  // Print the response header (uncomment for debugging)

        if (line.startsWith("Content-Length: ")) {
            contentLength = line.substring(line.lastIndexOf(':') + 1).toInt();
        } else if (line.length() == 0) {
            break;
        }
    }

    Serial.print("Content-Length: ");
    Serial.println(contentLength);

    // Open the firmware file in SPIFFS for writing
    File firmwareFile = SPIFFS.open("/update.bin", "w");
    if (!firmwareFile) {
        Serial.println("Failed to open firmware file for writing");
        client.stop();
        Blynk.connect();
        return;
    }

    // Reading and handling the response
    uint32_t readLength = 0;


    Serial.println("Downloading firmware...");

    while (client.available()) {
        uint8_t c = client.read();

        readLength++;

        // Write firmware data to SPIFFS
        if (firmwareFile.write(c) != 1) {
            Serial.println("Error writing firmware data to SPIFFS");
            firmwareFile.close();
            client.stop();
            Blynk.connect();
            return;
        }

        // Your update logic here, similar to the previous example
        if (contentLength != 0 && readLength % (contentLength / 13) == 0) {
            // Print progress if needed
            Serial.print("\r");
            Serial.println((100.0 * readLength) / contentLength);
            Serial.print('%');
        }
    }

    Serial.println();  // New line after progress

    // Close the firmware file
    firmwareFile.close();

    // Close the client connection
    client.stop();

    // Check CRC and update completion
    if (readLength == contentLength) {
        Serial.println("Update successful!");
        updateFromFS();
        // Reboot or perform other necessary actions
        ESP.restart();
    } else {
        Serial.println("Update failed!");
        Blynk.connect();
        return;
    }
}
void setup()
{
//setup serial monitor, configure tinygsm, blynk setup, other functions etc
    if (!SPIFFS.begin(true))
    {
        Serial.println("SPIFFS Mount Failed");
        return;
    }
    SPIFFS.format();
    listDir(SPIFFS, "/", 0);
//Format the spiffs to accept updates
}

void loop()
{
Blynk.run();
timer.run();
}

The disconnects you see in the image is me purposefully messing with the power supply of the sim800 to check whether Blynk manages to ship the firmware if there is a signal quality drop and/or total disconnection via GSM

a 1MB .bin file took about 2:30 minutes to download, so about 6.8 KBPS, an acceptable speed for a 2g connection :slight_smile:

this is a drop-in replacement for all Blynk sketches that run Blynk 2.0 but are not operating via edgent due to a lack of support

thank you @PeteKnight for pointing me in the right direction earlier when i was stuck with this

Happy Blynking to everyone

1 Like