Back to Pretty OTA

This commit is contained in:
Carsten Graf
2025-07-26 02:03:35 +02:00
parent a85bd6227e
commit 0147ff2cd9
26 changed files with 3980 additions and 23 deletions

View File

@@ -0,0 +1,73 @@
/*
Copyright (c) 2025 Marc Schöndorf
This software is provided 'as-is', without any express or implied
warranty. In no event will the authors be held liable for any damages
arising from the use of this software.
Branding or white-labeling (changing the logo and name of PrettyOTA) is permitted only
with a commercial license. See README for details.
Permission is granted to anyone to use this software for private and commercial
applications, to alter it and redistribute it, subject to the following restrictions:
1. The origin of this software must not be misrepresented. You must not
claim that you wrote the original software. If you use this software
in a product, an acknowledgment is required.
2. Altered source versions must be plainly marked as such, and must not be
misrepresented as being the original software.
3. You are not allowed to change the logo or name of PrettyOTA without a commercial
license, even when redistributing modified source code.
4. This notice may not be removed or altered from any source distribution.
******************************************************
* PRETTY OTA *
* *
* A better looking Web-OTA. *
******************************************************
Description:
Custom type declarations.
*/
#pragma once
#include <cstdint>
namespace NSPrettyOTA
{
enum class UPDATE_MODE : uint8_t
{
FIRMWARE = 0,
FILESYSTEM
};
// Return type for ESPUpdateManager
enum class UPDATE_ERROR : uint8_t
{
OK = 0,
ABORT,
ERROR_OUT_OF_MEMORY,
ERROR_NO_PARTITION,
ERROR_NO_SPACE,
ERROR_INVALID_HASH,
ERROR_HASH_MISMATCH,
ERROR_READ,
ERROR_WRITE,
ERROR_ERASE,
ERROR_ACTIVATE,
ERROR_MAGIC_BYTE
};
// Return type for FirmwarePullManager
enum class PULL_RESULT : uint8_t
{
OK = 0,
NO_UPDATE_AVAILABLE = 1,
NO_CONFIGURATION_PROFILE_MATCH_FOUND = 2,
ERROR = 3
};
}

View File

