Documentation

An easy to use small client library for the VoyagerOTA platform, compatible with ESP32 and ESP8266 devices. It allows fetching the latest firmware and downloading OTA binaries from VoyagerOTA or any custom backend.

Requirements

Software
Hardware
  • ESP32 or ESP8266
  • Wi-Fi connectivity
  • Sufficient flash memory for OTA updates

Firmware Upload Rules

All firmware binaries uploaded to Voyager must be built with __ENABLE_DEVELOPMENT_MODE__ false. Uploading development-enabled builds is strictly prohibited and will be rejected by the platform.

VoyagerOTA library manages firmware releases using channel-based staging and production environments. Understanding this workflow is critical to prevent accidental deployment of development builds to field devices.

Step 1 - Upload Production Build
Always build firmware with __ENABLE_DEVELOPMENT_MODE__ false before uploading. This ensures the platform treats it as a production build.
Step 2 - Platform Staging Phase
Voyager automatically places uploaded production builds in the STAGING channel. This allows you to test the firmware locally while still using production binaries.
Step 3 - Local Testing
On your device, enable __ENABLE_DEVELOPMENT_MODE__ true temporarily. The library will fetch the latest STAGING release, which contains your production build, allowing safe testing.
Step 4 - Promote to Production
Once testing is complete, promote the release to PRODUCTION. Voyager does not rebuild the binary-it simply makes the validated build available to production devices.
Step 5 - Voyager Protection
The platform is intelligent and will reject any build uploaded with ENABLE_DEVELOPMENT_MODE true. This prevents field devices from accidentally receiving development firmware and ensures that all release channels are tracked correctly.

Quick Start

Example usage for fetching releases and updating firmware:


// Development mode is for staging/testing.......
#define __ENABLE_DEVELOPMENT_MODE__ true
#define CURRENT_FIRMWARE_VERSION "1.0.0"

#include <VoyagerOTA.hpp>
using namespace Voyager;

void setup() {
    Serial.begin(9600);
    OTA<> ota(CURRENT_FIRMWARE_VERSION);

    ota.setCredentials("voyager-project-id-here....", "voyager-api-key-here...");
    auto release = ota.fetchLatestRelease();

    if (release && ota.isNewVersion(release->version)) {
        Serial.println("New version available: " + release->version);
        Serial.println("Changelog: " + release->changeLog);
        ota.setDownloadURL(release->downloadURL);
        ota.performUpdate();
    } else {
        Serial.println("No updates available");
    }
}

void loop() {}
                

Endpoints Design Approach

  • Release Info Endpoint - Returns JSON metadata, must include version.
  • Download Endpoint - Serves the firmware binary.

Advanced Mode

Advanced Mode allows endpoint-specific parsers and custom backends. Voyager-specific features are disabled.

Info
Built-in GitHub parser is provided only in Advanced Mode.

#define __ENABLE_ADVANCED_MODE__ true
#define CURRENT_FIRMWARE_VERSION "1.0.0"

#include <VoyagerOTA.hpp>
using namespace Voyager;

void setup() {
    Serial.begin(9600);
    std::unique_ptr<GithubJSONParser> parser = std::make_unique<GithubJSONParser>();
    OTA<HTTPResponseData, GithubReleaseModel> ota(std::move(parser), CURRENT_FIRMWARE_VERSION);

    std::vector<Header> releaseHeaders = {
        {"Authorization", "Bearer your-github-token"},
        {"X-GitHub-Api-Version", "2022-11-28"},
        {"Accept", "application/vnd.github+json"},
    };

    ota.setReleaseURL("https://api.github.com/repos/{owner}/{repo}/releases", releaseHeaders);

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

        std::vector<Header>  downloadHeaders = {
            {"Authorization", "Bearer your-github-token..."},
            {"X-GitHub-Api-Version", "2022-11-28"},
            {"Accept", "application/octet-stream"},
        };

        ota.setDownloadURL(release->browserDownloadUrl, downloadHeaders);
        ota.performUpdate();
    }
}

void loop() {}
                
GitHub Integration

To integrate VoyagerOTA with GitHub, you'll need a Personal Access Token with repository read permissions.

The GitHub API response includes the tag_name field, which the library uses as the firmware version for comparison. Ensure your release tags follow semantic versioning (e.g., v1.0.0 or 4.3.0).

Custom Backend and Parser

Custom parsers allow integration with any backend. All models must extend BaseModel.


#define __ENABLE_ADVANCED_MODE__ true
#define CURRENT_FIRMWARE_VERSION "1.0.0"

#include <VoyagerOTA.hpp>

struct CustomPayload : public Voyager::BaseModel {
    String description;
    String downloadUrl;
    int statusCode;
};

