Add EEPROM EUI-48 device identity across firmware and API.

Read the factory 24AA025E48 serial after ADAU I2C boot, derive SoftAP SSID, BT name, STA hostname, and expose serialNumber on GET /api/health with graceful fallbacks.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-07 09:00:38 +02:00
co-authored by Cursor
parent 11ad6b43ad
commit cd94e36a9d
35 changed files with 999 additions and 83 deletions
@@ -15,6 +15,8 @@ add_library(digiradio_core STATIC
"${CORE_SRC_DIR}/FirmwareVersion.cpp"
"${CORE_SRC_DIR}/HealthStatus.cpp"
"${CORE_SRC_DIR}/HealthStatusJson.cpp"
"${CORE_SRC_DIR}/Eui48.cpp"
"${CORE_SRC_DIR}/DeviceIdentity.cpp"
"${CORE_SRC_DIR}/Secret.cpp"
"${CORE_SRC_DIR}/WifiSsid.cpp"
"${CORE_SRC_DIR}/WifiCredentials.cpp"
@@ -49,6 +51,10 @@ target_include_directories(digiradio_core PUBLIC
"${CMAKE_CURRENT_SOURCE_DIR}/../include"
)
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)
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)
@@ -0,0 +1,100 @@
/**
* @file device_identity_test.cpp
* @brief Host tests for Eui48 and DeviceIdentity derivation.
*
* 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/DeviceIdentity.hpp"
#include "core/Eui48.hpp"
#include <array>
#include <cstdlib>
#include <iostream>
#include <string>
namespace {
[[nodiscard]] bool expectEqual(std::string_view actual,
std::string_view expected)
{
if (actual == expected) {
return true;
}
std::cerr << "expected: " << expected << "\nactual: " << actual << '\n';
return false;
}
[[nodiscard]] int runKnownEuiTest()
{
const std::array<std::uint8_t, 6> bytes = {
0x00U, 0x04U, 0xA3U, 0x12U, 0x34U, 0x56U,
};
const core::Eui48 eui = core::Eui48::fromBytes(bytes);
if (!expectEqual(eui.serialNumber(), "0004A3123456")) {
return EXIT_FAILURE;
}
if (!expectEqual(eui.shortSuffix(), "123456")) {
return EXIT_FAILURE;
}
const core::DeviceIdentity identity = core::DeviceIdentity::fromEui48(eui);
if (!identity.isKnown()) {
std::cerr << "expected known identity\n";
return EXIT_FAILURE;
}
if (!expectEqual(identity.serialNumber(), "0004A3123456")) {
return EXIT_FAILURE;
}
if (!expectEqual(identity.softApSsid(), "DigiRadio-123456")) {
return EXIT_FAILURE;
}
if (!expectEqual(identity.bluetoothName(), "DigiRadio-123456")) {
return EXIT_FAILURE;
}
if (!expectEqual(identity.hostname(), "digiradio-123456")) {
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
[[nodiscard]] int runUnknownIdentityTest()
{
const core::DeviceIdentity identity = core::DeviceIdentity::unknown();
if (identity.isKnown()) {
std::cerr << "expected unknown identity\n";
return EXIT_FAILURE;
}
if (!expectEqual(identity.serialNumber(), "unknown")) {
return EXIT_FAILURE;
}
if (!expectEqual(identity.softApSsid(), "DigiRadio-setup")) {
return EXIT_FAILURE;
}
if (!expectEqual(identity.bluetoothName(), "DigiRadio")) {
return EXIT_FAILURE;
}
if (!expectEqual(identity.hostname(), "digiradio")) {
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
} // namespace
int main()
{
if (runKnownEuiTest() != EXIT_SUCCESS) {
return EXIT_FAILURE;
}
if (runUnknownIdentityTest() != EXIT_SUCCESS) {
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
@@ -7,11 +7,6 @@
* 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
*/
@@ -27,18 +22,6 @@
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)
{
@@ -49,23 +32,13 @@ namespace {
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"})")) {
if (!expectEqual(json,
R"({"status":"ok","fw":"0.1.0","serialNumber":"unknown"})")) {
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
@@ -73,17 +46,6 @@ namespace {
} // 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()
{
if (runHealthStatusJsonTest() != EXIT_SUCCESS) {
@@ -96,11 +58,12 @@ int main()
.si4684Ready = true,
.adau1701Ready = true,
.bt1035Ready = true,
});
},
"0004A3123456");
const std::string chipsJson = core::serializeHealthStatusJson(withChips);
if (!expectEqual(
chipsJson,
R"({"status":"ok","fw":"0.6.0","chips":{"si4684":true,"adau1701":true,"bt1035":true}})")) {
R"({"status":"ok","fw":"0.6.0","serialNumber":"0004A3123456","chips":{"si4684":true,"adau1701":true,"bt1035":true}})")) {
return EXIT_FAILURE;
}
return EXIT_SUCCESS;