@@ -0,0 +1,401 @@
/*
Copyright (c) 2025 Marc Schöndorf
This software is provided 'as-is', without any express or implied
warranty. In no event will the authors be held liable for any damages
arising from the use of this software.
Branding or white-labeling (changing the logo and name of PrettyOTA) is permitted only
with a commercial license. See README for details.
Permission is granted to anyone to use this software for private and commercial
applications, to alter it and redistribute it, subject to the following restrictions:
1. The origin of this software must not be misrepresented. You must not
claim that you wrote the original software. If you use this software
in a product, an acknowledgment is required.
2. Altered source versions must be plainly marked as such, and must not be
misrepresented as being the original software.
3. You are not allowed to change the logo or name of PrettyOTA without a commercial
license, even when redistributing modified source code.
4. This notice may not be removed or altered from any source distribution.
******************************************************
* PRETTY OTA *
* *
* A better looking Web-OTA. *
******************************************************
Description:
Internal handler for writing updates to ESP flash.
*/
#include "ESPUpdateManager.h"
using namespace NSPrettyOTA;
bool ESPUpdateManager::IsPartitionBootable(const esp_partition_t* const partition) const
{
if(!partition)
return false;
uint8_t partitionData[UM_ENCRYPTED_BLOCK_SIZE] = {0};
// Read beginning of partition
if(esp_partition_read(partition, 0, reinterpret_cast<uint32_t*>(partitionData), UM_ENCRYPTED_BLOCK_SIZE) != ESP_OK)
return false;
// Check header magic byte
if(partitionData[0] != ESP_IMAGE_HEADER_MAGIC)
return false;
return true;
}
bool ESPUpdateManager::CheckDataAlignment(const uint8_t* data, uint64_t size) const
{
// Only check 32-bit aligned blocks
if (size == 0 || size % sizeof(uint32_t))
return true;
uint64_t dwl = size / sizeof(uint32_t);
do {
if (*reinterpret_cast<const uint32_t*>(data) ^ 0xffffffff)
return true;
data += sizeof(uint32_t);
} while (--dwl);
return false;
}
void ESPUpdateManager::ResetState()
{
if(m_Buffer)
delete[] m_Buffer;
if(m_SkipBuffer)
delete[] m_SkipBuffer;
m_Buffer = nullptr;
m_SkipBuffer = nullptr;
m_UpdateMode = UPDATE_MODE::FIRMWARE;
m_UpdateSize = 0;
m_UpdateProgress = 0;
m_BufferSize = 0;
m_ExpectedMD5Hash = "";
m_TargetPartition = nullptr;
}
bool ESPUpdateManager::Begin(UPDATE_MODE updateMode, const char* const expectedMD5Hash, const char* SPIFFSPartitionLabel)
{
if(m_UpdateSize > 0) // Already running?
return false;
// Reset state
ResetState();
m_LastError = UPDATE_ERROR::OK;
m_ExpectedMD5Hash = expectedMD5Hash;
// Convert hash to lower case
for(char& c : m_ExpectedMD5Hash)
c = std::tolower(c);
// Check hash
if(m_ExpectedMD5Hash.length() != 32)
{
m_LastError = UPDATE_ERROR::ERROR_INVALID_HASH;
return false;
}
// Get target partition for update
if(updateMode == UPDATE_MODE::FIRMWARE)
{
m_TargetPartition = esp_ota_get_next_update_partition(nullptr);
if(!m_TargetPartition)
{
m_LastError = UPDATE_ERROR::ERROR_NO_PARTITION;
return false;
}
}
else if(updateMode == UPDATE_MODE::FILESYSTEM)
{
// Try finding SPIFFS partition (with given label) first
m_TargetPartition = esp_partition_find_first(ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_DATA_SPIFFS, SPIFFSPartitionLabel);
if(!m_TargetPartition)
{
// No SPIFFS partition (with given label) found.
// Fallback to searching for FAT partition (without a label)
m_TargetPartition = esp_partition_find_first(ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_DATA_FAT, nullptr);
if(!m_TargetPartition)
{
m_LastError = UPDATE_ERROR::ERROR_NO_PARTITION;
return false;
}
}
}
m_UpdateSize = m_TargetPartition->size;
m_UpdateMode = updateMode;
m_MD5Hasher.Begin();
// Initialize update buffer
m_Buffer = new (std::nothrow) uint8_t[SPI_FLASH_SEC_SIZE];
if(!m_Buffer)
{
m_LastError = UPDATE_ERROR::ERROR_OUT_OF_MEMORY;
return false;
}
return true;
}
bool ESPUpdateManager::End()
{
if(HasError() || m_UpdateSize == 0)
return false;
// Write remaining buffer
if(m_BufferSize > 0)
{
if(!WriteBufferToFlash())
return false;
}
m_MD5Hasher.Calculate();
m_UpdateSize = m_UpdateProgress;
// Compare expected hash to calculated firmware hash
if(m_ExpectedMD5Hash != m_MD5Hasher.GetHashAsString())
{
Abort(UPDATE_ERROR::ERROR_HASH_MISMATCH);
return false;
}
// Verify end of firmware
if(m_UpdateMode == UPDATE_MODE::FIRMWARE)
{
// Enable partition by writing the stashed buffer (first 16 bytes of partition)
if(esp_partition_write(m_TargetPartition, 0, reinterpret_cast<uint32_t*>(m_SkipBuffer), UM_ENCRYPTED_BLOCK_SIZE) != ESP_OK)
{
Abort(UPDATE_ERROR::ERROR_WRITE);
return false;
}
if(!IsPartitionBootable(m_TargetPartition))
{
Abort(UPDATE_ERROR::ERROR_READ);
return false;
}
// Set boot partition
if(esp_ota_set_boot_partition(m_TargetPartition) != ESP_OK)
{
Abort(UPDATE_ERROR::ERROR_ACTIVATE);
return false;
}
}
ResetState();
return true;
}
void ESPUpdateManager::Abort()
{
Abort(UPDATE_ERROR::ABORT);
}
void NSPrettyOTA::ESPUpdateManager::Abort(UPDATE_ERROR reason)
{
ResetState();
m_LastError = reason;
}
bool NSPrettyOTA::ESPUpdateManager::WriteBufferToFlash()
{
uint8_t skipSize = 0;
// Is it the beginning of new firmware?
if(m_UpdateProgress == 0 && m_UpdateMode == UPDATE_MODE::FIRMWARE)
{
// Check magic byte
if(m_Buffer[0] != ESP_IMAGE_HEADER_MAGIC)
{
Abort(UPDATE_ERROR::ERROR_MAGIC_BYTE);
return false;
}
// Stash the first 16 bytes of data and do not write them to flash now.
// The stashed 16 bytes will be written after all data has been written to flash.
// This way the partition stays invalid until all data and the stashed buffer has been written,
// to prevent booting a partial firmware in case the update didn't succeed.
skipSize = UM_ENCRYPTED_BLOCK_SIZE;
m_SkipBuffer = new (std::nothrow) uint8_t[skipSize];
if(!m_SkipBuffer)
{
Abort(UPDATE_ERROR::ERROR_OUT_OF_MEMORY);
return false;
}
// Copy beginning to skip buffer
memcpy(m_SkipBuffer, m_Buffer, skipSize);
}
const uint64_t offset = m_TargetPartition->address + m_UpdateProgress;
// If it's the block boundary, then erase the whole block from here
const bool eraseBlock = (m_UpdateSize - m_UpdateProgress >= UM_SPI_FLASH_BLOCK_SIZE) && (offset % UM_SPI_FLASH_BLOCK_SIZE == 0);
// Sector belongs to unaligned partition heading block
const bool partitionSectorHead = (m_TargetPartition->address % UM_SPI_FLASH_BLOCK_SIZE != 0) && (offset < (m_TargetPartition->address / UM_SPI_FLASH_BLOCK_SIZE + 1) * UM_SPI_FLASH_BLOCK_SIZE);
// Sector belongs to unaligned partition tailing block
const bool partitionSectorTail = (offset >= (m_TargetPartition->address + m_UpdateSize) / UM_SPI_FLASH_BLOCK_SIZE * UM_SPI_FLASH_BLOCK_SIZE);
if(eraseBlock || partitionSectorHead || partitionSectorTail)
{
if(esp_partition_erase_range(m_TargetPartition, m_UpdateProgress, eraseBlock ? UM_SPI_FLASH_BLOCK_SIZE : SPI_FLASH_SEC_SIZE) != ESP_OK)
{
Abort(UPDATE_ERROR::ERROR_ERASE);
return false;
}
}
// Try skipping empty blocks on unencrypted partitions
if ((m_TargetPartition->encrypted || CheckDataAlignment(m_Buffer + (skipSize / sizeof(uint32_t)), m_BufferSize - skipSize))
&& (esp_partition_write(m_TargetPartition, m_UpdateProgress + skipSize, reinterpret_cast<const uint32_t*>(m_Buffer) + (skipSize / sizeof(uint32_t)), m_BufferSize - skipSize) != ESP_OK))
{
Abort(UPDATE_ERROR::ERROR_WRITE);
return false;
}
// Restore magic byte or MD5 hash will be wrong
if((m_UpdateProgress == 0) && (m_UpdateMode == UPDATE_MODE::FIRMWARE))
m_Buffer[0] = ESP_IMAGE_HEADER_MAGIC;
// Add data to hasher
m_MD5Hasher.AddData(m_Buffer, m_BufferSize);
m_UpdateProgress += m_BufferSize;
m_BufferSize = 0;
return true;
}
uint64_t ESPUpdateManager::Write(const uint8_t* const data, uint64_t size)
{
if(HasError() || m_UpdateSize == 0)
return 0;
if(size > (m_UpdateSize - m_UpdateProgress))
{
Abort(UPDATE_ERROR::ERROR_NO_SPACE);
return 0;
}
uint64_t bytesLeft = size;
while((m_BufferSize + bytesLeft) > SPI_FLASH_SEC_SIZE)
{
const uint64_t toCopy = SPI_FLASH_SEC_SIZE - m_BufferSize;
memcpy(m_Buffer + m_BufferSize, data + (size - bytesLeft), toCopy);
m_BufferSize += toCopy;
if(!WriteBufferToFlash())
return (size - bytesLeft);
bytesLeft -= toCopy;
}
memcpy(m_Buffer + m_BufferSize, data + (size - bytesLeft), bytesLeft);
m_BufferSize += bytesLeft;
if(m_BufferSize == (m_UpdateSize - m_UpdateProgress))
{
if(!WriteBufferToFlash())
return (size - bytesLeft);
}
return size;
}
bool ESPUpdateManager::IsRollbackPossible() const
{
if(m_Buffer) // Update is running
return false;
const esp_partition_t* const partition = esp_ota_get_next_update_partition(nullptr);
return IsPartitionBootable(partition);
}
bool ESPUpdateManager::DoRollback()
{
if(m_Buffer) // Update is running
return false;
const esp_partition_t* const partition = esp_ota_get_next_update_partition(nullptr);
if(!IsPartitionBootable(partition))
return false;
if(esp_ota_set_boot_partition(partition) != ESP_OK)
return false;
return true;
}
std::string NSPrettyOTA::ESPUpdateManager::GetLastErrorAsString() const
{
switch(m_LastError)
{
case UPDATE_ERROR::OK:
return "No error";
break;
case UPDATE_ERROR::ABORT:
return "Aborted";
break;
case UPDATE_ERROR::ERROR_OUT_OF_MEMORY:
return "ERROR_OUT_OF_MEMORY: No available memory for allocation";
break;
case UPDATE_ERROR::ERROR_NO_PARTITION:
return "ERROR_NO_PARTITION: Partition could not be found";
break;
case UPDATE_ERROR::ERROR_NO_SPACE:
return "ERROR_NO_SPACE: Not enough free space";
break;
case UPDATE_ERROR::ERROR_INVALID_HASH:
return "ERROR_INVALID_HASH: Invalid MD5 hash";
break;
case UPDATE_ERROR::ERROR_HASH_MISMATCH:
return "ERROR_HASH_MISMATCH: The firmware hash does not match the expected hash";
break;
case UPDATE_ERROR::ERROR_READ:
return "ERROR_READ: Could not read flash";
break;
case UPDATE_ERROR::ERROR_WRITE:
return "ERROR_WRITE: Could not write flash";
break;
case UPDATE_ERROR::ERROR_ERASE:
return "ERROR_ERASE: Could not erase flash";
break;
case UPDATE_ERROR::ERROR_ACTIVATE:
return "ERROR_ACTIVATE: Could not activate target partition";
break;
case UPDATE_ERROR::ERROR_MAGIC_BYTE:
return "ERROR_MAGIC_BYTE: Magic byte is invalid";
break;
default:
return "Unknown";
break;
}
}