class CustomParser : public Voyager::IParser<Voyager::HTTPResponseData, CustomPayload> {
public:
    std::optional<CustomPayload> parse(Voyager::HTTPResponseData responseData, int statusCode) override {
        ArduinoJson::JsonDocument document;
        ArduinoJson::DeserializationError error = ArduinoJson::deserializeJson(document, responseData);

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

        if (statusCode != HTTP_CODE_OK) {             
            return std::nullopt;
        }

        CustomPayload payload(document["version"],
                              document["description"],
                              document["downloadUrl"],
                              statusCode);

        return payload;
    }
};

void setup() {
    Serial.begin(9600);
    auto parser = std::make_unique<CustomParser>();
    Voyager::OTA<Voyager::HTTPResponseData, CustomPayload> ota(std::move(parser), CURRENT_FIRMWARE_VERSION);

    ota.setReleaseURL("https://api.hack-nasa-backend.com/firmware/latest");
    auto release = ota.fetchLatestRelease();
    if (release && ota.isNewVersion(release->version)) {
        ota.setDownloadURL(release->downloadUrl);
        ota.performUpdate();
    }
}

void loop() {}
                

Platform Architecture & Release Flow

Every release is treated as a proper artifact. That means it always has a version, goes through checks, and is deployed carefully to the right channel.

Backend Overview

VoyagerOTA Architecture Diagram

Starting a Release (Draft)

Developers first create a release in a draft state. At this point, we only need some metadata like version number and changelog no binaries yet. This makes planning safe and coordinated.

stateDiagram-v2 [*] --> Draft Draft --> Draft : Update Metadata

Uploading Binaries

Once a binary is ready, it’s uploaded to the backend. We immediately check for duplicates using a hash. If it’s already in the system, it’s rejected duplicates never enter validation.

flowchart LR Dev[Developer] -->|Upload Binary| NGINX NGINX --> API[Backend API] API --> Hash[Compute Artifact Hash] Hash --> DB[(MySQL)] DB -->|Duplicate Found| Reject[Reject Upload] DB -->|New Artifact| Persist[Persist Artifact] Persist --> Enqueue[Enqueue Inspection Job] Enqueue --> RedisQ[(Redis Queue)] Reject --> End[End] RedisQ --> End

Detecting Build Type

After upload, background workers check the binary type. The backend API itself doesn’t decide promotions only workers do.

flowchart LR RedisQ[(Redis Queue)] --> Worker[Artifact Inspection Worker] Worker --> Extract[Extract Embedded Voyager Tokens] Extract --> Inspect[Determine Build Type] Inspect -->|Dev Build| Reject[Reject for Staging] Inspect -->|Unknown Build| Reject Inspect -->|Prod Build| Promote[Promote Release → Staging] Promote --> DB[(Update Release State)] Promote --> RedisC[(Prime Staging Cache)]

Any development or unknown builds are rejected and never go to staging. Only verified production builds move forward.

Release Lifecycle

Releases move through strict states. We don’t just flip flags each transition is validated and must be promoted explicitly.

stateDiagram-v2 [*] --> Draft Draft --> Validation : Binary Uploaded Validation --> Rejected : Dev / Unknown Build Validation --> Staging : Worker Validates Prod Build Staging --> Production : Explicit Promotion Production --> Revoked : Emergency Rollback Rejected --> [*] Revoked --> [*]

How Devices Fetch Updates

Devices get releases depending on their build mode:

  • __ENABLE_DEVELOPMENT_MODE__ trueSTAGING
  • __ENABLE_DEVELOPMENT_MODE__ falsePRODUCTION
Devices first check Redis cache, then fall back to MySQL if needed.

flowchart LR Device -->|Fetch Latest| NGINX NGINX --> API[Backend API] API --> Mode{Dev Mode?} Mode -->|true| Staging[Resolve STAGING Channel] Mode -->|false| Prod[Resolve PRODUCTION Channel] Staging --> RedisC[(Redis Cache)] Prod --> RedisC RedisC -->|Hit| Return[Return Release Metadata] RedisC -->|Miss| DB[(MySQL)] DB --> RedisC DB --> API API --> Device

Staging Channel

Verified production builds automatically go to staging. This is a safe space to test before releasing to all devices.

  • The cache is updated when a release hits staging.
  • Devices get data from Redis whenever possible.
  • Least-used releases are evicted automatically.

Promotion to Production

Once a release passes staging, it can be explicitly promoted to production. This is permanent for live devices, though emergency rollbacks are possible.

stateDiagram-v2 Staging --> Production : Explicit Promote Production --> Revoked : Emergency Rollback

Rules & Safeguards

  • Version numbers always move forward duplicates are rejected.
  • Only one non-production release per project is allowed at a time.