Add firmware slices 1–2: skeleton and Wi-Fi provisioning

Implement ESP-IDF walking skeleton with SoftAP, health API, and host
tests, then Slice 2 ISecureStore/NvsSecureStore, STA join, POST
/api/wifi, and the provisioning web UI.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-06 09:10:37 +02:00
co-authored by Cursor
parent 9f04c4bb0c
commit ddbee70c23
63 changed files with 3771 additions and 19 deletions
@@ -0,0 +1,33 @@
# Host unit tests for components/core (plain CMake + ctest)
cmake_minimum_required(VERSION 3.16)
project(digiradio_core_tests LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS ON)
enable_testing()
set(CORE_SRC_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../src")
add_library(digiradio_core STATIC
"${CORE_SRC_DIR}/FirmwareVersion.cpp"
"${CORE_SRC_DIR}/HealthStatus.cpp"
"${CORE_SRC_DIR}/HealthStatusJson.cpp"
"${CORE_SRC_DIR}/Secret.cpp"
"${CORE_SRC_DIR}/WifiSsid.cpp"
"${CORE_SRC_DIR}/WifiCredentials.cpp"
"${CORE_SRC_DIR}/WifiProvisionJson.cpp"
)
target_include_directories(digiradio_core PUBLIC
"${CMAKE_CURRENT_SOURCE_DIR}/../include"
)
add_executable(health_status_test health_status_test.cpp)
target_link_libraries(health_status_test PRIVATE digiradio_core)
add_test(NAME health_status_test COMMAND health_status_test)
add_executable(wifi_provision_test wifi_provision_test.cpp)
target_link_libraries(wifi_provision_test PRIVATE digiradio_core)
add_test(NAME wifi_provision_test COMMAND wifi_provision_test)
@@ -0,0 +1,89 @@
/**
* @file health_status_test.cpp
* @brief Host tests for HealthStatus JSON serialisation.
*
* DigiRadio firmware — https://github.com/manvalan/DigiRadio
*
* Copyright 2026 Michele Bigi
* SPDX-License-Identifier: Apache-2.0
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
*
* @author Michele Bigi
* @date 2026-07-06
*/
#include "core/FirmwareVersion.hpp"
#include "core/HealthStatus.hpp"
#include "core/HealthStatusJson.hpp"
#include <cstdlib>
#include <iostream>
#include <string>
namespace {
/**
* @brief expectEqual — assert two strings match.
*
* @dname expectEqual
* @param actual Observed value.
* @param expected Expected value.
* @return true when equal.
* @pubstate none
*
* @author Michele Bigi
* @date 2026-07-06
*/
[[nodiscard]] bool expectEqual(const std::string& actual,
const std::string& expected)
{
if (actual == expected) {
return true;
}
std::cerr << "expected: " << expected << "\nactual: " << actual << '\n';
return false;
}
/**
* @brief runHealthStatusJsonTest — verify nominal JSON output.
*
* @dname runHealthStatusJsonTest
* @param none
* @return EXIT_SUCCESS or EXIT_FAILURE.
* @pubstate none
*
* @author Michele Bigi
* @date 2026-07-06
*/
[[nodiscard]] int runHealthStatusJsonTest()
{
const core::HealthStatus status =
core::HealthStatus::ok(core::FirmwareVersion("0.1.0"));
const std::string json = core::serializeHealthStatusJson(status);
if (!expectEqual(json, R"({"status":"ok","fw":"0.1.0"})")) {
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
} // namespace
/**
* @brief main — host test entry point.
*
* @dname main
* @param none
* @return EXIT_SUCCESS when all tests pass.
* @pubstate none
*
* @author Michele Bigi
* @date 2026-07-06
*/
int main()
{
return runHealthStatusJsonTest();
}
@@ -0,0 +1,147 @@
/**
* @file wifi_provision_test.cpp
* @brief Host tests for Wi-Fi provisioning JSON parse/serialise.
*
* DigiRadio firmware — https://github.com/manvalan/DigiRadio
*
* Copyright 2026 Michele Bigi
* SPDX-License-Identifier: Apache-2.0
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
*
* @author Michele Bigi
* @date 2026-07-06
*/
#include "core/ParseError.hpp"
#include "core/WifiCredentials.hpp"
#include "core/WifiProvisionJson.hpp"
#include <cstdlib>
#include <iostream>
#include <string>
namespace {
/**
* @brief expectEqual — assert two strings match.
*
* @dname expectEqual
* @param actual Observed value.
* @param expected Expected value.
* @return true when equal.
* @pubstate none
*
* @author Michele Bigi
* @date 2026-07-06
*/
[[nodiscard]] bool expectEqual(const std::string& actual,
const std::string& expected)
{
if (actual == expected) {
return true;
}
std::cerr << "expected: " << expected << "\nactual: " << actual << '\n';
return false;
}
/**
* @brief runWifiProvisionParseTest — verify nominal JSON parsing.
*
* @dname runWifiProvisionParseTest
* @return EXIT_SUCCESS or EXIT_FAILURE.
* @pubstate none
*
* @author Michele Bigi
* @date 2026-07-06
*/
[[nodiscard]] int runWifiProvisionParseTest()
{
const auto parsed = core::parseWifiProvisionJson(
R"({"ssid":"MyNet","password":"secret12"})");
if (!parsed) {
std::cerr << "parse failed\n";
return EXIT_FAILURE;
}
if (parsed->ssid().value() != "MyNet") {
std::cerr << "unexpected ssid\n";
return EXIT_FAILURE;
}
bool pwdOk = false;
parsed->password().usePlaintext(
[&](std::string_view pwd) { pwdOk = (pwd == "secret12"); });
if (!pwdOk) {
std::cerr << "unexpected password\n";
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
/**
* @brief runWifiProvisionRejectTest — verify invalid SSID is rejected.
*
* @dname runWifiProvisionRejectTest
* @return EXIT_SUCCESS or EXIT_FAILURE.
* @pubstate none
*
* @author Michele Bigi
* @date 2026-07-06
*/
[[nodiscard]] int runWifiProvisionRejectTest()
{
const auto parsed =
core::parseWifiProvisionJson(R"({"ssid":"","password":"secret12"})");
if (parsed || parsed.error() != core::ParseError::InvalidSsid) {
std::cerr << "expected InvalidSsid\n";
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
/**
* @brief runWifiProvisionSerialiseTest — verify saved response JSON.
*
* @dname runWifiProvisionSerialiseTest
* @return EXIT_SUCCESS or EXIT_FAILURE.
* @pubstate none
*
* @author Michele Bigi
* @date 2026-07-06
*/
[[nodiscard]] int runWifiProvisionSerialiseTest()
{
if (!expectEqual(core::serializeWifiProvisionSavedJson(3),
R"({"status":"saved","reboot_in_sec":3})")) {
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
} // namespace
/**
* @brief main — host test entry point.
*
* @dname main
* @return EXIT_SUCCESS when all tests pass.
* @pubstate none
*
* @author Michele Bigi
* @date 2026-07-06
*/
int main()
{
if (runWifiProvisionParseTest() != EXIT_SUCCESS) {
return EXIT_FAILURE;
}
if (runWifiProvisionRejectTest() != EXIT_SUCCESS) {
return EXIT_FAILURE;
}
if (runWifiProvisionSerialiseTest() != EXIT_SUCCESS) {
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}