Documentation

VoyagerOTA is a header-only C++ library for remote OTA (Over-The-Air) firmware updates on ESP32/ESP8266 devices. It works out-of-the-box with the Voyager Platform backend but can be customized to work with any backend that follows a Two Endpoints Approach.

Overview

VoyagerOTA provides a simple yet powerful way to manage firmware updates for ESP32 and ESP8266 devices. The library is designed to be flexible, allowing you to use either the Voyager Platform backend or integrate with your own custom backend solution.

Features

  • Header-only Design - Simple integration without complex build configurations
  • Remote Updates - Firmware updates via HTTP with progress tracking
  • Version Comparison - Built-in semantic version comparison
  • Fully Customizable - Custom JSON parser and backend support
  • HTTP Headers - Custom authentication and header configuration
  • Environment Modes - STAGING/PRODUCTION support for Voyager Platform

Requirements

Software
  • C++17 or higher
  • ArduinoJson library version 7.0+
  • ESP32/ESP8266 Arduino framework
Hardware
  • ESP32 or ESP8266 board
  • WiFi connectivity
  • Sufficient flash memory for OTA

Quick Start

Tip
The Voyager Platform provides a complete backend solution with built-in version management, making it the easiest way to get started with OTA updates.
#include <VoyagerOTA.hpp>

void setup() {
    Serial.begin(115200);

    // Create OTA instance with current firmware version
    Voyager::OTA<> ota("1.0.0");

    // Set environment to PRODUCTION (defaults to STAGING)
    ota.setEnvironment(Voyager::PRODUCTION);

    // Fetch latest release information
    auto release = ota.fetchLatestRelease();

    if (release && ota.isNewVersion(release->version)) {
        Serial.println("New version available: " + release->version);
        Serial.println("Changelog: " + release->changeLog);

        // Perform the update
        ota.performUpdate();
    } else {
        Serial.println("No update available");
    }
}

void loop() {
    // Your main code here
}

Two Endpoints Approach

VoyagerOTA uses a Two Endpoints Approach for maximum flexibility:

Release Information Endpoint
Returns JSON metadata about the latest firmware (must include version field)
Download Endpoint
Serves the actual firmware binary file
Important
The release information response must contain a version field for semantic version comparison to work properly.

Benefits

  • Use different authentication methods for metadata vs. downloads
  • Implement custom logic for release selection
  • Support various backend architectures (CDN, database-driven, etc.)
  • Parse different JSON response formats

Environment Settings

Note
The STAGING/PRODUCTION environment settings are specifically for the Voyager Platform backend. They control which release channel your device checks for updates.
Important
All firmware binaries uploaded to Voyager must be built using production settings.

Voyager environments control release visibility, not how firmware is built. Uploaded binaries are processed once, locked, and first exposed to the STAGING environment for testing. During testing, new binaries may be re-uploaded, but every upload must remain a production build.

Promotion to PRODUCTION does not rebuild firmware it only makes the already validated binary available to production devices.
// Set environment (only applies to Voyager Platform)
ota.setEnvironment(Voyager::PRODUCTION);  // or Voyager::STAGING (default)
  • STAGING - For testing and validation before release
  • PRODUCTION - For production device distribution

Testing is performed by running production-ready firmware on devices configured to use the STAGING environment.

Default JSON Response Format

The Voyager Platform returns firmware metadata in this format:


{
  "release": {
    "id": "jJGoFvZ1Wxsb171NPQ2HL",
    "status": "staging",
    "version": "2.0.1",
    "changeLog": "Smart Lock Firmware",
    "releasedAt": null,
    "artifact": {
      "size": 1706512,
      "hash": "cbf34fe7537e1977b127eef7a207358a8a56980b14d903f998697cbdbe4e94f4",
      "downloadURL": "https://api.voyagerota.com/v1/releases/jJGoFvZ1Wxsb171NPQ2HL/downloads/UwkEyt7L_nUjXvqktor5l"
    }
  },
  "status": {
    "reason": "OK",
    "code": 200
  }
}

Default Payload Structure

The default DefaultPayloadModel includes:

  • version - Firmware version string (required for comparisons)
  • changeLog - Release notes
  • releasedDate - Release date
  • fileSize - Human readable file size
  • fileSizeInBytes - File size in bytes
  • boardType - Target board type
  • environment - Release environment
  • message - Status message
  • statusCode - HTTP response code

Custom Backend Implementation

Backend Requirements

For the Two Endpoints Approach, your backend must implement:

Release Info Endpoint
Returns JSON with at minimum a version field
Download Endpoint
Serves firmware binary with application/octet-stream
Important
The version field in your JSON response is mandatory for version comparison functionality. Without it, the library cannot determine if an update is available.

Example minimal backend response:

{
    "version": "2.0.0"
}

Custom JSON Parser

Create your own parser for different JSON response formats:

#include <VoyagerOTA.hpp>

// Define your custom payload structure
struct CustomPayload {
    String version;        // Required for version comparison
    String description;
    String downloadUrl;
    int statusCode;
};

