ESP32 painlessmesh OTA from web browser

Hi,
I want to be able to update the firmware of all nodes of a wireless mesh network by uploading the new firmware file on a web page.
Therefore my "sender" node will be connected to the mesh, and will also be connected as Client to my accespoint. It will run a webserver so that I can open a webpage and upload the new firmware for the "receiver" nodes.

I made a program merging this two examples:

  1. OTA via mesh fetching the file from SD card.
    examples/otaSender · master · painlessMesh / painlessMesh · GitLab

  2. OTA (without mesh) for the ESP32 running the webserver, loading the file from web page.
    https://lastminuteengineers.com/esp32-ota-web-updater-arduino-ide/

My merged program however is not complete on the part where I need to pass the file from the upload to the function that sends it to the mesh as OTA. How shall it be written?

My merged program is this:

//************************************************************
// this is an example that uses the painlessmesh library to
// upload firmware to another node on the network. 
// This will upload to an ESP device running the otaReceiver.ino
//
// The naming convetions should be as follows for bin files
// firmware_<hardware>_<role>.bin
// To create your own binary files, export them and rename
// them to follow this format, where hardware will be
// "ESP8266" or "ESP32" (capitalized.)
// If sending to the otacreceiver sketch, role should be
// "otareceiver" (lowercase)
//
//************************************************************
#include "IPAddress.h"
#include "painlessMesh.h"

#ifdef ESP8266
#include "Hash.h"
#include <ESPAsyncTCP.h>
#else
#include <AsyncTCP.h>
#endif
#include <ESPAsyncWebServer.h>


#define   STATION_SSID     "mywifi"
#define   STATION_PASSWORD "password"
#define HOSTNAME "HTTP_BRIDGE"

#define MESH_PREFIX "whateverYouLike"
#define MESH_PASSWORD "somethingSneaky"
#define MESH_PORT 5555

#define OTA_PART_SIZE 1024 //How many bytes to send per OTA data packet

// Prototype
void receivedCallback( const uint32_t &from, const String &msg );
IPAddress getlocalIP();

painlessMesh  mesh;
AsyncWebServer server(80);
IPAddress myIP(0,0,0,0);
IPAddress myAPIP(0,0,0,0);

//-----------------------------------------------------------------------------
void setup(){
  Serial.begin(115200);
  delay(1000);
  mesh.setDebugMsgTypes(
      ERROR | STARTUP | CONNECTION |
      DEBUG);  // set before init() so that you can see startup messages

  // Channel set to 6. Make sure to use the same channel for your mesh and for you other
  // network (STATION_SSID)
  mesh.init( MESH_PREFIX, MESH_PASSWORD, MESH_PORT, WIFI_AP_STA, 6 );
  mesh.onReceive(&receivedCallback);

  mesh.stationManual(STATION_SSID, STATION_PASSWORD);
  mesh.setHostname(HOSTNAME);

  // Bridge node, should (in most cases) be a root node. See [the wiki](https://gitlab.com/painlessMesh/painlessMesh/wikis/Possible-challenges-in-mesh-formation) for some background
  mesh.setRoot(true);
  // This node and all other nodes should ideally know the mesh contains a root, so call this on all nodes
  mesh.setContainsRoot(true);

  myAPIP = IPAddress(mesh.getAPIP());
  Serial.println("My AP IP is " + myAPIP.toString());

  //Async webserver
  server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
    request->send(200, "text/html", 
    "<form method='POST' action='/runupdate' enctype='multipart/form-data' id='upload_form'>Select file
"
    "<input type='file' name='update'>"
    "<input type='submit' value='Upload'></form>");
    
  });

  server.on("/runupdate", HTTP_POST, [](AsyncWebServerRequest *request){
    // here the file uploaded from the webpage is passed
    // to the function OTAmesh() 
    //OTAmesh();
    request->send(200, "text/html". "Update in progress");
  });
  
  server.begin();
} //end of setup

//-------------------------------------------------

void loop(){
  mesh.update();
  if(myIP != getlocalIP()){
    myIP = getlocalIP();
    Serial.println("My IP is " + myIP.toString());
  }
 }
//--------------------------------------------------

