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,8 @@
|
||||
idf_component_register(
|
||||
SRCS
|
||||
"src/StationService.cpp"
|
||||
INCLUDE_DIRS "include"
|
||||
REQUIRES core tuner
|
||||
)
|
||||
|
||||
target_compile_features(${COMPONENT_LIB} PUBLIC cxx_std_23)
|
||||
@@ -0,0 +1,127 @@
|
||||
/**
|
||||
* @file StationService.hpp
|
||||
* @brief Preset list orchestration with NVS persistence and tuner recall.
|
||||
*
|
||||
* 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/ISecureStore.hpp"
|
||||
#include "core/Station.hpp"
|
||||
#include "core/StationList.hpp"
|
||||
#include "core/StationListError.hpp"
|
||||
#include "core/StoreError.hpp"
|
||||
#include "core/TunerError.hpp"
|
||||
#include "tuner/TunerService.hpp"
|
||||
|
||||
#include <cstddef>
|
||||
#include <expected>
|
||||
|
||||
namespace station {
|
||||
|
||||
/**
|
||||
* @brief StationService — CRUD presets and tune via TunerService.
|
||||
*
|
||||
* @dname StationService
|
||||
* @return n/a (type)
|
||||
* @pubstate Owns in-memory StationList mirrored to ISecureStore on mutation.
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
class StationService {
|
||||
public:
|
||||
/**
|
||||
* @brief StationService — bind store and tuner for the process lifetime.
|
||||
*
|
||||
* @dname StationService
|
||||
* @param store Secure persistence for preset JSON.
|
||||
* @param tuner Tuner orchestration for recall.
|
||||
* @pubstate stores references; list empty until loadFromStore().
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
StationService(core::ISecureStore& store, tuner::TunerService& tuner);
|
||||
|
||||
/**
|
||||
* @brief loadFromStore — hydrate list from NVS (empty when absent).
|
||||
*
|
||||
* @dname loadFromStore
|
||||
* @return Ok on success, or StoreError / parse failure as IoFailed.
|
||||
* @pubstate replaces list_ from NVS JSON when present.
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
[[nodiscard]] std::expected<void, core::StoreError> loadFromStore();
|
||||
|
||||
/**
|
||||
* @brief list — read the in-memory preset collection.
|
||||
*
|
||||
* @dname list
|
||||
* @return Const reference to list_.
|
||||
* @pubstate reads list_.
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
[[nodiscard]] const core::StationList& list() const noexcept;
|
||||
|
||||
/**
|
||||
* @brief add — append a preset and persist.
|
||||
*
|
||||
* @dname add
|
||||
* @param station Validated preset from JSON boundary.
|
||||
* @return Ok on success, or StationListError / StoreError.
|
||||
* @pubstate mutates list_ and NVS on success.
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
[[nodiscard]] std::expected<void, core::StationListError> add(
|
||||
core::Station station);
|
||||
|
||||
/**
|
||||
* @brief removeAt — delete preset by index and persist.
|
||||
*
|
||||
* @dname removeAt
|
||||
* @param index Zero-based list position.
|
||||
* @return Ok on success, or StationListError / StoreError.
|
||||
* @pubstate mutates list_ and NVS on success.
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
[[nodiscard]] std::expected<void, core::StationListError> removeAt(
|
||||
std::size_t index);
|
||||
|
||||
/**
|
||||
* @brief tuneToIndex — recall a saved preset on the tuner.
|
||||
*
|
||||
* @dname tuneToIndex
|
||||
* @param index Zero-based list position.
|
||||
* @return Ok on success, or StationListError / TunerError.
|
||||
* @pubstate delegates tune/play to tuner_.
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
[[nodiscard]] std::expected<void, core::TunerError> tuneToIndex(
|
||||
std::size_t index);
|
||||
|
||||
private:
|
||||
[[nodiscard]] std::expected<void, core::StoreError> persist();
|
||||
|
||||
core::ISecureStore& store_;
|
||||
tuner::TunerService& tuner_;
|
||||
core::StationList list_;
|
||||
};
|
||||
|
||||
} // namespace station
|
||||
@@ -0,0 +1,109 @@
|
||||
/**
|
||||
* @file StationService.cpp
|
||||
* @brief StationService implementation.
|
||||
*
|
||||
* DigiRadio firmware — https://github.com/manvalan/DigiRadio
|
||||
*
|
||||
* Copyright 2026 Michele Bigi
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
|
||||
#include "station/StationService.hpp"
|
||||
|
||||
#include "core/StationListJson.hpp"
|
||||
#include "core/TunerBand.hpp"
|
||||
|
||||
namespace station {
|
||||
|
||||
StationService::StationService(core::ISecureStore& store,
|
||||
tuner::TunerService& tuner)
|
||||
: store_(store)
|
||||
, tuner_(tuner)
|
||||
{
|
||||
}
|
||||
|
||||
std::expected<void, core::StoreError> StationService::loadFromStore()
|
||||
{
|
||||
if (!store_.hasStationList()) {
|
||||
list_ = core::StationList{};
|
||||
return {};
|
||||
}
|
||||
|
||||
auto blob = store_.loadStationListJson();
|
||||
if (!blob) {
|
||||
return std::unexpected(blob.error());
|
||||
}
|
||||
|
||||
auto parsed = core::parseStationListJson(*blob);
|
||||
if (!parsed) {
|
||||
return std::unexpected(core::StoreError::InvalidData);
|
||||
}
|
||||
list_ = std::move(*parsed);
|
||||
return {};
|
||||
}
|
||||
|
||||
const core::StationList& StationService::list() const noexcept
|
||||
{
|
||||
return list_;
|
||||
}
|
||||
|
||||
std::expected<void, core::StoreError> StationService::persist()
|
||||
{
|
||||
return store_.saveStationListJson(core::serializeStationListJson(list_));
|
||||
}
|
||||
|
||||
std::expected<void, core::StationListError> StationService::add(
|
||||
core::Station station)
|
||||
{
|
||||
if (auto added = list_.add(std::move(station)); !added) {
|
||||
return added;
|
||||
}
|
||||
if (auto saved = persist(); !saved) {
|
||||
list_.removeAt(list_.stations().size() - 1U);
|
||||
return std::unexpected(core::StationListError::Full);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
std::expected<void, core::StationListError> StationService::removeAt(
|
||||
std::size_t index)
|
||||
{
|
||||
if (auto removed = list_.removeAt(index); !removed) {
|
||||
return removed;
|
||||
}
|
||||
if (auto saved = persist(); !saved) {
|
||||
return std::unexpected(core::StationListError::NotFound);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
std::expected<void, core::TunerError> StationService::tuneToIndex(
|
||||
std::size_t index)
|
||||
{
|
||||
if (index >= list_.stations().size()) {
|
||||
return std::unexpected(core::TunerError::NotBooted);
|
||||
}
|
||||
|
||||
const core::Station& station = list_.stations()[index];
|
||||
if (station.band() == core::TunerBand::Fm) {
|
||||
if (!station.fmFrequency()) {
|
||||
return std::unexpected(core::TunerError::WrongBand);
|
||||
}
|
||||
return tuner_.tuneFm(*station.fmFrequency());
|
||||
}
|
||||
|
||||
if (auto tuned = tuner_.tuneDab(station.dabFreqIndex()); !tuned) {
|
||||
return tuned;
|
||||
}
|
||||
|
||||
if (station.dabServiceId() && station.dabComponentId()) {
|
||||
return tuner_.playDabService(*station.dabServiceId(),
|
||||
*station.dabComponentId());
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
} // namespace station
|
||||
Reference in New Issue
Block a user