OTA Configuration

Uploading Firmware

Uploading Firmware via Platform IO (VSCode)

You can find an example project here: https://github.com/TechdroidInc

To upload directly from platform IO we use a pre- and post-script in the platform.ini file. The two scripts can be found here (post_extra_script.py, pre_extra_script.py) and should be places in your root folder. Configuration is done via specific variables in the platform.ini file.

There following variables are required:

  • api_token - an api token with access to the project on otabin
  • upload_server_url - otabin upload server URL without trailing slash (should almost always be ‘https://app.otabin.com ’)
  • ota_url - otabin server URL without trailing slash (should almost always be ‘https://app.otabin.com ’)
  • custom_prog_version - firmware version
  • custom_prog_board - hardware name
  • custom_hw_uuid - hardware unqiueID from the otabin platform

Step-by-step setup:

  1. Download the two scripts and place them in your project root.
  2. Create a private_config.ini in the root folder. Add a ‘otabin’ section
    [otabin]
    api_token = otabin_api_key
    upload_server_url = https://app.otabin.com
    
    Also add [otabin] section and pre- and post-scripts to your env section in your platform.ini
    [otabin]
    ota_url = https://app.otabin.com
    
    [env:esp32-s3-rev-a-release]
    ...
    custom_prog_version = 1.0.2
    custom_prog_board = esp32-s3-rev-a
    custom_hw_uuid = 7d2e5970-a8de-4c69-bd3d-6c8eb78fd3f9
    upload_protocol = custom
    extra_scripts =
        pre:pre_extra_script.py
        post:post_extra_script.py
    
  3. In platform IO the firmware will now be uploaded upon running this particular env. We recommend setting separate envs for local development and release. There is a complete project example available here:

Configure Hardware for OTA

Arduino - esp32FOTA

Similar to the upload configuration in order to have your hardware do OTAs you need to additionally add the following to your env in platform.ini

[env:esp32-s3-rev-a-release]
...
build_flags =
  -D VERSION=\"${this.custom_prog_version}\"
  -D BOARD=\"${this.custom_prog_board}\"
  -D OTA_HW_UUID=\"${this.custom_hw_uuid}\"
  -D OTA_URL=\"${otabin.server_url}/fw/l\"

This will inject 4 constants into your project which esp32FOTA will use to perform the OTA.

In your main.cpp do something similar to this:

...
#include <esp32FOTA.hpp>

esp32FOTA FOTA(BOARD, VERSION, false, true); // init esp32FOTA

int32_t updateCounter = 86401;  // Do update on boot
int32_t updateInterval = 86400; // Check for update every 24 hours if loop delay is 1 second

void setup()
{
  //...

  FOTA.setManifestURL(OTA_URL); // OTA without trailing slash
  FOTA.useDeviceId(true); // Unique HW identifier
  FOTA.setExtraHTTPHeader("x-hardware", BOARD); // env name of the build, will be sent as 'type' in json response
  FOTA.setExtraHTTPHeader("x-current-version", VERSION); // this is the current version
  FOTA.setExtraHTTPHeader("x-hardware-uuid", OTA_HW_UUID); // the hardware Unique ID

  // Other options such as reboot after update
  FOTA.setUpdateFinishedCb([](int partition, bool restart_after) {
    if (restart_after)
    {
      ESP.restart();
    }
  });

  //...

}

void loop()
{
  //...

  if (updateCounter > updateInterval)
  {
    updateCounter = 0;
    bool updatedNeeded = FOTA.execHTTPcheck();
    if (updatedNeeded)
    {
      FOTA.execOTA();
    }
  }
  updateCounter++;
  delay(1000);

  //...
}

Example pre_extra_script.py

Import("env")

try:
    import configparser
except ImportError:
    import ConfigParser as configparser
project_config = configparser.ConfigParser()
project_config.read("platformio.ini")

env.Replace(PROGNAME=env.GetProjectOption("custom_prog_board") + "_" + env.GetProjectOption("custom_prog_version")) 

Example post_extra_script.py

import requests
import sys
from os.path import basename

Import('env')

try:
    import configparser
except ImportError:
    import ConfigParser as configparser
project_config = configparser.ConfigParser(inline_comment_prefixes="#")
private_config = configparser.ConfigParser(inline_comment_prefixes="#")
project_config.read("platformio.ini")
private_config.read("private_config.ini")

ota_config = {k: v for k, v in project_config.items("otabin")}
otaupload_config = {k: v for k, v in private_config.items("otabin")}

def publish_firmware(source, target, env):

    hardware = env.GetProjectOption("custom_prog_board")
    uuid = env.GetProjectOption("custom_hw_uuid")
    version = env.GetProjectOption("custom_prog_version")
    firmware_path = str(source[0])
    firmware_name = basename(firmware_path)

    print("Uploading {0} to otabin.com. Version: {1}".format(firmware_path, version))

    url = "/".join([
        otaupload_config.get("upload_server_url"), "fw/upload"
    ])

    headers = {
        "X-Firmware-Version": version,
        "X-Hardware": hardware,
        "X-Hardware-Uuid": uuid,
        "Authorization": "Bearer " + otaupload_config['api_token']
    }

    r = None
    try:
        files = {'firmware': open(firmware_path, "rb")}
        r = requests.post(url,
                         files = files,
                         headers = headers)
        r.raise_for_status()
    except requests.exceptions.RequestException as e:
        if r.status_code >= 400:
            sys.stderr.write("Failed to submit package: %s\n" % r.text)           
        else:
            sys.stderr.write("Failed to submit package: %s\n" %
                            ("%s\n%s" % (r.status_code, r.text) if r else str(e)))
        env.Exit(1)

    print("The firmware has been successfuly uploaded to otabin.com")

env.Replace(UPLOADCMD=publish_firmware)