View File

@@ -0,0 +1,106 @@
/*
Copyright (c) 2025 Marc Schöndorf
This software is provided 'as-is', without any express or implied
warranty. In no event will the authors be held liable for any damages
arising from the use of this software.
Branding or white-labeling (changing the logo and name of PrettyOTA) is permitted only
with a commercial license. See README for details.
Permission is granted to anyone to use this software for private and commercial
applications, to alter it and redistribute it, subject to the following restrictions:
1. The origin of this software must not be misrepresented. You must not
claim that you wrote the original software. If you use this software
in a product, an acknowledgment is required.
2. Altered source versions must be plainly marked as such, and must not be
misrepresented as being the original software.
3. You are not allowed to change the logo or name of PrettyOTA without a commercial
license, even when redistributing modified source code.
4. This notice may not be removed or altered from any source distribution.
******************************************************
* PRETTY OTA *
* *
* A better looking Web-OTA. *
******************************************************
Description:
Internal handler for writing updates to ESP flash.
*/
#pragma once
// C++ API
#include <cstdint>
#include <functional>
#include <string> // For std::string
#include <string.h> // For memcpy, memset
// ESP-IDF
#include <esp_err.h>
#include <esp_spi_flash.h>
#include <esp_partition.h>
#include <esp_ota_ops.h>
#include <esp_app_format.h>
// PrettyOTA
#include "CustomTypes.h"
#include "MD5Hasher.h"
namespace NSPrettyOTA
{
class ESPUpdateManager
{
private:
// Constants
static const uint8_t UM_ENCRYPTED_BLOCK_SIZE = 16;
static const uint8_t UM_SPI_SECTORS_PER_BLOCK = 16;
static const uint32_t UM_SPI_FLASH_BLOCK_SIZE = (UM_SPI_SECTORS_PER_BLOCK * SPI_FLASH_SEC_SIZE);
private:
UPDATE_ERROR m_LastError = UPDATE_ERROR::OK;
UPDATE_MODE m_UpdateMode = UPDATE_MODE::FIRMWARE;
uint64_t m_UpdateSize = 0;
uint64_t m_UpdateProgress = 0;
uint64_t m_BufferSize = 0;
uint8_t* m_Buffer = nullptr;
uint8_t* m_SkipBuffer = nullptr;
std::string m_ExpectedMD5Hash = "";
MD5Hasher m_MD5Hasher;
const esp_partition_t* m_TargetPartition = nullptr;
// Methods
void ResetState();
void Abort(UPDATE_ERROR reason);
bool WriteBufferToFlash();
// Helper
bool IsPartitionBootable(const esp_partition_t* const partition) const;
bool CheckDataAlignment(const uint8_t* data, uint64_t size) const;
public:
ESPUpdateManager() = default;
bool Begin(UPDATE_MODE updateMode, const char* const expectedMD5Hash, const char* const SPIFFSPartitionLabel = nullptr);
bool End();
void Abort();
uint64_t Write(const uint8_t* const data, uint64_t size);
bool HasError() const { return (m_LastError != UPDATE_ERROR::OK); }
UPDATE_ERROR GetLastError() const { return m_LastError; }
std::string GetLastErrorAsString() const;
bool IsRollbackPossible() const;
bool DoRollback();
};
}

View File

