🔍
v1.0.0

VoyagerOTA

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.

Requirements

  • C++17 or higher
  • ArduinoJson library version 7.0 or above
  • ESP32/ESP8266 Arduino framework

Features

  • Header-only library design
  • Remote firmware updates via HTTP
  • Built-in semantic version comparison
  • Custom JSON parser support
  • HTTP header customization
  • Environment modes (STAGING/PRODUCTION) for Voyager Platform
  • Internet connectivity checking
  • Progress callbacks and error handling
  • Two Endpoints Approach for maximum flexibility

Two Endpoints Approach

VoyagerOTA uses a Two Endpoints Approach for maximum flexibility:

  1. Release Information Endpoint: Returns JSON metadata about the latest firmware (must include version field)
  2. 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. This is how the library determines if an update is available.

This approach allows you to:

  • 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

Voyager Platform Integration

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.

Quick Start

#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 for Voyager Platform)
    ota.setEnvironment(Voyager::PRODUCTION);

    // Check internet connectivity
    if (Voyager::hasInternet()) {
        // 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
}

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.

// Set environment (only applies to Voyager Platform)
ota.setEnvironment(Voyager::PRODUCTION);  // or Voyager::STAGING (default)
  • STAGING: For testing and development releases
  • PRODUCTION: For stable, production-ready releases

Default JSON Response Format

The Voyager Platform returns firmware metadata in this format:

{
    "data": {
        "release": {
            "version": "2.0.0",
            "change_log": "Bug fixes and improvements",
            "released_date": "2025-06-29",
            "environment": "production"
        },
        "file": {
            "size": "1.44 MB",
            "size_in_bytes": 1509949
        },
        "board_type": "ESP32",
        "message": "Success"
    }
}

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:

  1. Release Info Endpoint: Returns JSON with at minimum a version field
  2. Download Endpoint: Serves the firmware binary file with application/octet-stream content type
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;
        }

        // Handle non-200 status codes
        if (statusCode != HTTP_CODE_OK) {
            CustomPayload payload = {
                .version = "0.0.0",  // Default version for error cases
                .statusCode = statusCode
            };
            return payload;
        }

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

        return payload;
    }
};

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

    // Create parser and OTA instance
    auto parser = std::make_unique<CustomParser>();
    Voyager::OTA<Voyager::ResponseData, CustomPayload> ota(std::move(parser), "1.0.0");

    // Set custom endpoints
    ota.setReleaseURL("https://api.example.com/firmware/latest");
    ota.setDownloadURL("https://api.example.com/firmware/download");

    if (Voyager::hasInternet()) {
        auto release = ota.fetchLatestRelease();
        if (release && ota.isNewVersion(release->version)) {
            Serial.println("Updating to: " + release->version);
            ota.performUpdate();
        }
    }
}

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 (recommended for different auth per endpoint)
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 (simpler for same auth)
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");  // No headers here
ota.setDownloadURL("https://api.example.com/download"); // No headers here
Caution

The library will log an error message if you try to set endpoint-specific headers after global headers have been initialized. This will prevent the headers from being applied.

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;
        }

        // Handle non-200 status codes
        if (statusCode != HTTP_CODE_OK) {
            GithubReleaseModel payload = {
                .version = "0.0.0",  // Default version for error cases
                .statusCode = statusCode,
            };
            return payload;
        }

        GithubReleaseModel payload = {
            .version = document["tag_name"].template as<String>(),                    // Required field
            .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);

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

    // Set GitHub API endpoint with headers
    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!");

    if (Voyager::hasInternet()) {
        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);

            // Set download URL from GitHub release
            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 to create 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").

Utilities

Internet Connectivity Check

Important

Always check internet connectivity before attempting OTA updates to avoid unnecessary failures and improve user experience.

if (Voyager::hasInternet()) {
    // Internet is available, proceed with update check
    auto release = ota.fetchLatestRelease();
    // ... rest of update logic
} else {
    Serial.println("No internet connection");
}

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 comprehensive 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
  • Network connectivity issues are handled gracefully
Tip

Monitor the Serial output during development to debug any issues with your OTA implementation. The library provides detailed logging for troubleshooting.

Common error scenarios:

  • Invalid JSON responses
  • Network connectivity issues
  • Authentication failures
  • Missing or malformed version fields
  • Server errors (4xx, 5xx HTTP status codes)

License

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