GITHUB OTA for ESP32 devices

Automate your OTA and CI/CD pipeline with Github Actions to update your ESP32 devices in the field direct from github releases

Features

  • Uses the esp_htps_ota library under the hood to update firmware images

  • Can also update spiffs/littlefs/fatfs partitions

  • Uses SemVer to compare versions and only update if a newer version is available

  • Plays nicely with App rollback and anti-rollback features of the esp-idf bootloader

  • Download firmware and partitiion images from the github release page directly

  • Supports multiple devices with different firmware images

  • Includes a sample Github Actions that builds and releases images when a new tag is pushed

  • Updates can be triggered manually, or via a interval timer

  • Uses a streaming JSON parser for to reduce memory usage (Github API responses can be huge)

  • Supports Private Repositories (Github API token required*)

  • Supports Github Enterprise

  • Supports Github Personal Access Tokens to overcome Github API Ratelimits

  • Sends progress of Updates via the esp_event_loop

Note: You should be careful with your GitHub PAT and putting it in the source code. I would suggest that you store the PAT in NVS, and the user enters it when running, as otherwise the PAT would be easily extractable from your firmware images.

Usage

via the Espressif Component Registry:

idf.py add-dependency Fishwaldo/ghota^1.0.0

Example

After Initilizing Network Access, Start a timer to periodically check for new releases:

    ghota_config_t ghconfig = {
        .filenamematch = "GithubOTA-esp32.bin", // Glob Pattern to match against the Firmware file
        .storagenamematch = "storage-esp32.bin", // Glob Pattern to match against the storage firmware file
        .storagepartitionname = "storage", // Update the storage partition
        .updateInterval = 60, // Check for updates every 60 minuites
    };
    ghota_client_handle_t *ghota_client = ghota_init(&ghconfig);
    if (ghota_client == NULL) {
        ESP_LOGE(TAG, "ghota_client_init failed");
        return;
    }
    esp_event_handler_register(GHOTA_EVENTS, ESP_EVENT_ANY_ID, &ghota_event_callback, ghota_client); // Register a handler to get updates on progress 
    ESP_ERROR_CHECK(ghota_start_update_timer(ghota_client)); // Start the timer to check for updates

Manually Checking for updates:

    ghota_config_t ghconfig = {
        .filenamematch = "GithubOTA-esp32.bin",
        .storagenamematch = "storage-esp32.bin",
        .storagepartitionname = "storage",
        .updateInterval = 60,
    };
    ghota_client_handle_t *ghota_client = ghota_init(&ghconfig);
    if (ghota_client == NULL) {
        ESP_LOGE(TAG, "ghota_client_init failed");
        return;
    }
    esp_event_handler_register(GHOTA_EVENTS, ESP_EVENT_ANY_ID, &ghota_event_callback, ghota_client);
    ESP_ERROR_CHECK(ghota_check(ghota_client));

    semver_t *cur = ghota_get_current_version(ghota_client);
    if (cur) {
        ESP_LOGI(TAG, "Current version: %d.%d.%d", cur->major, cur->minor, cur->patch);
        semver_free(cur);
    }

    semver_t *new = ghota_get_latest_version(ghota_client);
    if (new) {
        ESP_LOGI(TAG, "New version: %d.%d.%d", new->major, new->minor, new->patch);
        semver_free(new);
    }
    ESP_ERROR_CHECK(ghota_update(ghota_client));
    ESP_ERROR_CHECK(ghota_free(ghota_client));

Configuration

The following configuration options are available:

* config.filenamematch <- Glob pattern to match against the firmware file from the Github Releases page. 
* config.storagenamematch <- Glob pattern to match against the storage file from the Github Releases page.
* config.storagepartitionname <- Name of the storage partition to update (as defined in partitions.csv)
* config.hostname <- Hostname of the Github API (default: api.github.com)
* config.orgname <- Name of the Github User or Organization
* config.reponame <- Name of the Github Repository
* config.updateInterval <- Interval in minutes to check for updates

Github Actions

The Github Actions included in this repository can be used to build and release firmware images to Github Releases. This is a good way to automate your CI/CD pipeline, and update your devices in the field. In this example, we build two variants of the Firmware - on for a ESP32 and one for a ESP32-S3 device Using the filenamematch and storagenamematch config options, we can match against the correct firmware image for the device.

on:
  push:
  pull_request:
    branches: [master]

permissions:
  contents: write
name: Build
jobs:
  build:
    strategy:
      fail-fast: true
      matrix: 
        targets: [esp32, esp32s3]
    runs-on: ubuntu-latest
    steps:
    - name: Checkout repo
      uses: actions/checkout@v3
      with:
        submodules: 'recursive'
    - name: esp-idf build
      uses: espressif/esp-idf-ci-action@v1
      with:
        esp_idf_version: v4.4
        target: ${{ matrix.targets }}
    - name: Rename artifact
      run: |
        cp build/GithubOTA.bin GithubOTA-${{ matrix.targets }}.bin
        cp build/storage.bin storage-${{ matrix.targets }}.bin
    - name: Archive Firmware Files
      uses: actions/upload-artifact@v3
      with: 
        name: ${{ matrix.targets }}-firmware
        path: "*-${{ matrix.targets }}.bin"

  release:
    needs: build
    runs-on: ubuntu-latest
    steps:
    - name: Download Firmware Files
      uses: actions/download-artifact@v2
      with:
        path: release
    - name: Release Firmware
      uses: ncipollo/release-action@v1
      if: startsWith(github.ref, 'refs/tags/') 
      with:
        artifacts: release/*/*.bin
        generateReleaseNotes: true
        allowUpdates: true
        token: ${{ secrets.GITHUB_TOKEN }}