@@ -0,0 +1,281 @@
/*
Copyright (c) 2025 Marc Schöndorf
This software is provided 'as-is', without any express or implied
warranty. In no event will the authors be held liable for any damages
arising from the use of this software.
Branding or white-labeling (changing the logo and name of PrettyOTA) is permitted only
with a commercial license. See README for details.
Permission is granted to anyone to use this software for private and commercial
applications, to alter it and redistribute it, subject to the following restrictions:
1. The origin of this software must not be misrepresented. You must not
claim that you wrote the original software. If you use this software
in a product, an acknowledgment is required.
2. Altered source versions must be plainly marked as such, and must not be
misrepresented as being the original software.
3. You are not allowed to change the logo or name of PrettyOTA without a commercial
license, even when redistributing modified source code.
4. This notice may not be removed or altered from any source distribution.
******************************************************
* PRETTY OTA *
* *
* A better looking Web-OTA. *
******************************************************
Description:
The main source file.
*/
/*
====================================================================================
Example Json file:
{
"Configuration": [
{
"HardwareID": ["Board1", "Board2", "Board3"],
"CustomFilter": "custom1",
"Version": "2.0.0",
"FirmwareURL": "https://mydomain.com/firmware/board1_2_3.fw_v2_0_0.bin"
},
{
"HardwareID": "Board10",
"Version": "3.0.0",
"FirmwareURL": "https://mydomain.com/firmware/board10.fw_v3_0_0.bin"
},
{
"HardwareID": "Board99",
"CustomFilter": "custom99",
"Version": "10.0.0",
"FirmwareURL": "https://mydomain.com/firmware/board99.fw_v10_0_0.bin"
},
{
"HardwareID": "Invalid",
"Version": "10.0.0",
"FirmwareURL": ""
}
]
}
====================================================================================
*/
#include "FirmwarePullManager.h"
using namespace NSPrettyOTA;
void FirmwarePullManager::Begin(Stream* const serialStream, std::function<void(NSPrettyOTA::UPDATE_MODE updateMode)> onStart,
std::function<void(uint32_t currentSize, uint32_t totalSize)> onProgress,
std::function<void(bool successful)> onEnd)
{
m_SerialMonitorStream = serialStream;
m_OnStartUpdate = onStart;
m_OnProgressUpdate = onProgress;
m_OnEndUpdate = onEnd;
}
void FirmwarePullManager::Log(const std::string& message)
{
if(!m_SerialMonitorStream)
return;
m_SerialMonitorStream->println(("[FirmwarePullManager] " + message).c_str());
}
PULL_RESULT FirmwarePullManager::CheckForNewFirmwareAvailable(const char* const jsonURL, std::string& out_firmwareURL)
{
out_firmwareURL = "";
this->Log("Info: Checking for new firmware version...");
HTTPClient http;
http.useHTTP10(true);
http.setFollowRedirects(HTTPC_FORCE_FOLLOW_REDIRECTS);
if(!http.begin(jsonURL))
{
this->Log("Error: Could not initialize HTTPClient");
return PULL_RESULT::ERROR;
}
// Send HTTP GET request
const int32_t response = http.GET();
if(response != 200)
{
http.end();
this->Log("Error (Json download): Server replied with HTTP code: " + std::to_string(response));
return PULL_RESULT::ERROR;
}
// Get received data as stream
Stream* const stream = http.getStreamPtr();
if(!stream)
{
http.end();
this->Log("Error: Received Json is empty");
return PULL_RESULT::ERROR;
}
// Parse received Json
JsonDocument json;
const DeserializationError jsonError = deserializeJson(json, *stream);
if(jsonError)
{
http.end();
this->Log("Error: Could not parse Json (" + std::string(jsonError.c_str()) + ")");
return PULL_RESULT::ERROR;
}
// End connection
http.end();
// Are there any "Configuration" entries
if(json["Configuration"].as<JsonArray>().size() == 0)
{
this->Log("Error (Json): No valid \"Configuration\" entries found");
return PULL_RESULT::ERROR;
}
// Search if Json contains matching profile
// Iterate all "Configuration" entries
for (const auto configuration : json["Configuration"].as<JsonArray>())
{
// **********************************************************
// Search matching HardwareID
bool foundHardwareIDMatch = false;
if(configuration["HardwareID"].is<JsonArray>()) // HardwareID is array
{
for (auto i : configuration["HardwareID"].as<JsonArray>())
{
if(i.as<std::string>() == m_HardwareID)
{
foundHardwareIDMatch = true;
break;
}
}
}
else if(configuration["HardwareID"].is<std::string>()) // HardwareID is single string (only one HardwareID)
{
if(configuration["HardwareID"].as<std::string>() == m_HardwareID)
foundHardwareIDMatch = true;
}
else // HardwareID entry not found
{
this->Log("Error (Json): No valid \"HardwareID\" found in \"Configuration\". Skipping entry...");
continue;
}
// Go to next "Configuration" if no HardwareID matched
if(!foundHardwareIDMatch)
continue;
// **********************************************************
// Search matching CustomFilter
bool foundCustomFilterMatch = false;
if(configuration["CustomFilter"].is<std::string>())
{
if(configuration["CustomFilter"].as<std::string>() == m_CustomFilter)
foundCustomFilterMatch = true;
}
else
{
// If no CustomFilter entry is present, set as match
foundCustomFilterMatch = true;
}
// Go to next "Configuration" if no CustomFilter matched
if(!foundCustomFilterMatch)
continue;
// **********************************************************
// Check if version is present
if(!configuration["Version"].is<std::string>() || configuration["Version"].as<std::string>().length() == 0)
{
this->Log("Error (Json): No valid \"Version\" found in \"Configuration\". Skipping entry...");
continue;
}
// Check if version is newer (or different if downgrade is allowed)
bool newVersionAvailable = false;
if(m_AllowDowngrade)
{
if(configuration["Version"].as<std::string>() != m_CurrentAppVersion)
newVersionAvailable = true;
}
else
{
// Use lexicographical comparison
if(configuration["Version"].as<std::string>() > m_CurrentAppVersion)
newVersionAvailable = true;
}
if(!newVersionAvailable)
{
this->Log("Info: No updated firmware version available (Current: " + std::string(m_CurrentAppVersion) + ", New: " + configuration["Version"].as<std::string>() + ")");
return PULL_RESULT::NO_UPDATE_AVAILABLE;
}
this->Log("Info: New firmware version available (Current: " + std::string(m_CurrentAppVersion) + ", New: " + configuration["Version"].as<std::string>() + ")");
// **********************************************************
// Get firmware URL
if(!configuration["FirmwareURL"].is<std::string>() || configuration["FirmwareURL"].as<std::string>().length() == 0)
{
this->Log("Error (Json): No valid \"FirmwareURL\" found in \"Configuration\"");
continue;
}
out_firmwareURL = configuration["FirmwareURL"].as<std::string>();
return PULL_RESULT::OK;
}
this->Log("Warning: No matching profile found in Json");
return PULL_RESULT::NO_CONFIGURATION_PROFILE_MATCH_FOUND;
}
PULL_RESULT FirmwarePullManager::RunPullUpdate(const char* const jsonURL)
{
std::string firmwareURL = "";
const PULL_RESULT result = CheckForNewFirmwareAvailable(jsonURL, firmwareURL);
if(result != PULL_RESULT::OK)
return result;
// Download firmware file
HTTPClient http;
http.useHTTP10(true);
http.setFollowRedirects(HTTPC_FORCE_FOLLOW_REDIRECTS);
if(!http.begin(firmwareURL.c_str()))
{
this->Log("Error: Could not initialize HTTPClient");
return PULL_RESULT::ERROR;
}
// Send HTTP GET request
const int32_t response = http.GET();
if(response != 200)
{
http.end();
this->Log("Error (firmware download): Server replied with HTTP code: " + std::to_string(response));
return PULL_RESULT::ERROR;
}
const int32_t firmwareSize = http.getSize();
uint8_t buffer[1280] = { 0 };
// End connection
http.end();
return PULL_RESULT::OK;
}