void OTAmesh(File entry){ //over the mesh firmware update
  //This block of code parses the file name to make sure it is valid.
    //It will also get the role and hardware the firmware is targeted at.
      TSTRING name = entry.name();
      if (name.length() > 1 && name.indexOf('_') != -1 &&
          name.indexOf('_') != name.lastIndexOf('_') &&
          name.indexOf('.') != -1) {
        TSTRING firmware = name.substring(1, name.indexOf('_'));
        TSTRING hardware =
            name.substring(name.indexOf('_') + 1, name.lastIndexOf('_'));
        TSTRING role =
            name.substring(name.lastIndexOf('_') + 1, name.indexOf('.'));
        TSTRING extension =
            name.substring(name.indexOf('.') + 1, name.length());
        if (firmware.equals("firmware") &&
            (hardware.equals("ESP8266") || hardware.equals("ESP32")) &&
            extension.equals("bin")) {

          Serial.println("OTA FIRMWARE FOUND, NOW BROADCASTING");

          //This is the important bit for OTA, up to now was just getting the file. 
          //If you are using some other way to upload firmware, possibly from 
          //mqtt or something, this is what needs to be changed.
          //This function could also be changed to support OTA of multiple files
          //at the same time, potentially through using the pkg.md5 as a key in
          //a map to determine which to send
          mesh.initOTASend(
              [&entry](painlessmesh::plugin::ota::DataRequest pkg,
                       char* buffer) {
                
                //fill the buffer with the requested data packet from the node.
                entry.seek(OTA_PART_SIZE * pkg.partNo);
                entry.readBytes(buffer, OTA_PART_SIZE);
                
                //The buffer can hold OTA_PART_SIZE bytes, but the last packet may
                //not be that long. Return the actual size of the packet.
                return min((unsigned)OTA_PART_SIZE,
                           entry.size() - (OTA_PART_SIZE * pkg.partNo));
              },
              OTA_PART_SIZE);

          //Calculate the MD5 hash of the firmware we are trying to send. This will be used
          //to validate the firmware as well as tell if a node needs this firmware.
          MD5Builder md5;
          md5.begin();
          md5.addStream(entry, entry.size());
          md5.calculate(); 

          //Make it known to the network that there is OTA firmware available.
          //This will send a message every minute for an hour letting nodes know
          //that firmware is available.
          //This returns a task that allows you to do things on disable or more,
          //like closing your files or whatever.
          mesh.offerOTA(role, hardware, md5.toString(),
                        ceil(((float)entry.size()) / OTA_PART_SIZE), false);

        
      }
    }
}//end of OTAmesh
//------------------------------------------------


void receivedCallback( const uint32_t &from, const String &msg ) {
  Serial.printf("bridge: Received from %u msg=%s\n", from, msg.c_str());
}
//---------------------------------------------------
IPAddress getlocalIP() {
  return IPAddress(mesh.getStationIP());
}

//-----------------------------------------------------
void rebootEspWithReason(String reason) {
  Serial.println(reason);
  delay(1000);
  ESP.restart();
}

//-----------------------------------------------------

You need this libraries to compile the sketch:
painlessmesh
TaskScheduler
arduinojson
AsyncTC
ESPAsyncWebServer

1 Like

My merged program however is not complete on the part where I need to pass the file from the upload to the function that sends it to the mesh as OTA. How shall it be written?

Have a look at this section of the OTA page of the doc or the one for the update server.

Thanks for the links, I understood something more.

What is the meaning of those empty brackets ()[] in the server.on function?
Why there are two sets of code enclosed in curly brackets {...} and when are they executed?

  /*handling uploading firmware file */
  server.on("/update", HTTP_POST, []() {
    server.sendHeader("Connection", "close");
    server.send(200, "text/plain", (Update.hasError()) ? "FAIL" : "OK");
    ESP.restart();
  }, []() {
    HTTPUpload& upload = server.upload();
    if (upload.status == UPLOAD_FILE_START) {
      Serial.printf("Update: %s\n", upload.filename.c_str());
      if (!Update.begin(UPDATE_SIZE_UNKNOWN)) { //start with max available size
        Update.printError(Serial);
      }
    } else if (upload.status == UPLOAD_FILE_WRITE) {
      /* flashing firmware to ESP*/
      if (Update.write(upload.buf, upload.currentSize) != upload.currentSize) {
        Update.printError(Serial);
      }
    } else if (upload.status == UPLOAD_FILE_END) {
      if (Update.end(true)) { //true to set the size to the current progress
        Serial.printf("Update Success: %u\nRebooting...\n", upload.totalSize);
      } else {
        Update.printError(Serial);
      }
    }
  });

The definition from the library is here arduino-esp32/WebServer.cpp at master · espressif/arduino-esp32 · GitHub But it doesn't help me (I'm electrical engineer, not informatic).

Other question:
the memory of the ESP is divided in 3 partitions:

  1. the running firmware
  2. OTA (space where the new firmware is written)
  3. SPIFF

When the firmware is uploaded from the webpage is written in the OTA partition, if the checksum is correct it is then moved to the partition 1, the ESP32 reboot with the new firmware.
In my case I need to distribute the new firmware to the mesh nodes, which takes time, therefore do I need to store it temporarily in RAM? That's not possible because the firmware can be 1.5 MB, and ram is only 500 kB.
I am starting to think that I need to save the .bin file in the SD as intermediate step between the webpage and the over-te-mesh- update. Thoughts?

I’m on my iPhone - will come back to this when I can but regarding the {} construct have a look at Lambda expressions - basically you define a closure, see that as an unnamed function object capable of capturing variables in scope.

This topic was automatically closed 120 days after the last reply. New replies are no longer allowed.