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:
@@ -3,6 +3,8 @@ idf_component_register(
|
|||||||
"src/FirmwareVersion.cpp"
|
"src/FirmwareVersion.cpp"
|
||||||
"src/HealthStatus.cpp"
|
"src/HealthStatus.cpp"
|
||||||
"src/HealthStatusJson.cpp"
|
"src/HealthStatusJson.cpp"
|
||||||
|
"src/Eui48.cpp"
|
||||||
|
"src/DeviceIdentity.cpp"
|
||||||
"src/Secret.cpp"
|
"src/Secret.cpp"
|
||||||
"src/WifiSsid.cpp"
|
"src/WifiSsid.cpp"
|
||||||
"src/WifiCredentials.cpp"
|
"src/WifiCredentials.cpp"
|
||||||
|
|||||||
@@ -0,0 +1,147 @@
|
|||||||
|
/**
|
||||||
|
* @file DeviceIdentity.hpp
|
||||||
|
* @brief Per-board identity derived from the factory EUI-48 or fallbacks.
|
||||||
|
*
|
||||||
|
* DigiRadio firmware — https://github.com/manvalan/DigiRadio
|
||||||
|
*
|
||||||
|
* Copyright 2026 Michele Bigi
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*
|
||||||
|
* @author Michele Bigi
|
||||||
|
* @date 2026-07-07
|
||||||
|
*/
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "core/Eui48.hpp"
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
|
||||||
|
namespace core {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief DeviceIdentity — SSID, Bluetooth name, hostname, and serial strings.
|
||||||
|
*
|
||||||
|
* @dname DeviceIdentity
|
||||||
|
* @return n/a (type)
|
||||||
|
* @pubstate Immutable after construction. unknown() uses documented fallbacks
|
||||||
|
* when the EEPROM read fails.
|
||||||
|
*
|
||||||
|
* @author Michele Bigi
|
||||||
|
* @date 2026-07-07
|
||||||
|
*/
|
||||||
|
class DeviceIdentity {
|
||||||
|
public:
|
||||||
|
/**
|
||||||
|
* @brief unknown — identity when the EUI-48 read fails.
|
||||||
|
*
|
||||||
|
* @dname unknown
|
||||||
|
* @return DeviceIdentity with fallback SSID and serial "unknown".
|
||||||
|
* @pubstate none
|
||||||
|
*
|
||||||
|
* @author Michele Bigi
|
||||||
|
* @date 2026-07-07
|
||||||
|
*/
|
||||||
|
[[nodiscard]] static DeviceIdentity unknown() noexcept;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief fromEui48 — derive network-visible names from factory EUI-48.
|
||||||
|
*
|
||||||
|
* @dname fromEui48
|
||||||
|
* @param eui Factory-programmed identifier from the 24AA025E48.
|
||||||
|
* @return DeviceIdentity with DigiRadio-<suffix> strings.
|
||||||
|
* @pubstate none
|
||||||
|
*
|
||||||
|
* @author Michele Bigi
|
||||||
|
* @date 2026-07-07
|
||||||
|
*/
|
||||||
|
[[nodiscard]] static DeviceIdentity fromEui48(Eui48 eui);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief isKnown — whether a factory EUI-48 was read successfully.
|
||||||
|
*
|
||||||
|
* @dname isKnown
|
||||||
|
* @return true when fromEui48 was used.
|
||||||
|
* @pubstate reads eui_.
|
||||||
|
*
|
||||||
|
* @author Michele Bigi
|
||||||
|
* @date 2026-07-07
|
||||||
|
*/
|
||||||
|
[[nodiscard]] bool isKnown() const noexcept;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief serialNumber — canonical serial or "unknown".
|
||||||
|
*
|
||||||
|
* @dname serialNumber
|
||||||
|
* @return Uppercase hex serial when known.
|
||||||
|
* @pubstate reads serialNumber_.
|
||||||
|
*
|
||||||
|
* @author Michele Bigi
|
||||||
|
* @date 2026-07-07
|
||||||
|
*/
|
||||||
|
[[nodiscard]] std::string_view serialNumber() const noexcept;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief softApSsid — setup SoftAP SSID for this unit.
|
||||||
|
*
|
||||||
|
* @dname softApSsid
|
||||||
|
* @return DigiRadio-<suffix> or the DigiRadio-setup fallback.
|
||||||
|
* @pubstate reads softApSsid_.
|
||||||
|
*
|
||||||
|
* @author Michele Bigi
|
||||||
|
* @date 2026-07-07
|
||||||
|
*/
|
||||||
|
[[nodiscard]] std::string_view softApSsid() const noexcept;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief bluetoothName — GAP friendly name for the BT1035 module.
|
||||||
|
*
|
||||||
|
* @dname bluetoothName
|
||||||
|
* @return DigiRadio-<suffix> or plain DigiRadio when unknown.
|
||||||
|
* @pubstate reads bluetoothName_.
|
||||||
|
*
|
||||||
|
* @author Michele Bigi
|
||||||
|
* @date 2026-07-07
|
||||||
|
*/
|
||||||
|
[[nodiscard]] std::string_view bluetoothName() const noexcept;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief hostname — STA hostname / mDNS label (no .local suffix).
|
||||||
|
*
|
||||||
|
* @dname hostname
|
||||||
|
* @return digiradio-<suffix> or digiradio when unknown.
|
||||||
|
* @pubstate reads hostname_.
|
||||||
|
*
|
||||||
|
* @author Michele Bigi
|
||||||
|
* @date 2026-07-07
|
||||||
|
*/
|
||||||
|
[[nodiscard]] std::string_view hostname() const noexcept;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief eui48 — optional factory identifier backing this identity.
|
||||||
|
*
|
||||||
|
* @dname eui48
|
||||||
|
* @return EUI-48 when known; otherwise empty.
|
||||||
|
* @pubstate reads eui_.
|
||||||
|
*
|
||||||
|
* @author Michele Bigi
|
||||||
|
* @date 2026-07-07
|
||||||
|
*/
|
||||||
|
[[nodiscard]] const std::optional<Eui48>& eui48() const noexcept;
|
||||||
|
|
||||||
|
private:
|
||||||
|
DeviceIdentity(std::optional<Eui48> eui,
|
||||||
|
std::string serialNumber,
|
||||||
|
std::string softApSsid,
|
||||||
|
std::string bluetoothName,
|
||||||
|
std::string hostname);
|
||||||
|
|
||||||
|
std::optional<Eui48> eui_;
|
||||||
|
std::string serialNumber_;
|
||||||
|
std::string softApSsid_;
|
||||||
|
std::string bluetoothName_;
|
||||||
|
std::string hostname_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace core
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
/**
|
||||||
|
* @file Eui48.hpp
|
||||||
|
* @brief Strong type for a factory-programmed 48-bit EUI from 24AA025E48.
|
||||||
|
*
|
||||||
|
* DigiRadio firmware — https://github.com/manvalan/DigiRadio
|
||||||
|
*
|
||||||
|
* Copyright 2026 Michele Bigi
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*
|
||||||
|
* @author Michele Bigi
|
||||||
|
* @date 2026-07-07
|
||||||
|
*/
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
|
||||||
|
namespace core {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Eui48 — immutable six-byte globally unique identifier.
|
||||||
|
*
|
||||||
|
* @dname Eui48
|
||||||
|
* @return n/a (type)
|
||||||
|
* @pubstate Owns bytes_; constructed only from a full six-byte payload.
|
||||||
|
*
|
||||||
|
* @author Michele Bigi
|
||||||
|
* @date 2026-07-07
|
||||||
|
*/
|
||||||
|
class Eui48 {
|
||||||
|
public:
|
||||||
|
/**
|
||||||
|
* @brief fromBytes — wrap a factory EUI-48 read from EEPROM.
|
||||||
|
*
|
||||||
|
* @dname fromBytes
|
||||||
|
* @param bytes Six-byte EUI-48 in datasheet order (0xFA..0xFF).
|
||||||
|
* @return Eui48 value.
|
||||||
|
* @pubstate none
|
||||||
|
*
|
||||||
|
* @author Michele Bigi
|
||||||
|
* @date 2026-07-07
|
||||||
|
*/
|
||||||
|
[[nodiscard]] static Eui48 fromBytes(
|
||||||
|
const std::array<std::uint8_t, 6>& bytes) noexcept;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief bytes — read the raw EUI-48 octets.
|
||||||
|
*
|
||||||
|
* @dname bytes
|
||||||
|
* @return Reference to the six stored bytes.
|
||||||
|
* @pubstate reads bytes_.
|
||||||
|
*
|
||||||
|
* @author Michele Bigi
|
||||||
|
* @date 2026-07-07
|
||||||
|
*/
|
||||||
|
[[nodiscard]] const std::array<std::uint8_t, 6>& bytes() const noexcept;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief serialNumber — canonical uppercase hex serial (12 digits).
|
||||||
|
*
|
||||||
|
* @dname serialNumber
|
||||||
|
* @return Serial string without separators (e.g. AABBCCDDEEFF).
|
||||||
|
* @pubstate none
|
||||||
|
*
|
||||||
|
* @author Michele Bigi
|
||||||
|
* @date 2026-07-07
|
||||||
|
*/
|
||||||
|
[[nodiscard]] std::string serialNumber() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief shortSuffix — last three bytes as six uppercase hex digits.
|
||||||
|
*
|
||||||
|
* @dname shortSuffix
|
||||||
|
* @return Suffix for SSID, Bluetooth name, and hostname.
|
||||||
|
* @pubstate none
|
||||||
|
*
|
||||||
|
* @author Michele Bigi
|
||||||
|
* @date 2026-07-07
|
||||||
|
*/
|
||||||
|
[[nodiscard]] std::string shortSuffix() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
explicit Eui48(std::array<std::uint8_t, 6> bytes) noexcept;
|
||||||
|
|
||||||
|
std::array<std::uint8_t, 6> bytes_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace core
|
||||||
@@ -21,6 +21,8 @@
|
|||||||
#include "core/CompanionChipStatus.hpp"
|
#include "core/CompanionChipStatus.hpp"
|
||||||
|
|
||||||
#include <optional>
|
#include <optional>
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
|
||||||
namespace core {
|
namespace core {
|
||||||
|
|
||||||
@@ -71,8 +73,9 @@ public:
|
|||||||
* @brief ok — build health response including companion-chip flags.
|
* @brief ok — build health response including companion-chip flags.
|
||||||
*
|
*
|
||||||
* @dname ok
|
* @dname ok
|
||||||
* @param firmware Active firmware version to report.
|
* @param firmware Active firmware version to report.
|
||||||
* @param chips Si4684 / ADAU1701 / BT1035 boot snapshot.
|
* @param chips Si4684 / ADAU1701 / BT1035 boot snapshot.
|
||||||
|
* @param serialNumber Unit serial from EEPROM, or "unknown".
|
||||||
* @return HealthStatus with HealthState::Ok and chips populated.
|
* @return HealthStatus with HealthState::Ok and chips populated.
|
||||||
* @pubstate none
|
* @pubstate none
|
||||||
*
|
*
|
||||||
@@ -80,7 +83,8 @@ public:
|
|||||||
* @date 2026-07-06
|
* @date 2026-07-06
|
||||||
*/
|
*/
|
||||||
[[nodiscard]] static HealthStatus ok(FirmwareVersion firmware,
|
[[nodiscard]] static HealthStatus ok(FirmwareVersion firmware,
|
||||||
CompanionChipStatus chips);
|
CompanionChipStatus chips,
|
||||||
|
std::string_view serialNumber);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief state — read the health indicator.
|
* @brief state — read the health indicator.
|
||||||
@@ -119,15 +123,29 @@ public:
|
|||||||
[[nodiscard]] const std::optional<CompanionChipStatus>& chips() const
|
[[nodiscard]] const std::optional<CompanionChipStatus>& chips() const
|
||||||
noexcept;
|
noexcept;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief serialNumber — board serial from EEPROM or "unknown".
|
||||||
|
*
|
||||||
|
* @dname serialNumber
|
||||||
|
* @return Canonical serial string for /api/health.
|
||||||
|
* @pubstate reads serialNumber_.
|
||||||
|
*
|
||||||
|
* @author Michele Bigi
|
||||||
|
* @date 2026-07-07
|
||||||
|
*/
|
||||||
|
[[nodiscard]] std::string_view serialNumber() const noexcept;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
explicit HealthStatus(HealthState state, FirmwareVersion firmware);
|
explicit HealthStatus(HealthState state, FirmwareVersion firmware);
|
||||||
|
|
||||||
explicit HealthStatus(HealthState state, FirmwareVersion firmware,
|
explicit HealthStatus(HealthState state, FirmwareVersion firmware,
|
||||||
CompanionChipStatus chips);
|
CompanionChipStatus chips,
|
||||||
|
std::string serialNumber);
|
||||||
|
|
||||||
HealthState state_;
|
HealthState state_;
|
||||||
FirmwareVersion firmware_;
|
FirmwareVersion firmware_;
|
||||||
std::optional<CompanionChipStatus> chips_;
|
std::optional<CompanionChipStatus> chips_;
|
||||||
|
std::string serialNumber_;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace core
|
} // namespace core
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
/**
|
||||||
|
* @file IDeviceIdentitySource.hpp
|
||||||
|
* @brief Port for reading per-board identity from non-volatile storage.
|
||||||
|
*
|
||||||
|
* DigiRadio firmware — https://github.com/manvalan/DigiRadio
|
||||||
|
*
|
||||||
|
* Copyright 2026 Michele Bigi
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*
|
||||||
|
* @author Michele Bigi
|
||||||
|
* @date 2026-07-07
|
||||||
|
*/
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "core/DeviceIdentity.hpp"
|
||||||
|
#include "core/IdentityError.hpp"
|
||||||
|
|
||||||
|
#include <expected>
|
||||||
|
|
||||||
|
namespace core {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief IDeviceIdentitySource — reads DeviceIdentity from board storage.
|
||||||
|
*
|
||||||
|
* @dname IDeviceIdentitySource
|
||||||
|
* @return n/a (type)
|
||||||
|
* @pubstate Implementations live in the imperative shell (EEPROM driver).
|
||||||
|
*
|
||||||
|
* @author Michele Bigi
|
||||||
|
* @date 2026-07-07
|
||||||
|
*/
|
||||||
|
class IDeviceIdentitySource {
|
||||||
|
public:
|
||||||
|
virtual ~IDeviceIdentitySource() = default;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief readDeviceIdentity — load identity from the backing store.
|
||||||
|
*
|
||||||
|
* @dname readDeviceIdentity
|
||||||
|
* @return DeviceIdentity on success, or IdentityError.
|
||||||
|
* @pubstate none
|
||||||
|
*
|
||||||
|
* @author Michele Bigi
|
||||||
|
* @date 2026-07-07
|
||||||
|
*/
|
||||||
|
[[nodiscard]] virtual std::expected<DeviceIdentity, IdentityError>
|
||||||
|
readDeviceIdentity() = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace core
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
/**
|
||||||
|
* @file IdentityError.hpp
|
||||||
|
* @brief Failure causes when reading board identity from EEPROM.
|
||||||
|
*
|
||||||
|
* DigiRadio firmware — https://github.com/manvalan/DigiRadio
|
||||||
|
*
|
||||||
|
* Copyright 2026 Michele Bigi
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*
|
||||||
|
* @author Michele Bigi
|
||||||
|
* @date 2026-07-07
|
||||||
|
*/
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
namespace core {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief IdentityError — EEPROM / I2C identity read failures.
|
||||||
|
*
|
||||||
|
* @dname IdentityError
|
||||||
|
* @return n/a (type)
|
||||||
|
* @pubstate n/a
|
||||||
|
*
|
||||||
|
* @author Michele Bigi
|
||||||
|
* @date 2026-07-07
|
||||||
|
*/
|
||||||
|
enum class IdentityError {
|
||||||
|
I2cFailed, ///< Bus or device transaction failed.
|
||||||
|
ReadFailed, ///< Payload length or EEPROM response invalid.
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace core
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -15,29 +15,42 @@
|
|||||||
|
|
||||||
namespace core {
|
namespace core {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
constexpr std::string_view kSerialUnknown = "unknown";
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
HealthStatus HealthStatus::ok(FirmwareVersion firmware)
|
HealthStatus HealthStatus::ok(FirmwareVersion firmware)
|
||||||
{
|
{
|
||||||
return HealthStatus(HealthState::Ok, std::move(firmware));
|
return HealthStatus(HealthState::Ok, std::move(firmware));
|
||||||
}
|
}
|
||||||
|
|
||||||
HealthStatus HealthStatus::ok(FirmwareVersion 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)
|
HealthStatus::HealthStatus(HealthState state, FirmwareVersion firmware)
|
||||||
: state_(state)
|
: state_(state)
|
||||||
, firmware_(std::move(firmware))
|
, firmware_(std::move(firmware))
|
||||||
, chips_(std::nullopt)
|
, chips_(std::nullopt)
|
||||||
|
, serialNumber_(kSerialUnknown)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
HealthStatus::HealthStatus(HealthState state, FirmwareVersion firmware,
|
HealthStatus::HealthStatus(HealthState state, FirmwareVersion firmware,
|
||||||
CompanionChipStatus chips)
|
CompanionChipStatus chips,
|
||||||
|
std::string serialNumber)
|
||||||
: state_(state)
|
: state_(state)
|
||||||
, firmware_(std::move(firmware))
|
, firmware_(std::move(firmware))
|
||||||
, chips_(chips)
|
, chips_(chips)
|
||||||
|
, serialNumber_(std::move(serialNumber))
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,4 +69,9 @@ const std::optional<CompanionChipStatus>& HealthStatus::chips() const noexcept
|
|||||||
return chips_;
|
return chips_;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::string_view HealthStatus::serialNumber() const noexcept
|
||||||
|
{
|
||||||
|
return serialNumber_;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace core
|
} // namespace core
|
||||||
|
|||||||
@@ -39,7 +39,9 @@ namespace {
|
|||||||
std::string serializeHealthStatusJson(const HealthStatus& status)
|
std::string serializeHealthStatusJson(const HealthStatus& status)
|
||||||
{
|
{
|
||||||
std::string json = std::string("{\"status\":\"") + healthStateToken(status.state())
|
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) {
|
if (const std::optional<CompanionChipStatus>& chips = status.chips(); chips) {
|
||||||
json += std::string(",\"chips\":{")
|
json += std::string(",\"chips\":{")
|
||||||
+ "\"si4684\":" + (chips->si4684Ready ? "true" : "false")
|
+ "\"si4684\":" + (chips->si4684Ready ? "true" : "false")
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ add_library(digiradio_core STATIC
|
|||||||
"${CORE_SRC_DIR}/FirmwareVersion.cpp"
|
"${CORE_SRC_DIR}/FirmwareVersion.cpp"
|
||||||
"${CORE_SRC_DIR}/HealthStatus.cpp"
|
"${CORE_SRC_DIR}/HealthStatus.cpp"
|
||||||
"${CORE_SRC_DIR}/HealthStatusJson.cpp"
|
"${CORE_SRC_DIR}/HealthStatusJson.cpp"
|
||||||
|
"${CORE_SRC_DIR}/Eui48.cpp"
|
||||||
|
"${CORE_SRC_DIR}/DeviceIdentity.cpp"
|
||||||
"${CORE_SRC_DIR}/Secret.cpp"
|
"${CORE_SRC_DIR}/Secret.cpp"
|
||||||
"${CORE_SRC_DIR}/WifiSsid.cpp"
|
"${CORE_SRC_DIR}/WifiSsid.cpp"
|
||||||
"${CORE_SRC_DIR}/WifiCredentials.cpp"
|
"${CORE_SRC_DIR}/WifiCredentials.cpp"
|
||||||
@@ -49,6 +51,10 @@ target_include_directories(digiradio_core PUBLIC
|
|||||||
"${CMAKE_CURRENT_SOURCE_DIR}/../include"
|
"${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)
|
add_executable(health_status_test health_status_test.cpp)
|
||||||
target_link_libraries(health_status_test PRIVATE digiradio_core)
|
target_link_libraries(health_status_test PRIVATE digiradio_core)
|
||||||
add_test(NAME health_status_test COMMAND health_status_test)
|
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
|
* Copyright 2026 Michele Bigi
|
||||||
* SPDX-License-Identifier: Apache-2.0
|
* 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
|
* @author Michele Bigi
|
||||||
* @date 2026-07-06
|
* @date 2026-07-06
|
||||||
*/
|
*/
|
||||||
@@ -27,18 +22,6 @@
|
|||||||
|
|
||||||
namespace {
|
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,
|
[[nodiscard]] bool expectEqual(const std::string& actual,
|
||||||
const std::string& expected)
|
const std::string& expected)
|
||||||
{
|
{
|
||||||
@@ -49,23 +32,13 @@ namespace {
|
|||||||
return false;
|
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()
|
[[nodiscard]] int runHealthStatusJsonTest()
|
||||||
{
|
{
|
||||||
const core::HealthStatus status =
|
const core::HealthStatus status =
|
||||||
core::HealthStatus::ok(core::FirmwareVersion("0.1.0"));
|
core::HealthStatus::ok(core::FirmwareVersion("0.1.0"));
|
||||||
const std::string json = core::serializeHealthStatusJson(status);
|
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_FAILURE;
|
||||||
}
|
}
|
||||||
return EXIT_SUCCESS;
|
return EXIT_SUCCESS;
|
||||||
@@ -73,17 +46,6 @@ namespace {
|
|||||||
|
|
||||||
} // 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()
|
int main()
|
||||||
{
|
{
|
||||||
if (runHealthStatusJsonTest() != EXIT_SUCCESS) {
|
if (runHealthStatusJsonTest() != EXIT_SUCCESS) {
|
||||||
@@ -96,11 +58,12 @@ int main()
|
|||||||
.si4684Ready = true,
|
.si4684Ready = true,
|
||||||
.adau1701Ready = true,
|
.adau1701Ready = true,
|
||||||
.bt1035Ready = true,
|
.bt1035Ready = true,
|
||||||
});
|
},
|
||||||
|
"0004A3123456");
|
||||||
const std::string chipsJson = core::serializeHealthStatusJson(withChips);
|
const std::string chipsJson = core::serializeHealthStatusJson(withChips);
|
||||||
if (!expectEqual(
|
if (!expectEqual(
|
||||||
chipsJson,
|
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_FAILURE;
|
||||||
}
|
}
|
||||||
return EXIT_SUCCESS;
|
return EXIT_SUCCESS;
|
||||||
|
|||||||
@@ -112,6 +112,19 @@ public:
|
|||||||
*/
|
*/
|
||||||
[[nodiscard]] bool isBooted() const noexcept;
|
[[nodiscard]] bool isBooted() const noexcept;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief i2cBusHandle — borrow the shared I2C master bus after boot.
|
||||||
|
*
|
||||||
|
* @dname i2cBusHandle
|
||||||
|
* @return Opaque bus handle for the 24AA025E48 on the same bus, or null
|
||||||
|
* before boot().
|
||||||
|
* @pubstate reads i2cBus_.
|
||||||
|
*
|
||||||
|
* @author Michele Bigi
|
||||||
|
* @date 2026-07-07
|
||||||
|
*/
|
||||||
|
[[nodiscard]] void* i2cBusHandle() const noexcept;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief applyProfile — safeload mixer, EQ, and master from snapshot.
|
* @brief applyProfile — safeload mixer, EQ, and master from snapshot.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -122,6 +122,11 @@ bool Adau1701Driver::isBooted() const noexcept
|
|||||||
return booted_;
|
return booted_;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void* Adau1701Driver::i2cBusHandle() const noexcept
|
||||||
|
{
|
||||||
|
return i2cBus_;
|
||||||
|
}
|
||||||
|
|
||||||
std::expected<void, Adau1701Error> Adau1701Driver::ensureBooted() const
|
std::expected<void, Adau1701Error> Adau1701Driver::ensureBooted() const
|
||||||
{
|
{
|
||||||
if (!booted_) {
|
if (!booted_) {
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
#include "core/Bt1035At.hpp"
|
#include "core/Bt1035At.hpp"
|
||||||
|
|
||||||
#include <expected>
|
#include <expected>
|
||||||
|
#include <string_view>
|
||||||
|
|
||||||
namespace bt1035 {
|
namespace bt1035 {
|
||||||
|
|
||||||
@@ -168,6 +169,20 @@ public:
|
|||||||
*/
|
*/
|
||||||
[[nodiscard]] std::expected<void, Bt1035Error> disconnectA2dp();
|
[[nodiscard]] std::expected<void, Bt1035Error> disconnectA2dp();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief setDeviceName — set the module GAP friendly name (AT+NAME).
|
||||||
|
*
|
||||||
|
* @dname setDeviceName
|
||||||
|
* @param name Bluetooth name (Feasycom FSC-BT1035 AT+NAME command).
|
||||||
|
* @return Ok on success, or Bt1035Error.
|
||||||
|
* @pubstate sends AT+NAME after boot; does not alter AT+AUXCFG=1 init.
|
||||||
|
*
|
||||||
|
* @author Michele Bigi
|
||||||
|
* @date 2026-07-07
|
||||||
|
*/
|
||||||
|
[[nodiscard]] std::expected<void, Bt1035Error> setDeviceName(
|
||||||
|
std::string_view name);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
[[nodiscard]] std::expected<void, Bt1035Error> ensureBooted() const;
|
[[nodiscard]] std::expected<void, Bt1035Error> ensureBooted() const;
|
||||||
[[nodiscard]] std::expected<void, Bt1035Error> runInitSequence();
|
[[nodiscard]] std::expected<void, Bt1035Error> runInitSequence();
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
|
|
||||||
#include <array>
|
#include <array>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
|
||||||
namespace bt1035 {
|
namespace bt1035 {
|
||||||
|
|
||||||
@@ -161,6 +162,19 @@ std::expected<void, Bt1035Error> Bt1035Driver::disconnectA2dp()
|
|||||||
return sendCommand(core::Bt1035AtCommand::A2dpDisconnect);
|
return sendCommand(core::Bt1035AtCommand::A2dpDisconnect);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::expected<void, Bt1035Error> Bt1035Driver::setDeviceName(
|
||||||
|
std::string_view name)
|
||||||
|
{
|
||||||
|
if (auto ready = ensureBooted(); !ready) {
|
||||||
|
return ready;
|
||||||
|
}
|
||||||
|
if (name.empty() || name.size() > 32U) {
|
||||||
|
return std::unexpected(Bt1035Error::UnexpectedResponse);
|
||||||
|
}
|
||||||
|
std::string line = std::string("AT+NAME=") + std::string(name) + "\r\n";
|
||||||
|
return transmitAndExpectOk(line);
|
||||||
|
}
|
||||||
|
|
||||||
std::expected<void, Bt1035Error> Bt1035Driver::runInitSequence()
|
std::expected<void, Bt1035Error> Bt1035Driver::runInitSequence()
|
||||||
{
|
{
|
||||||
for (const core::Bt1035AtCommand command : core::bootInitSequence()) {
|
for (const core::Bt1035AtCommand command : core::bootInitSequence()) {
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
idf_component_register(
|
||||||
|
SRCS "src/Eeprom24aa.cpp"
|
||||||
|
INCLUDE_DIRS "include"
|
||||||
|
REQUIRES core driver esp_driver_i2c
|
||||||
|
)
|
||||||
|
|
||||||
|
target_compile_features(${COMPONENT_LIB} PUBLIC cxx_std_23)
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
/**
|
||||||
|
* @file Eeprom24aa.hpp
|
||||||
|
* @brief 24AA025E48 EEPROM driver — factory EUI-48 on the shared I2C bus.
|
||||||
|
*
|
||||||
|
* DigiRadio firmware — https://github.com/manvalan/DigiRadio
|
||||||
|
*
|
||||||
|
* Copyright 2026 Michele Bigi
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*
|
||||||
|
* @author Michele Bigi
|
||||||
|
* @date 2026-07-07
|
||||||
|
*/
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "core/IDeviceIdentitySource.hpp"
|
||||||
|
|
||||||
|
#include "driver/i2c_master.h"
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
namespace eeprom24aa {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Eeprom24aa — reads the factory EUI-48 from Microchip 24AA025E48.
|
||||||
|
*
|
||||||
|
* @dname Eeprom24aa
|
||||||
|
* @return n/a (type)
|
||||||
|
* @pubstate Borrows an existing I2C master bus (shared with ADAU1701). The
|
||||||
|
* EUI-48 lives at word address 0xFA..0xFF per the 24AA025E48
|
||||||
|
* datasheet (DS20001191).
|
||||||
|
*
|
||||||
|
* @author Michele Bigi
|
||||||
|
* @date 2026-07-07
|
||||||
|
*/
|
||||||
|
class Eeprom24aa : public core::IDeviceIdentitySource {
|
||||||
|
public:
|
||||||
|
/**
|
||||||
|
* @brief Eeprom24aa — bind to a running I2C master bus and 7-bit addr.
|
||||||
|
*
|
||||||
|
* @dname Eeprom24aa
|
||||||
|
* @param bus Shared I2C bus handle from Adau1701Driver after boot.
|
||||||
|
* @param addr7 7-bit EEPROM address (0x52 on DigiRadio).
|
||||||
|
* @pubstate stores bus_ and addr7_; does not own the bus.
|
||||||
|
*
|
||||||
|
* @author Michele Bigi
|
||||||
|
* @date 2026-07-07
|
||||||
|
*/
|
||||||
|
Eeprom24aa(i2c_master_bus_handle_t bus, std::uint8_t addr7) noexcept;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief readDeviceIdentity — read EUI-48 and derive DeviceIdentity.
|
||||||
|
*
|
||||||
|
* @dname readDeviceIdentity
|
||||||
|
* @return DeviceIdentity on success, or IdentityError.
|
||||||
|
* @pubstate performs one I2C read of six bytes at word address 0xFA.
|
||||||
|
*
|
||||||
|
* @author Michele Bigi
|
||||||
|
* @date 2026-07-07
|
||||||
|
*/
|
||||||
|
[[nodiscard]] std::expected<core::DeviceIdentity, core::IdentityError>
|
||||||
|
readDeviceIdentity() override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
i2c_master_bus_handle_t bus_;
|
||||||
|
std::uint8_t addr7_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace eeprom24aa
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
/**
|
||||||
|
* @file Eeprom24aa.cpp
|
||||||
|
* @brief Eeprom24aa 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 "eeprom24aa/Eeprom24aa.hpp"
|
||||||
|
|
||||||
|
#include "driver/i2c_master.h"
|
||||||
|
#include "esp_log.h"
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
|
||||||
|
namespace eeprom24aa {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
constexpr char kTag[] = "Eeprom24aa";
|
||||||
|
/** EUI-48 word address per Microchip 24AA025E48 datasheet (DS20001191). */
|
||||||
|
constexpr std::uint8_t kEui48WordAddress = 0xFAU;
|
||||||
|
constexpr int kI2cTimeoutMs = 100;
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
Eeprom24aa::Eeprom24aa(i2c_master_bus_handle_t bus,
|
||||||
|
std::uint8_t addr7) noexcept
|
||||||
|
: bus_(bus)
|
||||||
|
, addr7_(addr7)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
std::expected<core::DeviceIdentity, core::IdentityError>
|
||||||
|
Eeprom24aa::readDeviceIdentity()
|
||||||
|
{
|
||||||
|
if (bus_ == nullptr) {
|
||||||
|
return std::unexpected(core::IdentityError::I2cFailed);
|
||||||
|
}
|
||||||
|
|
||||||
|
i2c_device_config_t devCfg = {};
|
||||||
|
devCfg.dev_addr_length = I2C_ADDR_BIT_LEN_7;
|
||||||
|
devCfg.device_address = addr7_;
|
||||||
|
devCfg.scl_speed_hz = 100000;
|
||||||
|
|
||||||
|
i2c_master_dev_handle_t dev = nullptr;
|
||||||
|
if (i2c_master_bus_add_device(bus_, &devCfg, &dev) != ESP_OK) {
|
||||||
|
ESP_LOGW(kTag, "i2c_master_bus_add_device failed");
|
||||||
|
return std::unexpected(core::IdentityError::I2cFailed);
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::uint8_t wordAddress = kEui48WordAddress;
|
||||||
|
std::array<std::uint8_t, 6> payload = {};
|
||||||
|
const esp_err_t err = i2c_master_transmit_receive(
|
||||||
|
dev, &wordAddress, 1U, payload.data(), payload.size(), kI2cTimeoutMs);
|
||||||
|
|
||||||
|
i2c_master_bus_rm_device(dev);
|
||||||
|
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
ESP_LOGW(kTag, "EUI-48 read failed (err=0x%x)", static_cast<unsigned>(err));
|
||||||
|
return std::unexpected(core::IdentityError::ReadFailed);
|
||||||
|
}
|
||||||
|
|
||||||
|
return core::DeviceIdentity::fromEui48(core::Eui48::fromBytes(payload));
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace eeprom24aa
|
||||||
@@ -7,7 +7,7 @@ idf_component_register(
|
|||||||
"src/NetBootstrap.cpp"
|
"src/NetBootstrap.cpp"
|
||||||
INCLUDE_DIRS "include"
|
INCLUDE_DIRS "include"
|
||||||
EMBED_FILES "www/index.html.gz"
|
EMBED_FILES "www/index.html.gz"
|
||||||
REQUIRES core esp_wifi esp_netif esp_event nvs_flash esp_http_server secure_store tuner audio bluetooth station integration bt1035
|
REQUIRES core esp_wifi esp_netif esp_event nvs_flash esp_http_server mdns secure_store tuner audio bluetooth station integration bt1035
|
||||||
)
|
)
|
||||||
|
|
||||||
target_compile_features(${COMPONENT_LIB} PUBLIC cxx_std_23)
|
target_compile_features(${COMPONENT_LIB} PUBLIC cxx_std_23)
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "core/CompanionChipStatus.hpp"
|
#include "core/CompanionChipStatus.hpp"
|
||||||
|
#include "core/DeviceIdentity.hpp"
|
||||||
#include "core/ISecureStore.hpp"
|
#include "core/ISecureStore.hpp"
|
||||||
#include "net/NetError.hpp"
|
#include "net/NetError.hpp"
|
||||||
#include "net/NetState.hpp"
|
#include "net/NetState.hpp"
|
||||||
@@ -74,6 +75,7 @@ public:
|
|||||||
* @param stations Station preset service for REST routes.
|
* @param stations Station preset service for REST routes.
|
||||||
* @param integration Application orchestration for preset recall.
|
* @param integration Application orchestration for preset recall.
|
||||||
* @param companionChips Boot flags exposed on GET /api/health.
|
* @param companionChips Boot flags exposed on GET /api/health.
|
||||||
|
* @param deviceIdentity EEPROM-derived SSID, hostname, and serial.
|
||||||
* @return NetBootstrap on success, or a NetError.
|
* @return NetBootstrap on success, or a NetError.
|
||||||
* @pubstate none
|
* @pubstate none
|
||||||
*
|
*
|
||||||
@@ -85,7 +87,8 @@ public:
|
|||||||
audio::AudioService& audio, bluetooth::BluetoothService& bluetooth,
|
audio::AudioService& audio, bluetooth::BluetoothService& bluetooth,
|
||||||
station::StationService& stations,
|
station::StationService& stations,
|
||||||
integration::IntegrationService& integration,
|
integration::IntegrationService& integration,
|
||||||
core::CompanionChipStatus companionChips);
|
core::CompanionChipStatus companionChips,
|
||||||
|
const core::DeviceIdentity& deviceIdentity);
|
||||||
|
|
||||||
NetBootstrap(const NetBootstrap&) = delete;
|
NetBootstrap(const NetBootstrap&) = delete;
|
||||||
NetBootstrap& operator=(const NetBootstrap&) = delete;
|
NetBootstrap& operator=(const NetBootstrap&) = delete;
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "core/CompanionChipStatus.hpp"
|
#include "core/CompanionChipStatus.hpp"
|
||||||
|
#include "core/DeviceIdentity.hpp"
|
||||||
#include "core/ISecureStore.hpp"
|
#include "core/ISecureStore.hpp"
|
||||||
#include "net/NetError.hpp"
|
#include "net/NetError.hpp"
|
||||||
#include "net/NetState.hpp"
|
#include "net/NetState.hpp"
|
||||||
@@ -69,6 +70,7 @@ struct HttpRouteContext {
|
|||||||
station::StationService* stations; ///< Preset list REST routes.
|
station::StationService* stations; ///< Preset list REST routes.
|
||||||
integration::IntegrationService* integration; ///< Preset recall orchestration.
|
integration::IntegrationService* integration; ///< Preset recall orchestration.
|
||||||
core::CompanionChipStatus companionChips; ///< Boot flags for /api/health.
|
core::CompanionChipStatus companionChips; ///< Boot flags for /api/health.
|
||||||
|
core::DeviceIdentity deviceIdentity; ///< EEPROM-derived unit identity.
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -146,6 +148,7 @@ public:
|
|||||||
* @param stations Station preset service for list REST routes.
|
* @param stations Station preset service for list REST routes.
|
||||||
* @param integration Application orchestration for preset recall.
|
* @param integration Application orchestration for preset recall.
|
||||||
* @param companionChips Boot flags for GET /api/health.
|
* @param companionChips Boot flags for GET /api/health.
|
||||||
|
* @param deviceIdentity Unit identity for /api/health serialNumber.
|
||||||
* @return Ok on success, or NetError::HttpServerStartFailed.
|
* @return Ok on success, or NetError::HttpServerStartFailed.
|
||||||
* @pubstate writes server_, store_, netState_, and service pointers on success.
|
* @pubstate writes server_, store_, netState_, and service pointers on success.
|
||||||
*
|
*
|
||||||
@@ -158,7 +161,8 @@ public:
|
|||||||
bluetooth::BluetoothService& bluetooth,
|
bluetooth::BluetoothService& bluetooth,
|
||||||
station::StationService& stations,
|
station::StationService& stations,
|
||||||
integration::IntegrationService& integration,
|
integration::IntegrationService& integration,
|
||||||
core::CompanionChipStatus companionChips);
|
core::CompanionChipStatus companionChips,
|
||||||
|
const core::DeviceIdentity& deviceIdentity);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
httpd_handle* server_;
|
httpd_handle* server_;
|
||||||
|
|||||||
@@ -49,6 +49,19 @@ public:
|
|||||||
*/
|
*/
|
||||||
[[nodiscard]] static SoftApConfig setupDefault();
|
[[nodiscard]] static SoftApConfig setupDefault();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief forSsid — construct SoftAP settings with a custom SSID.
|
||||||
|
*
|
||||||
|
* @dname forSsid
|
||||||
|
* @param ssid Network name (max 32 bytes per 802.11).
|
||||||
|
* @return SoftApConfig with the given SSID.
|
||||||
|
* @pubstate none
|
||||||
|
*
|
||||||
|
* @author Michele Bigi
|
||||||
|
* @date 2026-07-07
|
||||||
|
*/
|
||||||
|
[[nodiscard]] static SoftApConfig forSsid(std::string_view ssid);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief ssid — read the broadcast SSID.
|
* @brief ssid — read the broadcast SSID.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
#include "net/NetError.hpp"
|
#include "net/NetError.hpp"
|
||||||
|
|
||||||
#include <expected>
|
#include <expected>
|
||||||
|
#include <string_view>
|
||||||
|
|
||||||
namespace net {
|
namespace net {
|
||||||
|
|
||||||
@@ -91,7 +92,8 @@ public:
|
|||||||
* @brief connect — join the network described by creds.
|
* @brief connect — join the network described by creds.
|
||||||
*
|
*
|
||||||
* @dname connect
|
* @dname connect
|
||||||
* @param creds Validated domain credentials from ISecureStore.
|
* @param creds Validated domain credentials from ISecureStore.
|
||||||
|
* @param hostname STA hostname / mDNS label (no .local suffix).
|
||||||
* @return Ok on success, or NetError::StaConnectTimeout /
|
* @return Ok on success, or NetError::StaConnectTimeout /
|
||||||
* NetError::StaConnectFailed.
|
* NetError::StaConnectFailed.
|
||||||
* @pubstate writes connected_ on success; uses creds via Secret.
|
* @pubstate writes connected_ on success; uses creds via Secret.
|
||||||
@@ -100,7 +102,8 @@ public:
|
|||||||
* @date 2026-07-06
|
* @date 2026-07-06
|
||||||
*/
|
*/
|
||||||
[[nodiscard]] std::expected<void, NetError>
|
[[nodiscard]] std::expected<void, NetError>
|
||||||
connect(const core::WifiCredentials& creds);
|
connect(const core::WifiCredentials& creds,
|
||||||
|
std::string_view hostname = {});
|
||||||
|
|
||||||
private:
|
private:
|
||||||
bool connected_;
|
bool connected_;
|
||||||
|
|||||||
@@ -101,11 +101,12 @@ startSetupMode(core::ISecureStore& store, tuner::TunerService& tuner,
|
|||||||
bluetooth::BluetoothService& bluetooth,
|
bluetooth::BluetoothService& bluetooth,
|
||||||
station::StationService& stations,
|
station::StationService& stations,
|
||||||
integration::IntegrationService& integration,
|
integration::IntegrationService& integration,
|
||||||
core::CompanionChipStatus companionChips)
|
core::CompanionChipStatus companionChips,
|
||||||
|
const core::DeviceIdentity& deviceIdentity)
|
||||||
{
|
{
|
||||||
esp_netif_create_default_wifi_ap();
|
esp_netif_create_default_wifi_ap();
|
||||||
|
|
||||||
SoftApHost softAp(SoftApConfig::setupDefault());
|
SoftApHost softAp(SoftApConfig::forSsid(deviceIdentity.softApSsid()));
|
||||||
if (auto apResult = softAp.start(); !apResult) {
|
if (auto apResult = softAp.start(); !apResult) {
|
||||||
return std::unexpected(apResult.error());
|
return std::unexpected(apResult.error());
|
||||||
}
|
}
|
||||||
@@ -113,12 +114,15 @@ startSetupMode(core::ISecureStore& store, tuner::TunerService& tuner,
|
|||||||
SetupWebServer webServer;
|
SetupWebServer webServer;
|
||||||
if (auto webResult =
|
if (auto webResult =
|
||||||
webServer.start(store, NetState::SoftApSetup, tuner, audio,
|
webServer.start(store, NetState::SoftApSetup, tuner, audio,
|
||||||
bluetooth, stations, integration, companionChips);
|
bluetooth, stations, integration, companionChips,
|
||||||
|
deviceIdentity);
|
||||||
!webResult) {
|
!webResult) {
|
||||||
return std::unexpected(webResult.error());
|
return std::unexpected(webResult.error());
|
||||||
}
|
}
|
||||||
|
|
||||||
ESP_LOGI(kTag, "setup mode ready — SSID DigiRadio-setup");
|
ESP_LOGI(kTag, "setup mode ready — SSID %.*s",
|
||||||
|
static_cast<int>(deviceIdentity.softApSsid().size()),
|
||||||
|
deviceIdentity.softApSsid().data());
|
||||||
return NetBootstrap(std::move(softAp), std::nullopt, std::move(webServer),
|
return NetBootstrap(std::move(softAp), std::nullopt, std::move(webServer),
|
||||||
NetState::SoftApSetup);
|
NetState::SoftApSetup);
|
||||||
}
|
}
|
||||||
@@ -140,7 +144,8 @@ startStaMode(core::ISecureStore& store, tuner::TunerService& tuner,
|
|||||||
bluetooth::BluetoothService& bluetooth,
|
bluetooth::BluetoothService& bluetooth,
|
||||||
station::StationService& stations,
|
station::StationService& stations,
|
||||||
integration::IntegrationService& integration,
|
integration::IntegrationService& integration,
|
||||||
core::CompanionChipStatus companionChips)
|
core::CompanionChipStatus companionChips,
|
||||||
|
const core::DeviceIdentity& deviceIdentity)
|
||||||
{
|
{
|
||||||
auto credsResult = store.loadWifiCredentials();
|
auto credsResult = store.loadWifiCredentials();
|
||||||
if (!credsResult) {
|
if (!credsResult) {
|
||||||
@@ -151,19 +156,24 @@ startStaMode(core::ISecureStore& store, tuner::TunerService& tuner,
|
|||||||
esp_netif_create_default_wifi_sta();
|
esp_netif_create_default_wifi_sta();
|
||||||
|
|
||||||
StaClient sta;
|
StaClient sta;
|
||||||
if (auto staResult = sta.connect(credsResult.value()); !staResult) {
|
if (auto staResult =
|
||||||
|
sta.connect(credsResult.value(), deviceIdentity.hostname());
|
||||||
|
!staResult) {
|
||||||
return std::unexpected(staResult.error());
|
return std::unexpected(staResult.error());
|
||||||
}
|
}
|
||||||
|
|
||||||
SetupWebServer webServer;
|
SetupWebServer webServer;
|
||||||
if (auto webResult =
|
if (auto webResult =
|
||||||
webServer.start(store, NetState::StaConnected, tuner, audio,
|
webServer.start(store, NetState::StaConnected, tuner, audio,
|
||||||
bluetooth, stations, integration, companionChips);
|
bluetooth, stations, integration, companionChips,
|
||||||
|
deviceIdentity);
|
||||||
!webResult) {
|
!webResult) {
|
||||||
return std::unexpected(webResult.error());
|
return std::unexpected(webResult.error());
|
||||||
}
|
}
|
||||||
|
|
||||||
ESP_LOGI(kTag, "STA mode ready");
|
ESP_LOGI(kTag, "STA mode ready — hostname %.*s.local",
|
||||||
|
static_cast<int>(deviceIdentity.hostname().size()),
|
||||||
|
deviceIdentity.hostname().data());
|
||||||
return NetBootstrap(std::nullopt, std::move(sta), std::move(webServer),
|
return NetBootstrap(std::nullopt, std::move(sta), std::move(webServer),
|
||||||
NetState::StaConnected);
|
NetState::StaConnected);
|
||||||
}
|
}
|
||||||
@@ -176,7 +186,8 @@ NetBootstrap::start(core::ISecureStore& store, tuner::TunerService& tuner,
|
|||||||
bluetooth::BluetoothService& bluetooth,
|
bluetooth::BluetoothService& bluetooth,
|
||||||
station::StationService& stations,
|
station::StationService& stations,
|
||||||
integration::IntegrationService& integration,
|
integration::IntegrationService& integration,
|
||||||
core::CompanionChipStatus companionChips)
|
core::CompanionChipStatus companionChips,
|
||||||
|
const core::DeviceIdentity& deviceIdentity)
|
||||||
{
|
{
|
||||||
if (auto platform = initPlatform(); !platform) {
|
if (auto platform = initPlatform(); !platform) {
|
||||||
return std::unexpected(platform.error());
|
return std::unexpected(platform.error());
|
||||||
@@ -188,7 +199,8 @@ NetBootstrap::start(core::ISecureStore& store, tuner::TunerService& tuner,
|
|||||||
|
|
||||||
if (store.hasWifiCredentials()) {
|
if (store.hasWifiCredentials()) {
|
||||||
auto staResult = startStaMode(store, tuner, audio, bluetooth, stations,
|
auto staResult = startStaMode(store, tuner, audio, bluetooth, stations,
|
||||||
integration, companionChips);
|
integration, companionChips,
|
||||||
|
deviceIdentity);
|
||||||
if (staResult) {
|
if (staResult) {
|
||||||
return staResult;
|
return staResult;
|
||||||
}
|
}
|
||||||
@@ -196,7 +208,7 @@ NetBootstrap::start(core::ISecureStore& store, tuner::TunerService& tuner,
|
|||||||
}
|
}
|
||||||
|
|
||||||
return startSetupMode(store, tuner, audio, bluetooth, stations, integration,
|
return startSetupMode(store, tuner, audio, bluetooth, stations, integration,
|
||||||
companionChips);
|
companionChips, deviceIdentity);
|
||||||
}
|
}
|
||||||
|
|
||||||
NetBootstrap::NetBootstrap(std::optional<SoftApHost> softAp,
|
NetBootstrap::NetBootstrap(std::optional<SoftApHost> softAp,
|
||||||
|
|||||||
@@ -217,7 +217,9 @@ esp_err_t healthGetHandler(httpd_req_t* req)
|
|||||||
.bt1035Ready = false,
|
.bt1035Ready = false,
|
||||||
};
|
};
|
||||||
const core::HealthStatus status = core::HealthStatus::ok(
|
const core::HealthStatus status = core::HealthStatus::ok(
|
||||||
core::FirmwareVersion(kFirmwareVersion), chips);
|
core::FirmwareVersion(kFirmwareVersion), chips,
|
||||||
|
ctx != nullptr ? ctx->deviceIdentity.serialNumber()
|
||||||
|
: std::string_view("unknown"));
|
||||||
const std::string json = core::serializeHealthStatusJson(status);
|
const std::string json = core::serializeHealthStatusJson(status);
|
||||||
httpd_resp_set_type(req, "application/json");
|
httpd_resp_set_type(req, "application/json");
|
||||||
return httpd_resp_send(req, json.c_str(), json.size());
|
return httpd_resp_send(req, json.c_str(), json.size());
|
||||||
@@ -916,7 +918,7 @@ SetupWebServer::SetupWebServer(SetupWebServer&& other) noexcept
|
|||||||
other.stations_ = nullptr;
|
other.stations_ = nullptr;
|
||||||
other.integration_ = nullptr;
|
other.integration_ = nullptr;
|
||||||
other.routeContext_ = {nullptr, nullptr, nullptr, nullptr, nullptr,
|
other.routeContext_ = {nullptr, nullptr, nullptr, nullptr, nullptr,
|
||||||
nullptr, {}};
|
nullptr, {}, core::DeviceIdentity::unknown()};
|
||||||
}
|
}
|
||||||
|
|
||||||
SetupWebServer& SetupWebServer::operator=(SetupWebServer&& other) noexcept
|
SetupWebServer& SetupWebServer::operator=(SetupWebServer&& other) noexcept
|
||||||
@@ -943,7 +945,7 @@ SetupWebServer& SetupWebServer::operator=(SetupWebServer&& other) noexcept
|
|||||||
other.stations_ = nullptr;
|
other.stations_ = nullptr;
|
||||||
other.integration_ = nullptr;
|
other.integration_ = nullptr;
|
||||||
other.routeContext_ = {nullptr, nullptr, nullptr, nullptr, nullptr,
|
other.routeContext_ = {nullptr, nullptr, nullptr, nullptr, nullptr,
|
||||||
nullptr, {}};
|
nullptr, {}, core::DeviceIdentity::unknown()};
|
||||||
}
|
}
|
||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
@@ -954,7 +956,8 @@ SetupWebServer::~SetupWebServer()
|
|||||||
httpd_stop(server_);
|
httpd_stop(server_);
|
||||||
server_ = nullptr;
|
server_ = nullptr;
|
||||||
}
|
}
|
||||||
routeContext_ = {nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, {}};
|
routeContext_ = {nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, {},
|
||||||
|
core::DeviceIdentity::unknown()};
|
||||||
}
|
}
|
||||||
|
|
||||||
std::expected<void, NetError> SetupWebServer::start(
|
std::expected<void, NetError> SetupWebServer::start(
|
||||||
@@ -965,7 +968,8 @@ std::expected<void, NetError> SetupWebServer::start(
|
|||||||
bluetooth::BluetoothService& bluetooth,
|
bluetooth::BluetoothService& bluetooth,
|
||||||
station::StationService& stations,
|
station::StationService& stations,
|
||||||
integration::IntegrationService& integration,
|
integration::IntegrationService& integration,
|
||||||
core::CompanionChipStatus companionChips)
|
core::CompanionChipStatus companionChips,
|
||||||
|
const core::DeviceIdentity& deviceIdentity)
|
||||||
{
|
{
|
||||||
if (server_ != nullptr) {
|
if (server_ != nullptr) {
|
||||||
return {};
|
return {};
|
||||||
@@ -985,6 +989,7 @@ std::expected<void, NetError> SetupWebServer::start(
|
|||||||
routeContext_.stations = &stations;
|
routeContext_.stations = &stations;
|
||||||
routeContext_.integration = &integration;
|
routeContext_.integration = &integration;
|
||||||
routeContext_.companionChips = companionChips;
|
routeContext_.companionChips = companionChips;
|
||||||
|
routeContext_.deviceIdentity = deviceIdentity;
|
||||||
|
|
||||||
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
|
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
|
||||||
config.server_port = 80;
|
config.server_port = 80;
|
||||||
@@ -993,7 +998,7 @@ std::expected<void, NetError> SetupWebServer::start(
|
|||||||
if (httpd_start(&server_, &config) != ESP_OK) {
|
if (httpd_start(&server_, &config) != ESP_OK) {
|
||||||
ESP_LOGE(kTag, "httpd_start failed");
|
ESP_LOGE(kTag, "httpd_start failed");
|
||||||
routeContext_ = {nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
|
routeContext_ = {nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
|
||||||
{}};
|
{}, core::DeviceIdentity::unknown()};
|
||||||
return std::unexpected(NetError::HttpServerStartFailed);
|
return std::unexpected(NetError::HttpServerStartFailed);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,12 @@ constexpr std::uint8_t kSetupMaxConnections = 4;
|
|||||||
|
|
||||||
SoftApConfig SoftApConfig::setupDefault()
|
SoftApConfig SoftApConfig::setupDefault()
|
||||||
{
|
{
|
||||||
return SoftApConfig(kSetupSsid, kSetupChannel, kSetupMaxConnections);
|
return forSsid(kSetupSsid);
|
||||||
|
}
|
||||||
|
|
||||||
|
SoftApConfig SoftApConfig::forSsid(std::string_view ssid)
|
||||||
|
{
|
||||||
|
return SoftApConfig(ssid, kSetupChannel, kSetupMaxConnections);
|
||||||
}
|
}
|
||||||
|
|
||||||
SoftApConfig::SoftApConfig(std::string_view ssid,
|
SoftApConfig::SoftApConfig(std::string_view ssid,
|
||||||
|
|||||||
@@ -20,12 +20,15 @@
|
|||||||
|
|
||||||
#include "esp_event.h"
|
#include "esp_event.h"
|
||||||
#include "esp_log.h"
|
#include "esp_log.h"
|
||||||
|
#include "esp_netif.h"
|
||||||
#include "esp_wifi.h"
|
#include "esp_wifi.h"
|
||||||
|
#include "mdns.h"
|
||||||
#include "freertos/FreeRTOS.h"
|
#include "freertos/FreeRTOS.h"
|
||||||
#include "freertos/event_groups.h"
|
#include "freertos/event_groups.h"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
namespace net {
|
namespace net {
|
||||||
|
|
||||||
@@ -104,12 +107,17 @@ StaClient& StaClient::operator=(StaClient&& other) noexcept
|
|||||||
}
|
}
|
||||||
|
|
||||||
std::expected<void, NetError>
|
std::expected<void, NetError>
|
||||||
StaClient::connect(const core::WifiCredentials& creds)
|
StaClient::connect(const core::WifiCredentials& creds, std::string_view hostname)
|
||||||
{
|
{
|
||||||
if (connected_) {
|
if (connected_) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::string hostLabel;
|
||||||
|
if (!hostname.empty()) {
|
||||||
|
hostLabel.assign(hostname.begin(), hostname.end());
|
||||||
|
}
|
||||||
|
|
||||||
s_wifiEventGroup = xEventGroupCreate();
|
s_wifiEventGroup = xEventGroupCreate();
|
||||||
if (s_wifiEventGroup == nullptr) {
|
if (s_wifiEventGroup == nullptr) {
|
||||||
return std::unexpected(NetError::StaConnectFailed);
|
return std::unexpected(NetError::StaConnectFailed);
|
||||||
@@ -132,6 +140,13 @@ StaClient::connect(const core::WifiCredentials& creds)
|
|||||||
return std::unexpected(NetError::WifiConfigFailed);
|
return std::unexpected(NetError::WifiConfigFailed);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!hostLabel.empty()) {
|
||||||
|
esp_netif_t* netif = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF");
|
||||||
|
if (netif != nullptr) {
|
||||||
|
esp_netif_set_hostname(netif, hostLabel.c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
wifi_config_t wifiCfg = {};
|
wifi_config_t wifiCfg = {};
|
||||||
const std::string_view ssid = creds.ssid().value();
|
const std::string_view ssid = creds.ssid().value();
|
||||||
const std::size_t ssidCopy =
|
const std::size_t ssidCopy =
|
||||||
@@ -167,6 +182,11 @@ StaClient::connect(const core::WifiCredentials& creds)
|
|||||||
|
|
||||||
if ((bits & kConnectedBit) != 0) {
|
if ((bits & kConnectedBit) != 0) {
|
||||||
connected_ = true;
|
connected_ = true;
|
||||||
|
if (!hostLabel.empty()) {
|
||||||
|
if (mdns_init() == ESP_OK) {
|
||||||
|
mdns_hostname_set(hostLabel.c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
ESP_LOGI(kTag, "connected to %.*s",
|
ESP_LOGI(kTag, "connected to %.*s",
|
||||||
static_cast<int>(ssid.size()), ssid.data());
|
static_cast<int>(ssid.size()), ssid.data());
|
||||||
return {};
|
return {};
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ Returns a health-check DTO serialised by
|
|||||||
|
|
||||||
\begin{drnote}[Response schema]
|
\begin{drnote}[Response schema]
|
||||||
\begin{drcode}[JSON]
|
\begin{drcode}[JSON]
|
||||||
{"status":"ok","fw":"0.8.3",
|
{"status":"ok","fw":"0.8.3","serialNumber":"0004A3123456",
|
||||||
"chips":{"si4684":true,"adau1701":true,"bt1035":true}}
|
"chips":{"si4684":true,"adau1701":true,"bt1035":true}}
|
||||||
\end{drcode}
|
\end{drcode}
|
||||||
\begin{itemize}
|
\begin{itemize}
|
||||||
@@ -44,6 +44,8 @@ Returns a health-check DTO serialised by
|
|||||||
firmware is running normally.
|
firmware is running normally.
|
||||||
\item \texttt{fw} --- firmware release string
|
\item \texttt{fw} --- firmware release string
|
||||||
(\texttt{core::FirmwareVersion}).
|
(\texttt{core::FirmwareVersion}).
|
||||||
|
\item \texttt{serialNumber} --- canonical EEPROM serial, or
|
||||||
|
\texttt{unknown} when the 24AA025E48 read fails.
|
||||||
\item \texttt{chips} --- companion-chip boot flags after
|
\item \texttt{chips} --- companion-chip boot flags after
|
||||||
\texttt{HardwareBootstrap::boot()} (Slice~8 integration).
|
\texttt{HardwareBootstrap::boot()} (Slice~8 integration).
|
||||||
\end{itemize}
|
\end{itemize}
|
||||||
|
|||||||
@@ -36,9 +36,26 @@ trusted downstream by \texttt{ITuner}, \texttt{Si4684Driver}, and status DTOs.
|
|||||||
|
|
||||||
\section{HealthStatus}\label{cls:HealthStatus}
|
\section{HealthStatus}\label{cls:HealthStatus}
|
||||||
Immutable health-check DTO returned by \texttt{GET /api/health}. Built via
|
Immutable health-check DTO returned by \texttt{GET /api/health}. Built via
|
||||||
\texttt{HealthStatus::ok(FirmwareVersion)} in the pure core; JSON
|
\texttt{HealthStatus::ok(FirmwareVersion, CompanionChipStatus, serialNumber)}
|
||||||
serialisation lives in \texttt{serializeHealthStatusJson()}. No ESP-IDF
|
in the pure core; JSON serialisation lives in \texttt{serializeHealthStatusJson()}.
|
||||||
headers.
|
The \texttt{serialNumber} field carries the EEPROM-derived serial or
|
||||||
|
\texttt{unknown} when the EUI-48 read fails. No ESP-IDF headers.
|
||||||
|
|
||||||
|
\section{Eui48}\label{cls:Eui48}
|
||||||
|
Strong type for the factory-programmed six-byte EUI-48 read from the
|
||||||
|
24AA025E48 at word address \texttt{0xFA} (Microchip DS20001191). Exposes
|
||||||
|
\texttt{serialNumber()} (12 uppercase hex digits) and \texttt{shortSuffix()}
|
||||||
|
(last three bytes) for SSID and hostname derivation.
|
||||||
|
|
||||||
|
\section{DeviceIdentity}\label{cls:DeviceIdentity}
|
||||||
|
Per-board identity derived from \texttt{Eui48}: SoftAP SSID
|
||||||
|
\texttt{DigiRadio-<suffix>}, Bluetooth name, STA hostname
|
||||||
|
\texttt{digiradio-<suffix>}, and canonical serial. \texttt{unknown()} yields
|
||||||
|
documented fallbacks (\texttt{DigiRadio-setup}, serial \texttt{unknown}).
|
||||||
|
|
||||||
|
\section{IDeviceIdentitySource}\label{cls:IDeviceIdentitySource}
|
||||||
|
Port for reading \texttt{DeviceIdentity} from board storage. Implemented in
|
||||||
|
the shell by \texttt{eeprom24aa::Eeprom24aa} on the shared I2C bus.
|
||||||
|
|
||||||
% ------------------------------------------------------------------
|
% ------------------------------------------------------------------
|
||||||
% Network shell (Slice 1)
|
% Network shell (Slice 1)
|
||||||
@@ -46,8 +63,9 @@ headers.
|
|||||||
|
|
||||||
\section{SoftApConfig}\label{cls:SoftApConfig}
|
\section{SoftApConfig}\label{cls:SoftApConfig}
|
||||||
Immutable value type holding SoftAP parameters (SSID, channel, station
|
Immutable value type holding SoftAP parameters (SSID, channel, station
|
||||||
limit). Factory \texttt{setupDefault()} yields SSID \texttt{DigiRadio-setup}
|
limit). \texttt{forSsid()} builds setup parameters from
|
||||||
for first-time provisioning.
|
\texttt{DeviceIdentity::softApSsid()}; \texttt{setupDefault()} keeps the
|
||||||
|
\texttt{DigiRadio-setup} fallback when the EEPROM read fails.
|
||||||
|
|
||||||
\section{SoftApHost}\label{cls:SoftApHost}
|
\section{SoftApHost}\label{cls:SoftApHost}
|
||||||
RAII wrapper around the ESP-IDF Wi-Fi stack that starts and stops the
|
RAII wrapper around the ESP-IDF Wi-Fi stack that starts and stops the
|
||||||
@@ -147,10 +165,18 @@ RAII I\textsuperscript{2}C driver for the ADAU1701 SigmaDSP
|
|||||||
(Chapter~\ref{ch:adau1701}). \texttt{boot()} asserts RESET\#,
|
(Chapter~\ref{ch:adau1701}). \texttt{boot()} asserts RESET\#,
|
||||||
initialises the shared I2C bus, and replays the SigmaStudio export from
|
initialises the shared I2C bus, and replays the SigmaStudio export from
|
||||||
\texttt{Firmware/ADAU1701-Firmware/} on every power-up (no EEPROM self-boot
|
\texttt{Firmware/ADAU1701-Firmware/} on every power-up (no EEPROM self-boot
|
||||||
on DigiRadio). Runtime mixer, EQ, and master volume updates use the
|
on DigiRadio). Exposes \texttt{i2cBusHandle()} after boot for the shared
|
||||||
|
24AA025E48 EEPROM. Runtime mixer, EQ, and master volume updates use the
|
||||||
ADAU1701 safeload mechanism via \texttt{applyProfile()} and related methods
|
ADAU1701 safeload mechanism via \texttt{applyProfile()} and related methods
|
||||||
(Section~\ref{sec:adau1701-driver}).
|
(Section~\ref{sec:adau1701-driver}).
|
||||||
|
|
||||||
|
\section{Eeprom24aa}\label{cls:Eeprom24aa}
|
||||||
|
I\textsuperscript{2}C shell driver for the 24AA025E48 identity EEPROM
|
||||||
|
(Chapter~\ref{ch:hardware}, address \texttt{0x52}). Implements
|
||||||
|
\texttt{IDeviceIdentitySource}: reads six bytes at word address \texttt{0xFA}
|
||||||
|
per Microchip DS20001191 and builds \texttt{DeviceIdentity}. Called from
|
||||||
|
\texttt{HardwareBootstrap} after \texttt{Adau1701Driver::boot()}.
|
||||||
|
|
||||||
\section{Adau1701Dsp}\label{cls:Adau1701Dsp}
|
\section{Adau1701Dsp}\label{cls:Adau1701Dsp}
|
||||||
\texttt{core::IDsp} adapter over \texttt{Adau1701Driver}. Maps domain-level
|
\texttt{core::IDsp} adapter over \texttt{Adau1701Driver}. Maps domain-level
|
||||||
audio control to safeload I\textsuperscript{2}C transactions without exposing
|
audio control to safeload I\textsuperscript{2}C transactions without exposing
|
||||||
|
|||||||
@@ -3,5 +3,5 @@ idf_component_register(
|
|||||||
"main.cpp"
|
"main.cpp"
|
||||||
"hardware_bootstrap.cpp"
|
"hardware_bootstrap.cpp"
|
||||||
INCLUDE_DIRS "."
|
INCLUDE_DIRS "."
|
||||||
REQUIRES core net secure_store adau1701 si4684 tuner audio bt1035 bluetooth station integration
|
REQUIRES core net secure_store adau1701 si4684 tuner audio bt1035 bluetooth station integration eeprom24aa
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -18,6 +18,9 @@
|
|||||||
#include "audio/AudioService.hpp"
|
#include "audio/AudioService.hpp"
|
||||||
#include "board_pins.hpp"
|
#include "board_pins.hpp"
|
||||||
#include "bt1035/Bt1035Driver.hpp"
|
#include "bt1035/Bt1035Driver.hpp"
|
||||||
|
#include "core/DeviceIdentity.hpp"
|
||||||
|
#include "driver/i2c_master.h"
|
||||||
|
#include "eeprom24aa/Eeprom24aa.hpp"
|
||||||
#include "secure_store/NvsAudioProfileStore.hpp"
|
#include "secure_store/NvsAudioProfileStore.hpp"
|
||||||
#include "si4684/Si4684Band.hpp"
|
#include "si4684/Si4684Band.hpp"
|
||||||
#include "si4684/Si4684Driver.hpp"
|
#include "si4684/Si4684Driver.hpp"
|
||||||
@@ -69,6 +72,7 @@ bt1035::Bt1035Driver gBt1035(
|
|||||||
.sysCtlGpio = board::pins::Bt1035SysCtl,
|
.sysCtlGpio = board::pins::Bt1035SysCtl,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
core::DeviceIdentity gDeviceIdentity = core::DeviceIdentity::unknown();
|
||||||
bool gReady = false;
|
bool gReady = false;
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
@@ -91,6 +95,21 @@ std::expected<void, HardwareBootError> HardwareBootstrap::boot()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const auto* busHandle =
|
||||||
|
static_cast<i2c_master_bus_handle_t>(gAdau1701.i2cBusHandle());
|
||||||
|
eeprom24aa::Eeprom24aa eeprom(busHandle,
|
||||||
|
static_cast<std::uint8_t>(
|
||||||
|
board::pins::Eeprom24aaAddr));
|
||||||
|
if (auto identity = eeprom.readDeviceIdentity(); identity) {
|
||||||
|
gDeviceIdentity = std::move(*identity);
|
||||||
|
ESP_LOGI(kTag, "unit serial %.*s",
|
||||||
|
static_cast<int>(gDeviceIdentity.serialNumber().size()),
|
||||||
|
gDeviceIdentity.serialNumber().data());
|
||||||
|
} else {
|
||||||
|
gDeviceIdentity = core::DeviceIdentity::unknown();
|
||||||
|
ESP_LOGW(kTag, "EUI-48 read failed — using fallback identity");
|
||||||
|
}
|
||||||
|
|
||||||
if (auto audioResult = gAudioService.loadAndApply(); !audioResult) {
|
if (auto audioResult = gAudioService.loadAndApply(); !audioResult) {
|
||||||
ESP_LOGW(kTag, "ADAU1701 profile apply failed");
|
ESP_LOGW(kTag, "ADAU1701 profile apply failed");
|
||||||
}
|
}
|
||||||
@@ -100,6 +119,11 @@ std::expected<void, HardwareBootError> HardwareBootstrap::boot()
|
|||||||
return std::unexpected(HardwareBootError::Bt1035BootFailed);
|
return std::unexpected(HardwareBootError::Bt1035BootFailed);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (auto nameResult =
|
||||||
|
gBt1035.setDeviceName(gDeviceIdentity.bluetoothName()); !nameResult) {
|
||||||
|
ESP_LOGW(kTag, "BT1035 device name set failed");
|
||||||
|
}
|
||||||
|
|
||||||
gReady = true;
|
gReady = true;
|
||||||
ESP_LOGI(kTag, "companion chips ready");
|
ESP_LOGI(kTag, "companion chips ready");
|
||||||
return {};
|
return {};
|
||||||
@@ -129,4 +153,9 @@ bt1035::Bt1035Driver& HardwareBootstrap::bt1035Driver()
|
|||||||
return gBt1035;
|
return gBt1035;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const core::DeviceIdentity& HardwareBootstrap::deviceIdentity() noexcept
|
||||||
|
{
|
||||||
|
return gDeviceIdentity;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace hardware
|
} // namespace hardware
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "core/CompanionChipStatus.hpp"
|
#include "core/CompanionChipStatus.hpp"
|
||||||
|
#include "core/DeviceIdentity.hpp"
|
||||||
|
|
||||||
#include "bt1035/Bt1035Driver.hpp"
|
#include "bt1035/Bt1035Driver.hpp"
|
||||||
|
|
||||||
@@ -124,6 +125,18 @@ public:
|
|||||||
* @date 2026-07-06
|
* @date 2026-07-06
|
||||||
*/
|
*/
|
||||||
[[nodiscard]] static bt1035::Bt1035Driver& bt1035Driver();
|
[[nodiscard]] static bt1035::Bt1035Driver& bt1035Driver();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief deviceIdentity — per-board identity from the 24AA025E48 EUI-48.
|
||||||
|
*
|
||||||
|
* @dname deviceIdentity
|
||||||
|
* @return Reference to identity loaded during boot().
|
||||||
|
* @pubstate reads gDeviceIdentity after boot(); unknown() on EEPROM failure.
|
||||||
|
*
|
||||||
|
* @author Michele Bigi
|
||||||
|
* @date 2026-07-07
|
||||||
|
*/
|
||||||
|
[[nodiscard]] static const core::DeviceIdentity& deviceIdentity() noexcept;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace hardware
|
} // namespace hardware
|
||||||
|
|||||||
@@ -85,7 +85,8 @@ extern "C" void app_main()
|
|||||||
bluetoothService,
|
bluetoothService,
|
||||||
stationService,
|
stationService,
|
||||||
integration,
|
integration,
|
||||||
hardware::HardwareBootstrap::companionChipStatus());
|
hardware::HardwareBootstrap::companionChipStatus(),
|
||||||
|
hardware::HardwareBootstrap::deviceIdentity());
|
||||||
if (!netResult) {
|
if (!netResult) {
|
||||||
ESP_LOGE(kTag, "network bootstrap failed");
|
ESP_LOGE(kTag, "network bootstrap failed");
|
||||||
return;
|
return;
|
||||||
|
|||||||
Reference in New Issue
Block a user