View File

@@ -0,0 +1,85 @@
/*
Copyright (c) 2025 Marc Schöndorf
This software is provided 'as-is', without any express or implied
warranty. In no event will the authors be held liable for any damages
arising from the use of this software.
Branding or white-labeling (changing the logo and name of PrettyOTA) is permitted only
with a commercial license. See README for details.
Permission is granted to anyone to use this software for private and commercial
applications, to alter it and redistribute it, subject to the following restrictions:
1. The origin of this software must not be misrepresented. You must not
claim that you wrote the original software. If you use this software
in a product, an acknowledgment is required.
2. Altered source versions must be plainly marked as such, and must not be
misrepresented as being the original software.
3. You are not allowed to change the logo or name of PrettyOTA without a commercial
license, even when redistributing modified source code.
4. This notice may not be removed or altered from any source distribution.
******************************************************
* PRETTY OTA *
* *
* A better looking Web-OTA. *
******************************************************
Description:
Internal handler for writing updates to ESP flash.
*/
#pragma once
// C++ API
#include <cstdint>
//#include <functional>
#include <string> // For std::string
// Arduino dependencies
#include <HTTPClient.h>
#include <ArduinoJson.h>
// PrettyOTA
#include "CustomTypes.h"
namespace NSPrettyOTA
{
class FirmwarePullManager
{
private:
Stream* m_SerialMonitorStream = nullptr;
bool m_AllowDowngrade = false;
std::string m_HardwareID = "";
std::string m_CustomFilter = "";
std::string m_CurrentAppVersion = "";
// User callbacks
std::function<void(NSPrettyOTA::UPDATE_MODE updateMode)> m_OnStartUpdate = nullptr;
std::function<void(uint32_t currentSize, uint32_t totalSize)> m_OnProgressUpdate = nullptr;
std::function<void(bool successful)> m_OnEndUpdate = nullptr;
void Log(const std::string& message);
public:
FirmwarePullManager() = default;
void Begin(Stream* const serialStream,
std::function<void(NSPrettyOTA::UPDATE_MODE updateMode)> onStart,
std::function<void(uint32_t currentSize, uint32_t totalSize)> onProgress,
std::function<void(bool successful)> onEnd);
PULL_RESULT CheckForNewFirmwareAvailable(const char* const jsonURL, std::string& out_firmwareURL);
PULL_RESULT RunPullUpdate(const char* const jsonURL);
void SetHardwareID(const char* const hardwareID) { m_HardwareID = hardwareID; }
void SetCustomFilter(const char* const customFilter) { m_CustomFilter = customFilter; }
void SetCurrentAppVersion(const char* const currentAppVersion) { m_CurrentAppVersion = currentAppVersion; }
void SetAllowDowngrade(bool allowDowngrade) { m_AllowDowngrade = allowDowngrade; }
};
}

View File

@@ -0,0 +1,74 @@
/*
Copyright (c) 2025 Marc Schöndorf
This software is provided 'as-is', without any express or implied
warranty. In no event will the authors be held liable for any damages
arising from the use of this software.
Branding or white-labeling (changing the logo and name of PrettyOTA) is permitted only
with a commercial license. See README for details.
Permission is granted to anyone to use this software for private and commercial
applications, to alter it and redistribute it, subject to the following restrictions:
1. The origin of this software must not be misrepresented. You must not
claim that you wrote the original software. If you use this software
in a product, an acknowledgment is required.
2. Altered source versions must be plainly marked as such, and must not be
misrepresented as being the original software.
3. You are not allowed to change the logo or name of PrettyOTA without a commercial
license, even when redistributing modified source code.
4. This notice may not be removed or altered from any source distribution.
******************************************************
* PRETTY OTA *
* *
* A better looking Web-OTA. *
******************************************************
Description:
Helper class for building MD5 hashes.
*/
#include "MD5Hasher.h"
using namespace NSPrettyOTA;
void MD5Hasher::Begin()
{
memset(m_Buffer, 0x00, ESP_ROM_MD5_DIGEST_LEN);
esp_rom_md5_init(&m_Context);
}
void MD5Hasher::AddData(const uint8_t* data, uint32_t size)
{
esp_rom_md5_update(&m_Context, data, size);
}
void MD5Hasher::AddData(const char* data, uint32_t size)
{
AddData(reinterpret_cast<const uint8_t*>(data), size);
}
void MD5Hasher::Calculate()
{
esp_rom_md5_final(m_Buffer, &m_Context);
}
void MD5Hasher::GetHashAsBytes(uint8_t out[ESP_ROM_MD5_DIGEST_LEN]) const
{
memcpy(out, m_Buffer, ESP_ROM_MD5_DIGEST_LEN);
}
std::string MD5Hasher::GetHashAsString() const
{
char out_MD5Str[MD5_HASH_STR_SIZE];
for(uint8_t i = 0; i < ESP_ROM_MD5_DIGEST_LEN; i++)
sprintf(out_MD5Str + (i * 2), "%02x", m_Buffer[i]);
return out_MD5Str;
}

