Add ESP32 OTA firmware update with rollback confirmation.

Stream application binaries to the inactive OTA slot via POST /api/system/ota,
validate the esp_app_desc project name in core, and cancel rollback after a
healthy network boot through OtaService::confirmBoot().

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-07 09:41:29 +02:00
co-authored by Cursor
parent cc4a03168e
commit b0cff8369e
20 changed files with 862 additions and 13 deletions
@@ -49,6 +49,7 @@ add_library(digiradio_core STATIC
"${CORE_SRC_DIR}/DspProgram.cpp"
"${CORE_SRC_DIR}/RegisterWrite.cpp"
"${CORE_SRC_DIR}/DspProgramBlob.cpp"
"${CORE_SRC_DIR}/OtaAppDescriptor.cpp"
)
target_include_directories(digiradio_core PUBLIC
"${CMAKE_CURRENT_SOURCE_DIR}/../include"
@@ -58,6 +59,10 @@ add_executable(dsp_program_blob_test dsp_program_blob_test.cpp)
target_link_libraries(dsp_program_blob_test PRIVATE digiradio_core)
add_test(NAME dsp_program_blob_test COMMAND dsp_program_blob_test)
add_executable(ota_app_descriptor_test ota_app_descriptor_test.cpp)
target_link_libraries(ota_app_descriptor_test PRIVATE digiradio_core)
add_test(NAME ota_app_descriptor_test COMMAND ota_app_descriptor_test)
add_executable(device_identity_test device_identity_test.cpp)
target_link_libraries(device_identity_test PRIVATE digiradio_core)
add_test(NAME device_identity_test COMMAND device_identity_test)
@@ -0,0 +1,87 @@
/**
* @file ota_app_descriptor_test.cpp
* @brief Host tests for OTA app descriptor validation.
*
* DigiRadio firmware — https://github.com/manvalan/DigiRadio
*
* Copyright 2026 Michele Bigi
* SPDX-License-Identifier: Apache-2.0
*
* @author Michele Bigi
* @date 2026-07-07
*/
#include "core/OtaAppDescriptor.hpp"
#include <array>
#include <cassert>
#include <cstring>
#include <cstdint>
namespace {
void writeLe32(std::uint8_t* p, std::uint32_t value)
{
p[0] = static_cast<std::uint8_t>(value);
p[1] = static_cast<std::uint8_t>(value >> 8);
p[2] = static_cast<std::uint8_t>(value >> 16);
p[3] = static_cast<std::uint8_t>(value >> 24);
}
std::array<std::uint8_t, 256> makeValidPrefix()
{
std::array<std::uint8_t, 256> image{};
writeLe32(image.data() + core::kOtaAppDescriptorOffset,
core::kOtaAppDescriptorMagic);
std::memcpy(image.data() + core::kOtaAppDescriptorOffset + 40U,
core::kOtaProjectName,
std::strlen(core::kOtaProjectName));
return image;
}
void testValidDescriptor()
{
const auto image = makeValidPrefix();
const auto result = core::validateOtaAppDescriptor(image);
assert(result.has_value());
}
void testInsufficientHeader()
{
const std::array<std::uint8_t, 32> shortImage{};
const auto result = core::validateOtaAppDescriptor(shortImage);
assert(!result.has_value());
assert(result.error() == core::OtaImageError::InsufficientHeader);
}
void testInvalidMagic()
{
auto image = makeValidPrefix();
writeLe32(image.data() + core::kOtaAppDescriptorOffset, 0U);
const auto result = core::validateOtaAppDescriptor(image);
assert(!result.has_value());
assert(result.error() == core::OtaImageError::InvalidMagic);
}
void testInvalidProject()
{
auto image = makeValidPrefix();
const char foreign[] = "other-project";
std::memcpy(image.data() + core::kOtaAppDescriptorOffset + 40U,
foreign,
sizeof(foreign));
const auto result = core::validateOtaAppDescriptor(image);
assert(!result.has_value());
assert(result.error() == core::OtaImageError::InvalidProject);
}
} // namespace
int main()
{
testValidDescriptor();
testInsufficientHeader();
testInvalidMagic();
testInvalidProject();
return 0;
}