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
@@ -0,0 +1,95 @@
/**
* @file DeviceIdentity.cpp
* @brief DeviceIdentity implementation.
*
* 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"
namespace core {
namespace {
constexpr std::string_view kSerialUnknown = "unknown";
constexpr std::string_view kSoftApFallback = "DigiRadio-setup";
constexpr std::string_view kBluetoothFallback = "DigiRadio";
constexpr std::string_view kHostnameFallback = "digiradio";
[[nodiscard]] std::string prefixed(std::string_view prefix,
std::string_view suffix)
{
return std::string(prefix) + std::string(suffix);
}
} // namespace
DeviceIdentity DeviceIdentity::unknown() noexcept
{
return DeviceIdentity(std::nullopt,
std::string(kSerialUnknown),
std::string(kSoftApFallback),
std::string(kBluetoothFallback),
std::string(kHostnameFallback));
}
DeviceIdentity DeviceIdentity::fromEui48(Eui48 eui)
{
const std::string suffix = eui.shortSuffix();
return DeviceIdentity(eui,
eui.serialNumber(),
prefixed("DigiRadio-", suffix),
prefixed("DigiRadio-", suffix),
prefixed("digiradio-", suffix));
}
bool DeviceIdentity::isKnown() const noexcept
{
return eui_.has_value();
}
std::string_view DeviceIdentity::serialNumber() const noexcept
{
return serialNumber_;
}
std::string_view DeviceIdentity::softApSsid() const noexcept
{
return softApSsid_;
}
std::string_view DeviceIdentity::bluetoothName() const noexcept
{
return bluetoothName_;
}
std::string_view DeviceIdentity::hostname() const noexcept
{
return hostname_;
}
const std::optional<Eui48>& DeviceIdentity::eui48() const noexcept
{
return eui_;
}
DeviceIdentity::DeviceIdentity(std::optional<Eui48> eui,
std::string serialNumber,
std::string softApSsid,
std::string bluetoothName,
std::string hostname)
: eui_(std::move(eui))
, serialNumber_(std::move(serialNumber))
, softApSsid_(std::move(softApSsid))
, bluetoothName_(std::move(bluetoothName))
, hostname_(std::move(hostname))
{
}
} // namespace core
+63
View File
@@ -0,0 +1,63 @@
/**
* @file Eui48.cpp
* @brief Eui48 implementation.
*
* 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/Eui48.hpp"
#include <iomanip>
#include <sstream>
namespace core {
namespace {
[[nodiscard]] std::string toUpperHex(std::string_view bytes)
{
std::ostringstream out;
out << std::uppercase << std::hex << std::setfill('0');
for (const unsigned char byte : bytes) {
out << std::setw(2) << static_cast<unsigned>(byte);
}
return out.str();
}
} // namespace
Eui48 Eui48::fromBytes(const std::array<std::uint8_t, 6>& bytes) noexcept
{
return Eui48(bytes);
}
Eui48::Eui48(std::array<std::uint8_t, 6> bytes) noexcept
: bytes_(bytes)
{
}
const std::array<std::uint8_t, 6>& Eui48::bytes() const noexcept
{
return bytes_;
}
std::string Eui48::serialNumber() const
{
return toUpperHex(
std::string_view(reinterpret_cast<const char*>(bytes_.data()),
bytes_.size()));
}
std::string Eui48::shortSuffix() const
{
return toUpperHex(
std::string_view(reinterpret_cast<const char*>(bytes_.data() + 3U), 3U));
}
} // namespace core
+21 -3
View File
@@ -15,29 +15,42 @@
namespace core {
namespace {
constexpr std::string_view kSerialUnknown = "unknown";
} // namespace
HealthStatus HealthStatus::ok(FirmwareVersion firmware)
{
return HealthStatus(HealthState::Ok, std::move(firmware));
}
HealthStatus HealthStatus::ok(FirmwareVersion firmware,
CompanionChipStatus chips)
CompanionChipStatus chips,
std::string_view serialNumber)
{
return HealthStatus(HealthState::Ok, std::move(firmware), chips);
return HealthStatus(HealthState::Ok,
std::move(firmware),
chips,
std::string(serialNumber));
}
HealthStatus::HealthStatus(HealthState state, FirmwareVersion firmware)
: state_(state)
, firmware_(std::move(firmware))
, chips_(std::nullopt)
, serialNumber_(kSerialUnknown)
{
}
HealthStatus::HealthStatus(HealthState state, FirmwareVersion firmware,
CompanionChipStatus chips)
CompanionChipStatus chips,
std::string serialNumber)
: state_(state)
, firmware_(std::move(firmware))
, chips_(chips)
, serialNumber_(std::move(serialNumber))
{
}
@@ -56,4 +69,9 @@ const std::optional<CompanionChipStatus>& HealthStatus::chips() const noexcept
return chips_;
}
std::string_view HealthStatus::serialNumber() const noexcept
{
return serialNumber_;
}
} // namespace core
@@ -39,7 +39,9 @@ namespace {
std::string serializeHealthStatusJson(const HealthStatus& status)
{
std::string json = std::string("{\"status\":\"") + healthStateToken(status.state())
+ "\",\"fw\":\"" + std::string(status.firmware().value()) + "\"";
+ "\",\"fw\":\"" + std::string(status.firmware().value())
+ "\",\"serialNumber\":\""
+ std::string(status.serialNumber()) + "\"";
if (const std::optional<CompanionChipStatus>& chips = status.chips(); chips) {
json += std::string(",\"chips\":{")
+ "\"si4684\":" + (chips->si4684Ready ? "true" : "false")