View File

@@ -0,0 +1,65 @@
/*
Copyright (c) 2025 Marc Schöndorf
This software is provided 'as-is', without any express or implied
warranty. In no event will the authors be held liable for any damages
arising from the use of this software.
Branding or white-labeling (changing the logo and name of PrettyOTA) is permitted only
with a commercial license. See README for details.
Permission is granted to anyone to use this software for private and commercial
applications, to alter it and redistribute it, subject to the following restrictions:
1. The origin of this software must not be misrepresented. You must not
claim that you wrote the original software. If you use this software
in a product, an acknowledgment is required.
2. Altered source versions must be plainly marked as such, and must not be
misrepresented as being the original software.
3. You are not allowed to change the logo or name of PrettyOTA without a commercial
license, even when redistributing modified source code.
4. This notice may not be removed or altered from any source distribution.
******************************************************
* PRETTY OTA *
* *
* A better looking Web-OTA. *
******************************************************
Description:
Helper class for building MD5 hashes.
*/
#pragma once
#include <string> // For std::string
#include <string.h> // For memcpy, memset
#include <esp_system.h>
#include <esp_rom_md5.h>
namespace NSPrettyOTA
{
class MD5Hasher
{
private:
md5_context_t m_Context;
uint8_t m_Buffer[ESP_ROM_MD5_DIGEST_LEN] = {0};
public:
static const uint8_t MD5_HASH_STR_SIZE = (2 * ESP_ROM_MD5_DIGEST_LEN + 1);
MD5Hasher() = default;
void Begin();
void AddData(const uint8_t* data, uint32_t size);
void AddData(const char* data, uint32_t size);
void Calculate();
void GetHashAsBytes(uint8_t out[ESP_ROM_MD5_DIGEST_LEN]) const;
std::string GetHashAsString() const;
};
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,222 @@
/*
Copyright (c) 2025 Marc Schöndorf
This software is provided 'as-is', without any express or implied
warranty. In no event will the authors be held liable for any damages
arising from the use of this software.
Branding or white-labeling (changing the logo and name of PrettyOTA) is permitted only
with a commercial license. See README for details.
Permission is granted to anyone to use this software for private and commercial
applications, to alter it and redistribute it, subject to the following restrictions:
1. The origin of this software must not be misrepresented. You must not
claim that you wrote the original software. If you use this software
in a product, an acknowledgment is required.
2. Altered source versions must be plainly marked as such, and must not be
misrepresented as being the original software.
3. You are not allowed to change the logo or name of PrettyOTA without a commercial
license, even when redistributing modified source code.
4. This notice may not be removed or altered from any source distribution.
******************************************************
* PRETTY OTA *
* *
* A better looking Web-OTA. *
******************************************************
Description:
The main header file. Include this file in your project.
*/
#pragma once
// ********************************************************
// Settings
#define PRETTY_OTA_ENABLE_ARDUINO_OTA 1
#define DEV_PRETTY_OTA_ENABLE_FIRMWARE_PULLING 0 // Do not change
// std-lib
#include <string>
#include <vector>
#include <new> //std::nothrow
// Arduino include
#include <Arduino.h>
#include <ArduinoJson.h>
#if (PRETTY_OTA_ENABLE_ARDUINO_OTA == 1)
#include <ArduinoOTA.h>
#endif
// ESP-IDF
#include <esp_err.h>
#include <esp_ota_ops.h>
#include <nvs.h>
#include <nvs_flash.h>
#include <mdns.h>
// Arduino dependencies
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
// PrettyOTA includes
#include "CustomTypes.h"
#include "MD5Hasher.h"
#include "ESPUpdateManager.h"
#if (DEV_PRETTY_OTA_ENABLE_FIRMWARE_PULLING == 1)
#include "FirmwarePullManager.h"
#endif
// ********************************************************
// Compile checks
#ifndef ESP32
#error PrettyOTA only supports ESP32 devices. Support for RaspberryPi Pico W will follow soon.
#endif
// Is it the correct version and fork of ESP32AsyncWebServer?
#if !defined(ASYNCWEBSERVER_VERSION) || ASYNCWEBSERVER_VERSION_MAJOR < 3
#error PrettyOTA needs the "ESPAsyncWebServer" library (from ESP32Async) version 3.0 or newer. If you have it installed, make sure you only have one library with the name "ESPAsyncWebServer" installed (there are two libraries with the same name).
#endif
// Is it the correct version of ArduinoJson?
#if !defined(ARDUINOJSON_VERSION_MAJOR) || ARDUINOJSON_VERSION_MAJOR < 7
#error PrettyOTA needs the "ArduinoJson" library version 7.0 or newer.
#endif
class PrettyOTA
{
private:
// Constants
static const uint8_t PRETTY_OTA_VERSION_MAJOR = 1;
static const uint8_t PRETTY_OTA_VERSION_MINOR = 1;
static const uint8_t PRETTY_OTA_VERSION_REVISION = 3;
static const uint32_t BACKGROUND_TASK_STACK_SIZE = 3072;
static const uint8_t BACKGROUND_TASK_PRIORITY = 4;
static const uint8_t MAX_NUM_LOGGED_IN_CLIENTS = 5;
// Website code
static const uint8_t PRETTY_OTA_WEBSITE_DATA[12706];
static const uint8_t PRETTY_OTA_LOGIN_DATA[6208];
private:
// UUID generation
using UUID_t = uint8_t[16];
void GenerateUUID(UUID_t* out_uuid) const;
std::string UUIDToString(const UUID_t uuid) const;
private:
// Variables
static std::string m_AppBuildTime;
static std::string m_AppBuildDate;
static std::string m_AppVersion;
static std::string m_HardwareID;
std::string m_LoginURL = "";
std::string m_MainURL = "";
static Stream* m_SerialMonitorStream;
AsyncWebServer* m_Server = nullptr;
NSPrettyOTA::ESPUpdateManager m_UpdateManager;
#if (DEV_PRETTY_OTA_ENABLE_FIRMWARE_PULLING == 1)
NSPrettyOTA::FirmwarePullManager m_FirmwarePullManager;
#endif
bool m_IsInitialized = false;
bool m_IsUpdateRunning = false;
bool m_AutoRebootEnabled = true;
bool m_RequestReboot = false;
static bool m_DefaultCallbackPrintWithColor;
uint32_t m_RebootRequestTime = 0;
// Authentication
bool m_AuthenticationEnabled = false;
std::string m_Username = "";
std::string m_Password = "";
std::vector<std::string> m_AuthenticatedSessionIDs;
// User callbacks
std::function<void(NSPrettyOTA::UPDATE_MODE updateMode)> m_OnStartUpdate = nullptr;
std::function<void(uint32_t currentSize, uint32_t totalSize)> m_OnProgressUpdate = nullptr;
std::function<void(bool successful)> m_OnEndUpdate = nullptr;
private:
// Default callback functions
static void OnOTAStart(NSPrettyOTA::UPDATE_MODE updateMode);
static void OnOTAProgress(uint32_t currentSize, uint32_t totalSize);
static void OnOTAEnd(bool successful);
// Log functions
static void P_LOG_I(const std::string& message);
static void P_LOG_W(const std::string& message);
static void P_LOG_E(const std::string& message);
// Methods
static void BackgroundTask(void* parameter);
void EnableArduinoOTA(const char* const password, bool passwordIsMD5Hash, uint16_t OTAport);
bool IsAuthenticated(const AsyncWebServerRequest* const request) const;
// NVS storage
bool SaveSessionIDsToNVS();
bool LoadSessionIDsFromNVS();
// Helper
std::string GetVersionAsString() const;
//std::string SHA256ToString(const uint8_t hash[32]) const;
public:
PrettyOTA() = default;
bool Begin(AsyncWebServer* const server, const char* const username = "", const char* const password = "", bool passwordIsMD5Hash = false, const char* const mainURL = "/update", const char* const loginURL = "/login", uint16_t OTAport = 3232);
void SetAuthenticationDetails(const char* const username, const char* const password, bool passwordIsMD5Hash = false);
#if (DEV_PRETTY_OTA_ENABLE_FIRMWARE_PULLING == 1)
bool DoFirmwarePull(const char* const customFilter);
#endif
// Is an update running? (web interface or pulling in background)
bool IsUpdateRunning() const { return m_IsUpdateRunning; }
// Set user callbacks
void OnStart(std::function<void(NSPrettyOTA::UPDATE_MODE updateMode)> func) { m_OnStartUpdate = func; }
void OnProgress(std::function<void(uint32_t currentSize, uint32_t totalSize)> func) { m_OnProgressUpdate = func; }
void OnEnd(std::function<void(bool successful)> func) { m_OnEndUpdate = func; }
// Use built in callbacks that print info to the serial monitor
void UseDefaultCallbacks(bool printWithColor = false);
// Set the HardwareID. It should be a unique identifier for your hardware/board
void SetHardwareID(const char* const hardwareID) { m_HardwareID = hardwareID; }
// Set app version
static void SetAppVersion(const char* const appVersion);
// Alias for backwards compatibility. DO NOT USE
[[deprecated("Use SetAppVersion() instead.")]]
static constexpr auto OverwriteAppVersion = SetAppVersion;
// Set build time and date
static void SetAppBuildTimeAndDate(const char* const appBuildTime, const char* const appBuildDate);
// Alias for backwards compatibility. DO NOT USE
[[deprecated("Use SetAppBuildTimeAndDate() instead.")]]
static constexpr auto OverwriteAppBuildTimeAndDate = SetAppBuildTimeAndDate;
// Set the Stream to write log messages too (Example: Use &Serial as argument)
void SetSerialOutputStream(Stream* const serialStream) { m_SerialMonitorStream = serialStream; }
};
// ********************************************************
// Helper macro to be able to set build time and date when using ArduinoIDE.
// This is not required for PlatformIO, however you can use it to overwrite the
// build time and date read by PrettyOTA from the firmware image itself
// using esp_ota_get_app_description().
#define PRETTY_OTA_SET_CURRENT_BUILD_TIME_AND_DATE() PrettyOTA::SetAppBuildTimeAndDate(__TIME__, __DATE__)

View File

@@ -0,0 +1,131 @@
/*
Copyright (c) 2025 Marc Schöndorf
This software is provided 'as-is', without any express or implied
warranty. In no event will the authors be held liable for any damages
arising from the use of this software.
Branding or white-labeling (changing the logo and name of PrettyOTA) is permitted only
with a commercial license. See README for details.
Permission is granted to anyone to use this software for private and commercial
applications, to alter it and redistribute it, subject to the following restrictions:
1. The origin of this software must not be misrepresented. You must not
claim that you wrote the original software. If you use this software
in a product, an acknowledgment is required.
2. Altered source versions must be plainly marked as such, and must not be
misrepresented as being the original software.
3. You are not allowed to change the logo or name of PrettyOTA without a commercial
license, even when redistributing modified source code.
4. This notice may not be removed or altered from any source distribution.
******************************************************
* PRETTY OTA *
* *
* A better looking Web-OTA. *
******************************************************
Description:
Default callbacks for PrettyOTA.
*/
#include "PrettyOTA.h"
const char* const ROW_OF_STARS = "************************************************";
// ********************************************************
// OTA default callbacks
void PrettyOTA::OnOTAStart(NSPrettyOTA::UPDATE_MODE updateMode)
{
if (!m_SerialMonitorStream)
return;
m_SerialMonitorStream->println("\n");
m_SerialMonitorStream->println(ROW_OF_STARS);
if(m_DefaultCallbackPrintWithColor)
m_SerialMonitorStream->println("* \033[1;7m OTA UPDATE \033[0m *");
else
m_SerialMonitorStream->println("* OTA UPDATE *");
if(m_DefaultCallbackPrintWithColor)
{
if(updateMode == NSPrettyOTA::UPDATE_MODE::FIRMWARE)
m_SerialMonitorStream->println("* \033[1mFirmware\033[0m *");
else
m_SerialMonitorStream->println("* \033[1mFilesystem\033[0m *");
}
else
{
if(updateMode == NSPrettyOTA::UPDATE_MODE::FIRMWARE)
m_SerialMonitorStream->println("* Firmware *");
else
m_SerialMonitorStream->println("* Filesystem *");
}
m_SerialMonitorStream->println(ROW_OF_STARS);
m_SerialMonitorStream->println("\n");
m_SerialMonitorStream->println("Starting OTA update...");
}
void PrettyOTA::OnOTAProgress(uint32_t currentSize, uint32_t totalSize)
{
if (!m_SerialMonitorStream)
return;
static float lastPercentage = 0.0f;
const float percentage = 100.0f * static_cast<float>(currentSize) / static_cast<float>(totalSize);
const uint8_t numBarsToShow = static_cast<uint8_t>(percentage / 3.3333f);
if(percentage - lastPercentage >= 2.0f)
{
// Print progress bar
m_SerialMonitorStream->print("Updating... [");
for(uint8_t i = 0; i < 30; i++)
{
if (i < numBarsToShow)
m_SerialMonitorStream->print("=");
else
m_SerialMonitorStream->print(" ");
}
m_SerialMonitorStream->printf("] %02u%%\n", static_cast<uint8_t>(percentage));
if(m_DefaultCallbackPrintWithColor)
m_SerialMonitorStream->print("\033[1F"); // Move cursor to begining of previous line
lastPercentage = percentage;
}
}
void PrettyOTA::OnOTAEnd(bool successful)
{
if (!m_SerialMonitorStream)
return;
if (successful)
m_SerialMonitorStream->println("Updating... [==============================] 100%");
m_SerialMonitorStream->println("");
m_SerialMonitorStream->println(ROW_OF_STARS);
if(m_DefaultCallbackPrintWithColor)
{
if (successful)
m_SerialMonitorStream->println("* \033[1;92;7m OTA UPDATE SUCCESSFUL \033[0m *");
else
m_SerialMonitorStream->println("* \033[1;91;7m OTA UPDATE FAILED \033[0m *");
}
else
{
if (successful)
m_SerialMonitorStream->println("* OTA UPDATE SUCCESSFUL *");
else
m_SerialMonitorStream->println("* OTA UPDATE FAILED *");
}
m_SerialMonitorStream->println(ROW_OF_STARS);
m_SerialMonitorStream->println("");
}

View File

@@ -0,0 +1,106 @@
/*
Copyright (c) 2025 Marc Schöndorf
This software is provided 'as-is', without any express or implied
warranty. In no event will the authors be held liable for any damages
arising from the use of this software.
Branding or white-labeling (changing the logo and name of PrettyOTA) is permitted only
with a commercial license. See README for details.
Permission is granted to anyone to use this software for private and commercial
applications, to alter it and redistribute it, subject to the following restrictions:
1. The origin of this software must not be misrepresented. You must not
claim that you wrote the original software. If you use this software
in a product, an acknowledgment is required.
2. Altered source versions must be plainly marked as such, and must not be
misrepresented as being the original software.
3. You are not allowed to change the logo or name of PrettyOTA without a commercial
license, even when redistributing modified source code.
4. This notice may not be removed or altered from any source distribution.
******************************************************
* PRETTY OTA *
* *
* A better looking Web-OTA. *
******************************************************
Description:
Utils and helpers for PrettyOTA.
*/
#include "PrettyOTA.h"
// ********************************************************
// Log functions
void PrettyOTA::P_LOG_I(const std::string& message)
{
if (!m_SerialMonitorStream)
return;
m_SerialMonitorStream->println(("[PrettyOTA] Info: " + message).c_str());
}
void PrettyOTA::P_LOG_W(const std::string& message)
{
if (!m_SerialMonitorStream)
return;
m_SerialMonitorStream->println(("[PrettyOTA] Warning: " + message).c_str());
}
void PrettyOTA::P_LOG_E(const std::string& message)
{
if (!m_SerialMonitorStream)
return;
m_SerialMonitorStream->println(("[PrettyOTA] Error: " + message).c_str());
}
// ********************************************************
// Get PrettyOTA version string
std::string PrettyOTA::GetVersionAsString() const
{
return std::to_string(PRETTY_OTA_VERSION_MAJOR) + "." +
std::to_string(PRETTY_OTA_VERSION_MINOR) + "." +
std::to_string(PRETTY_OTA_VERSION_REVISION);
}
// ********************************************************
// UUID helpers
void PrettyOTA::GenerateUUID(UUID_t* out_uuid) const
{
esp_fill_random(*out_uuid, sizeof(UUID_t));
(*out_uuid)[6] = 0x40 | ((*out_uuid)[6] & 0xF); // UUID version
(*out_uuid)[8] = (0x80 | (*out_uuid)[8]) & ~0x40; // UUID variant
}
std::string PrettyOTA::UUIDToString(const UUID_t uuid) const
{
char out[37] = {};
snprintf(out, 37, "%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x",
uuid[0], uuid[1], uuid[2], uuid[3], uuid[4], uuid[5], uuid[6], uuid[7], uuid[8], uuid[9], uuid[10], uuid[11], uuid[12], uuid[13], uuid[14], uuid[15]);
return out;
}
// ********************************************************
// SHA256 helpers
/*std::string PrettyOTA::SHA256ToString(const uint8_t hash[32]) const
{
static const char* const SHA256StringLookup = "0123456789abcdef";
std::string result = "";
for(uint32_t i = 0; i < 32; i++)
{
result += SHA256StringLookup[hash[i] >> 4];
result += SHA256StringLookup[hash[i] & 0x0F];
}
return result;
}*/