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:
- Release Information Endpoint: Returns JSON metadata about the latest firmware (must include
versionfield) - Download Endpoint: Serves the actual firmware binary file
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
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
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 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:
- Release Info Endpoint: Returns JSON with at minimum a
versionfield - Download Endpoint: Serves the firmware binary file with
application/octet-streamcontent type
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
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
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
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() {}
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
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");
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
statusCodefield - Update progress and errors are reported through Serial output
- Network connectivity issues are handled gracefully
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.