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
- C++17 or higher
- ArduinoJson library version 7.0+
- ESP32/ESP8266 Arduino framework
- ESP32 or ESP8266 board
- WiFi connectivity
- Sufficient flash memory for OTA
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)
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:
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
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 notesreleasedDate- Release datefileSize- Human readable file sizefileSizeInBytes- File size in bytesboardType- Target board typeenvironment- Release environmentmessage- Status messagestatusCode- HTTP response code
Custom Backend Implementation
Backend Requirements
For the Two Endpoints Approach, your backend must implement:
version fieldapplication/octet-streamversion 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
// 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
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() {}
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");
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
License
MIT License - see the source file header for complete license text.