// Implement custom parser
class CustomParser : public Voyager::IParser<Voyager::ResponseData, CustomPayload> {
public:
    std::optional<CustomPayload> parse(Voyager::ResponseData responseData, int statusCode) override {
        ArduinoJson::JsonDocument doc;
        ArduinoJson::DeserializationError error = ArduinoJson::deserializeJson(doc, responseData);

        if (error) {
            Serial.println("JSON parsing failed");
            return std::nullopt;
        }

        if (statusCode != HTTP_CODE_OK) {
            CustomPayload payload = {
                .version = "0.0.0",
                .statusCode = statusCode
            };
            return payload;
        }

        CustomPayload payload = {
            .version = doc["version"].as<String>(),
            .description = doc["description"].as<String>(),
            .downloadUrl = doc["download_url"].as<String>(),
            .statusCode = statusCode
        };

        return payload;
    }
};

Configuration Methods

Header Configuration

Warning
If global headers are initialized, you cannot set endpoint-specific headers for release and download URLs. Choose one approach and stick with it.
// Option 1: Endpoint-specific headers
std::vector<Voyager::OTA<>::Header> releaseHeaders = {
    {"Authorization", "Bearer your-token"},
    {"Content-Type", "application/json"}
};
ota.setReleaseURL("https://api.example.com/releases", releaseHeaders);

std::vector<Voyager::OTA<>::Header> downloadHeaders = {
    {"Authorization", "Bearer your-token"},
    {"Accept", "application/octet-stream"}
};
ota.setDownloadURL("https://api.example.com/download", downloadHeaders);

// Option 2: Global headers for both endpoints
std::vector<Voyager::OTA<>::Header> globalHeaders = {
    {"User-Agent", "VoyagerOTA/1.0.0"},
    {"Authorization", "Bearer your-token"}
};
ota.setGlobalHeaders(globalHeaders);
ota.setReleaseURL("https://api.example.com/releases");
ota.setDownloadURL("https://api.example.com/download");

GitHub Releases Example

Tip
GitHub Releases provides a free and reliable way to distribute firmware updates with built-in version control and asset management.

Here's a complete example of integrating VoyagerOTA with GitHub releases:

#include <VoyagerOTA.hpp>

// Define GitHub release payload structure
struct GithubReleaseModel {
    String version;           // Required for version comparison
    String name;
    String publishedAt;
    String browserDownloadUrl;
    int size;
    int statusCode;
};

// Implement GitHub JSON parser
class GithubJSONParser : public Voyager::IParser<Voyager::ResponseData, GithubReleaseModel> {
public:
    std::optional<GithubReleaseModel> parse(Voyager::ResponseData responseData, int statusCode) override {
        ArduinoJson::JsonDocument document;
        ArduinoJson::DeserializationError error = ArduinoJson::deserializeJson(document, responseData);

        if (error) {
            Serial.printf("VoyagerOTA JSON Error : %s\n", error.c_str());
            return std::nullopt;
        }

        if (statusCode != HTTP_CODE_OK) {
            GithubReleaseModel payload = {
                .version = "0.0.0",
                .statusCode = statusCode,
            };
            return payload;
        }

        GithubReleaseModel payload = {
            .version = document["tag_name"].template as<String>(),
            .name = document["name"].template as<String>(),
            .publishedAt = document["published_at"].template as<String>(),
            .browserDownloadUrl = document["assets"][0]["url"].template as<String>(),
            .size = document["assets"][0]["size"].template as<int>(),
            .statusCode = statusCode
        };

        return payload;
    }
};

void setup() {
    Serial.begin(115200);

    std::unique_ptr<GithubJSONParser> parser = std::make_unique<GithubJSONParser>();
    Voyager::OTA<Voyager::ResponseData, GithubReleaseModel> ota(std::move(parser), "2.0.0");

    ota.setReleaseURL("https://api.github.com/repos/owner/repo/releases/latest",
                      {
                          {"Authorization", "Bearer your-github-token"},
                          {"X-GitHub-Api-Version", "2022-11-28"},
                          {"Accept", "application/vnd.github+json"},
                      });

    Serial.println("OTA Engine Started!");

    std::optional<GithubReleaseModel> release = ota.fetchLatestRelease();

    if (release && ota.isNewVersion(release->version)) {
        Serial.println("New version available: " + release->version);
        Serial.println("Release name: " + release->name);

        ota.setDownloadURL(release->browserDownloadUrl,
                            {
                                {"Authorization", "Bearer your-github-token"},
                                {"X-GitHub-Api-Version", "2022-11-28"},
                                {"Accept", "application/octet-stream"},
                            });

        ota.performUpdate();
    } else {
        Serial.println("No updates available yet!");
    }
}

void loop() {}
Note
For GitHub integration, you'll need a Personal Access Token with repository read permissions. Public repositories may work without authentication for release metadata, but private repositories require authentication. GitHub's API response includes the tag_name field which serves as the version for comparison. Make sure your GitHub release tags follow semantic versioning (e.g., "v1.0.0" or "1.0.0").

Version Management

// Set current firmware version
ota.setCurrentVersion("2.1.0");

// Get current version
String current = ota.getCurrentVersion();

// Check if a version is newer
bool hasUpdate = ota.isNewVersion("2.2.0");
Note
Version comparison uses semantic versioning. Make sure your version strings follow the format "major.minor.patch" (e.g., "1.0.0", "2.1.5").

Error Handling

The library provides some error handling and logging:

  • JSON parsing errors are logged to Serial with detailed error messages
  • HTTP errors are captured in the payload's statusCode field
  • Update progress and errors are reported through Serial output
Tip
Monitor the Serial output during development to debug any issues with your OTA implementation. The library provides detailed logging for troubleshooting.

License

MIT License - see the source file header for complete license text.