Add BT1035 pairing and station presets (fw 0.7.0).
Expose discoverable mode and A2DP control over REST, add persisted DAB/FM preset list with web UI, and document gaps in docs/TODO.md. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* @file BluetoothJson.hpp
|
||||
* @brief JSON serialisation for Bluetooth REST API (pure core).
|
||||
*
|
||||
* DigiRadio firmware — https://github.com/manvalan/DigiRadio
|
||||
*
|
||||
* Copyright 2026 Michele Bigi
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "core/Bt1035At.hpp"
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace core {
|
||||
|
||||
/**
|
||||
* @brief BluetoothStatus — snapshot for GET /api/bluetooth/status.
|
||||
*
|
||||
* @dname BluetoothStatus
|
||||
* @return n/a (type)
|
||||
* @pubstate Plain DTO assembled by BluetoothService.
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
struct BluetoothStatus {
|
||||
bool booted; ///< BT1035 driver ready after boot().
|
||||
bool pairing; ///< Discoverable mode requested by firmware.
|
||||
Bt1035A2dpState a2dpState; ///< Last read A2DP link state.
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief serializeBluetoothStatusJson — serialise BluetoothStatus for HTTP.
|
||||
*
|
||||
* @dname serializeBluetoothStatusJson
|
||||
* @param status Domain snapshot.
|
||||
* @return JSON object string.
|
||||
* @pubstate none
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
[[nodiscard]] std::string serializeBluetoothStatusJson(
|
||||
const BluetoothStatus& status);
|
||||
|
||||
/**
|
||||
* @brief serializeBluetoothErrorJson — serialise a Bluetooth API error.
|
||||
*
|
||||
* @dname serializeBluetoothErrorJson
|
||||
* @param reason Short machine-readable cause.
|
||||
* @return JSON error object.
|
||||
* @pubstate none
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
[[nodiscard]] std::string serializeBluetoothErrorJson(const char* reason);
|
||||
|
||||
} // namespace core
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <expected>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
@@ -33,8 +34,31 @@ namespace core {
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
enum class Bt1035AtCommand {
|
||||
Ping, ///< AT — link check.
|
||||
AuxLineIn, ///< AT+AUXCFG=1 — wired Line-In from ADAU1701 (mandatory).
|
||||
Ping, ///< AT — link check.
|
||||
AuxLineIn, ///< AT+AUXCFG=1 — wired Line-In from ADAU1701 (mandatory).
|
||||
PairDiscoverable, ///< AT+PAIR=1 — enter BR/EDR/BLE discoverable mode.
|
||||
PairHidden, ///< AT+PAIR=0 — leave discoverable mode.
|
||||
A2dpStat, ///< AT+A2DPSTAT — read A2DP link state.
|
||||
A2dpDisconnect, ///< AT+A2DPDISC — release current A2DP connection.
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Bt1035A2dpState — A2DP link state from +A2DPSTAT=Param (BT1035 manual).
|
||||
*
|
||||
* @dname Bt1035A2dpState
|
||||
* @return n/a (type)
|
||||
* @pubstate Numeric values match Feasycom firmware event codes.
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
enum class Bt1035A2dpState : std::uint8_t {
|
||||
Unsupported = 0,
|
||||
Standby = 1,
|
||||
Connecting = 2,
|
||||
Connected = 3,
|
||||
Streaming = 4,
|
||||
Paused = 5,
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -96,4 +120,31 @@ bootInitSequence() noexcept;
|
||||
[[nodiscard]] Bt1035AtResponseKind parseBt1035AtResponse(
|
||||
std::string_view line) noexcept;
|
||||
|
||||
/**
|
||||
* @brief parseBt1035A2dpStatResponse — extract A2DP state from UART payload.
|
||||
*
|
||||
* @dname parseBt1035A2dpStatResponse
|
||||
* @param response Full module reply (may include +A2DPSTAT and OK lines).
|
||||
* @return Bt1035A2dpState on success, or ParseError::MissingField.
|
||||
* @pubstate none
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
[[nodiscard]] std::expected<Bt1035A2dpState, ParseError>
|
||||
parseBt1035A2dpStatResponse(std::string_view response);
|
||||
|
||||
/**
|
||||
* @brief a2dpStateToken — serialise A2DP state for JSON APIs.
|
||||
*
|
||||
* @dname a2dpStateToken
|
||||
* @param state Parsed A2DP link state.
|
||||
* @return Short stable string (e.g. "streaming").
|
||||
* @pubstate none
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
[[nodiscard]] const char* a2dpStateToken(Bt1035A2dpState state) noexcept;
|
||||
|
||||
} // namespace core
|
||||
|
||||
@@ -21,6 +21,8 @@
|
||||
#include "core/WifiCredentials.hpp"
|
||||
|
||||
#include <expected>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace core {
|
||||
|
||||
@@ -103,6 +105,58 @@ public:
|
||||
*/
|
||||
[[nodiscard]] virtual std::expected<void, StoreError>
|
||||
clearWifiCredentials() = 0;
|
||||
|
||||
/**
|
||||
* @brief hasStationList — check whether presets are stored.
|
||||
*
|
||||
* @dname hasStationList
|
||||
* @return true when loadStationListJson would succeed.
|
||||
* @pubstate reads backing storage via implementation.
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
[[nodiscard]] virtual bool hasStationList() const = 0;
|
||||
|
||||
/**
|
||||
* @brief saveStationListJson — persist serialised preset list JSON.
|
||||
*
|
||||
* @dname saveStationListJson
|
||||
* @param json Output of core::serializeStationListJson().
|
||||
* @return Ok on success, or StoreError::IoFailed.
|
||||
* @pubstate writes backing storage via implementation.
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
[[nodiscard]] virtual std::expected<void, StoreError>
|
||||
saveStationListJson(std::string_view json) = 0;
|
||||
|
||||
/**
|
||||
* @brief loadStationListJson — read stored preset list JSON.
|
||||
*
|
||||
* @dname loadStationListJson
|
||||
* @return JSON blob on success, or StoreError.
|
||||
* @pubstate reads backing storage via implementation.
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
[[nodiscard]] virtual std::expected<std::string, StoreError>
|
||||
loadStationListJson() const = 0;
|
||||
|
||||
/**
|
||||
* @brief clearStationList — erase stored presets.
|
||||
*
|
||||
* @dname clearStationList
|
||||
* @return Ok on success, or StoreError::IoFailed.
|
||||
* @pubstate clears backing storage via implementation.
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
[[nodiscard]] virtual std::expected<void, StoreError>
|
||||
clearStationList() = 0;
|
||||
};
|
||||
|
||||
} // namespace core
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* @file PresetSlot.hpp
|
||||
* @brief Strong type for a physical preset button slot (1–20).
|
||||
*
|
||||
* DigiRadio firmware — https://github.com/manvalan/DigiRadio
|
||||
*
|
||||
* Copyright 2026 Michele Bigi
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "core/ParseError.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
#include <expected>
|
||||
|
||||
namespace core {
|
||||
|
||||
/**
|
||||
* @brief PresetSlot — optional hardware preset index on the radio.
|
||||
*
|
||||
* @dname PresetSlot
|
||||
* @return n/a (type)
|
||||
* @pubstate Owns slot_ in [1, kMaxSlot]. Immutable after construction.
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
class PresetSlot {
|
||||
public:
|
||||
/** Highest preset button index supported by the list model. */
|
||||
static constexpr std::uint8_t kMaxSlot = 20U;
|
||||
|
||||
/**
|
||||
* @brief tryFrom — validate a preset slot at the boundary.
|
||||
*
|
||||
* @dname tryFrom
|
||||
* @param slot Untrusted slot number from JSON (1–20).
|
||||
* @return PresetSlot on success, or ParseError::MissingField.
|
||||
* @pubstate none
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
[[nodiscard]] static std::expected<PresetSlot, ParseError>
|
||||
tryFrom(unsigned slot) noexcept;
|
||||
|
||||
/**
|
||||
* @brief value — read the slot number.
|
||||
*
|
||||
* @dname value
|
||||
* @return Slot index 1–20.
|
||||
* @pubstate reads slot_.
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
[[nodiscard]] std::uint8_t value() const noexcept;
|
||||
|
||||
private:
|
||||
explicit PresetSlot(std::uint8_t slot) noexcept;
|
||||
|
||||
std::uint8_t slot_;
|
||||
};
|
||||
|
||||
} // namespace core
|
||||
@@ -0,0 +1,93 @@
|
||||
/**
|
||||
* @file Station.hpp
|
||||
* @brief Value type for one saved tuner preset (DAB or FM).
|
||||
*
|
||||
* DigiRadio firmware — https://github.com/manvalan/DigiRadio
|
||||
*
|
||||
* Copyright 2026 Michele Bigi
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "core/FrequencyKHz.hpp"
|
||||
#include "core/PresetSlot.hpp"
|
||||
#include "core/StationName.hpp"
|
||||
#include "core/TunerBand.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
|
||||
namespace core {
|
||||
|
||||
/**
|
||||
* @brief Station — immutable preset: label, band, tune target, optional slot.
|
||||
*
|
||||
* @dname Station
|
||||
* @return n/a (type)
|
||||
* @pubstate All fields fixed at construction. FM presets carry fmFrequency_;
|
||||
* DAB presets carry dabFreqIndex_ and optional service/component ids.
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
class Station {
|
||||
public:
|
||||
/**
|
||||
* @brief Station — construct a validated preset value.
|
||||
*
|
||||
* @dname Station
|
||||
* @param name User-visible label.
|
||||
* @param band DAB or FM target band.
|
||||
* @param dabFreqIndex Ensemble index 0–37 when band is Dab.
|
||||
* @param dabServiceId Optional DAB service id for play().
|
||||
* @param dabComponentId Optional DAB component id for play().
|
||||
* @param fmFrequency FM centre frequency when band is Fm.
|
||||
* @param presetSlot Optional hardware preset button slot.
|
||||
* @pubstate stores immutable tune target fields.
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
Station(StationName name,
|
||||
TunerBand band,
|
||||
std::uint8_t dabFreqIndex,
|
||||
std::optional<std::uint32_t> dabServiceId,
|
||||
std::optional<std::uint32_t> dabComponentId,
|
||||
std::optional<FrequencyKHz> fmFrequency,
|
||||
std::optional<PresetSlot> presetSlot);
|
||||
|
||||
[[nodiscard]] const StationName& name() const noexcept;
|
||||
[[nodiscard]] TunerBand band() const noexcept;
|
||||
[[nodiscard]] std::uint8_t dabFreqIndex() const noexcept;
|
||||
[[nodiscard]] std::optional<std::uint32_t> dabServiceId() const noexcept;
|
||||
[[nodiscard]] std::optional<std::uint32_t> dabComponentId() const noexcept;
|
||||
[[nodiscard]] std::optional<FrequencyKHz> fmFrequency() const noexcept;
|
||||
[[nodiscard]] std::optional<PresetSlot> presetSlot() const noexcept;
|
||||
|
||||
/**
|
||||
* @brief sameTuneTarget — compare band-specific tune coordinates.
|
||||
*
|
||||
* @dname sameTuneTarget
|
||||
* @param other Candidate preset.
|
||||
* @return true when both would tune/play the same source.
|
||||
* @pubstate none
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
[[nodiscard]] bool sameTuneTarget(const Station& other) const noexcept;
|
||||
|
||||
private:
|
||||
StationName name_;
|
||||
TunerBand band_;
|
||||
std::uint8_t dabFreqIndex_;
|
||||
std::optional<std::uint32_t> dabServiceId_;
|
||||
std::optional<std::uint32_t> dabComponentId_;
|
||||
std::optional<FrequencyKHz> fmFrequency_;
|
||||
std::optional<PresetSlot> presetSlot_;
|
||||
};
|
||||
|
||||
} // namespace core
|
||||
@@ -0,0 +1,123 @@
|
||||
/**
|
||||
* @file StationList.hpp
|
||||
* @brief In-memory collection of tuner presets with CRUD validation.
|
||||
*
|
||||
* DigiRadio firmware — https://github.com/manvalan/DigiRadio
|
||||
*
|
||||
* Copyright 2026 Michele Bigi
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "core/Station.hpp"
|
||||
#include "core/StationListError.hpp"
|
||||
|
||||
#include <cstddef>
|
||||
#include <expected>
|
||||
#include <vector>
|
||||
|
||||
namespace core {
|
||||
|
||||
/**
|
||||
* @brief StationList — ordered preset collection (pure domain).
|
||||
*
|
||||
* @dname StationList
|
||||
* @return n/a (type)
|
||||
* @pubstate Owns stations_ (max kMaxStations entries). Duplicate tune targets
|
||||
* and preset slots are rejected on add().
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
class StationList {
|
||||
public:
|
||||
/** Maximum presets persisted in NVS. */
|
||||
static constexpr std::size_t kMaxStations = 20U;
|
||||
|
||||
/**
|
||||
* @brief StationList — construct an empty list.
|
||||
*
|
||||
* @dname StationList
|
||||
* @pubstate clears stations_.
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
StationList();
|
||||
|
||||
/**
|
||||
* @brief stations — read the ordered preset vector.
|
||||
*
|
||||
* @dname stations
|
||||
* @return Const reference to internal storage.
|
||||
* @pubstate reads stations_.
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
[[nodiscard]] const std::vector<Station>& stations() const noexcept;
|
||||
|
||||
/**
|
||||
* @brief replaceAll — replace the entire list (e.g. after NVS load).
|
||||
*
|
||||
* @dname replaceAll
|
||||
* @param stations Validated presets (caller must enforce limits).
|
||||
* @pubstate moves stations into stations_.
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
void replaceAll(std::vector<Station> stations);
|
||||
|
||||
/**
|
||||
* @brief add — append a preset after duplicate and capacity checks.
|
||||
*
|
||||
* @dname add
|
||||
* @param station New preset value.
|
||||
* @return Ok on success, or StationListError.
|
||||
* @pubstate appends to stations_ on success.
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
[[nodiscard]] std::expected<void, StationListError> add(Station station);
|
||||
|
||||
/**
|
||||
* @brief removeAt — delete preset by list index.
|
||||
*
|
||||
* @dname removeAt
|
||||
* @param index Zero-based position in stations().
|
||||
* @return Ok on success, or StationListError::NotFound.
|
||||
* @pubstate erases one element on success.
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
[[nodiscard]] std::expected<void, StationListError> removeAt(std::size_t index);
|
||||
|
||||
/**
|
||||
* @brief move — reorder a preset within the list.
|
||||
*
|
||||
* @dname move
|
||||
* @param fromIndex Current index.
|
||||
* @param toIndex Target index.
|
||||
* @return Ok on success, or StationListError::NotFound.
|
||||
* @pubstate reorders stations_ on success.
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
[[nodiscard]] std::expected<void, StationListError> move(std::size_t fromIndex,
|
||||
std::size_t toIndex);
|
||||
|
||||
private:
|
||||
[[nodiscard]] bool hasDuplicateTuneTarget(const Station& candidate) const;
|
||||
[[nodiscard]] bool slotTaken(const PresetSlot& slot) const;
|
||||
|
||||
std::vector<Station> stations_;
|
||||
};
|
||||
|
||||
} // namespace core
|
||||
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* @file StationListError.hpp
|
||||
* @brief Domain errors for station preset list operations.
|
||||
*
|
||||
* DigiRadio firmware — https://github.com/manvalan/DigiRadio
|
||||
*
|
||||
* Copyright 2026 Michele Bigi
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
namespace core {
|
||||
|
||||
/**
|
||||
* @brief StationListError — failure causes for preset list CRUD.
|
||||
*
|
||||
* @dname StationListError
|
||||
* @return n/a (type)
|
||||
* @pubstate n/a
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
enum class StationListError {
|
||||
Duplicate,
|
||||
Full,
|
||||
NotFound,
|
||||
SlotInUse,
|
||||
};
|
||||
|
||||
} // namespace core
|
||||
@@ -0,0 +1,122 @@
|
||||
/**
|
||||
* @file StationListJson.hpp
|
||||
* @brief JSON parse/serialise for station preset list (pure core).
|
||||
*
|
||||
* DigiRadio firmware — https://github.com/manvalan/DigiRadio
|
||||
*
|
||||
* Copyright 2026 Michele Bigi
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "core/ParseError.hpp"
|
||||
#include "core/Station.hpp"
|
||||
#include "core/StationList.hpp"
|
||||
#include "core/StationListError.hpp"
|
||||
|
||||
#include <cstddef>
|
||||
#include <expected>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace core {
|
||||
|
||||
/**
|
||||
* @brief StationRemoveRequest — parsed POST /api/stations/remove body.
|
||||
*
|
||||
* @dname StationRemoveRequest
|
||||
* @return n/a (type)
|
||||
* @pubstate Plain DTO filled by parseStationRemoveJson.
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
struct StationRemoveRequest {
|
||||
std::size_t index; ///< Zero-based list index to delete.
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief serializeStationListJson — serialise all presets for GET /api/stations.
|
||||
*
|
||||
* @dname serializeStationListJson
|
||||
* @param list Domain preset collection.
|
||||
* @return JSON object with a stations array.
|
||||
* @pubstate none
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
[[nodiscard]] std::string serializeStationListJson(const StationList& list);
|
||||
|
||||
/**
|
||||
* @brief parseStationListJson — load presets from persisted NVS JSON.
|
||||
*
|
||||
* @dname parseStationListJson
|
||||
* @param json Untrusted blob from secure storage.
|
||||
* @return StationList on success, or ParseError.
|
||||
* @pubstate none
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
[[nodiscard]] std::expected<StationList, ParseError>
|
||||
parseStationListJson(std::string_view json);
|
||||
|
||||
/**
|
||||
* @brief parseStationJson — validate one POST /api/stations body.
|
||||
*
|
||||
* @dname parseStationJson
|
||||
* @param json Untrusted request body.
|
||||
* @return Station on success, or ParseError.
|
||||
* @pubstate none
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
[[nodiscard]] std::expected<Station, ParseError> parseStationJson(
|
||||
std::string_view json);
|
||||
|
||||
/**
|
||||
* @brief parseStationRemoveJson — validate POST /api/stations/remove body.
|
||||
*
|
||||
* @dname parseStationRemoveJson
|
||||
* @param json Untrusted request body with index field.
|
||||
* @return StationRemoveRequest on success, or ParseError.
|
||||
* @pubstate none
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
[[nodiscard]] std::expected<StationRemoveRequest, ParseError>
|
||||
parseStationRemoveJson(std::string_view json);
|
||||
|
||||
/**
|
||||
* @brief serializeStationListErrorJson — serialise a station API error.
|
||||
*
|
||||
* @dname serializeStationListErrorJson
|
||||
* @param reason Short machine-readable cause.
|
||||
* @return JSON error object.
|
||||
* @pubstate none
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
[[nodiscard]] std::string serializeStationListErrorJson(const char* reason);
|
||||
|
||||
/**
|
||||
* @brief stationListErrorToken — map domain error to API reason string.
|
||||
*
|
||||
* @dname stationListErrorToken
|
||||
* @param error StationListError from add/remove/move.
|
||||
* @return Stable reason token.
|
||||
* @pubstate none
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
[[nodiscard]] const char* stationListErrorToken(StationListError error) noexcept;
|
||||
|
||||
} // namespace core
|
||||
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* @file StationName.hpp
|
||||
* @brief Strong type for a user-visible station preset label.
|
||||
*
|
||||
* DigiRadio firmware — https://github.com/manvalan/DigiRadio
|
||||
*
|
||||
* Copyright 2026 Michele Bigi
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "core/ParseError.hpp"
|
||||
|
||||
#include <cstddef>
|
||||
#include <expected>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace core {
|
||||
|
||||
/**
|
||||
* @brief StationName — validated preset display name (1–32 UTF-8 bytes).
|
||||
*
|
||||
* @dname StationName
|
||||
* @return n/a (type)
|
||||
* @pubstate Owns name_ (immutable after construction).
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
class StationName {
|
||||
public:
|
||||
/** Maximum label length stored in NVS. */
|
||||
static constexpr std::size_t kMaxLength = 32;
|
||||
|
||||
/**
|
||||
* @brief tryFrom — validate a station name at the HTTP boundary.
|
||||
*
|
||||
* @dname tryFrom
|
||||
* @param raw Untrusted label from JSON input.
|
||||
* @return StationName on success, or ParseError::MissingField.
|
||||
* @pubstate none
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
[[nodiscard]] static std::expected<StationName, ParseError>
|
||||
tryFrom(std::string_view raw);
|
||||
|
||||
/**
|
||||
* @brief value — read the stored label.
|
||||
*
|
||||
* @dname value
|
||||
* @return Validated preset name.
|
||||
* @pubstate reads name_.
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
[[nodiscard]] std::string_view value() const noexcept;
|
||||
|
||||
private:
|
||||
explicit StationName(std::string name);
|
||||
|
||||
std::string name_;
|
||||
};
|
||||
|
||||
} // namespace core
|
||||
Reference in New Issue
Block a user