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/BluetoothService.cpp"
|
||||
INCLUDE_DIRS "include"
|
||||
REQUIRES core bt1035
|
||||
)
|
||||
|
||||
target_compile_features(${COMPONENT_LIB} PUBLIC cxx_std_23)
|
||||
@@ -0,0 +1,102 @@
|
||||
/**
|
||||
* @file BluetoothService.hpp
|
||||
* @brief Intent-level Bluetooth API for HTTP and UI.
|
||||
*
|
||||
* 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 "bt1035/Bt1035Driver.hpp"
|
||||
#include "bt1035/Bt1035Error.hpp"
|
||||
#include "core/BluetoothJson.hpp"
|
||||
|
||||
#include <expected>
|
||||
|
||||
namespace bluetooth {
|
||||
|
||||
/**
|
||||
* @brief BluetoothService — pairing and A2DP status for the web API.
|
||||
*
|
||||
* @dname BluetoothService
|
||||
* @return n/a (type)
|
||||
* @pubstate Borrows bt1035::Bt1035Driver for the process lifetime. Tracks
|
||||
* whether discoverable mode was requested via startPairing().
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
class BluetoothService {
|
||||
public:
|
||||
/**
|
||||
* @brief BluetoothService — bind to the BT1035 driver.
|
||||
*
|
||||
* @dname BluetoothService
|
||||
* @param driver Booted BT1035 driver (must outlive this service).
|
||||
* @pubstate stores driver reference; pairing inactive initially.
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
explicit BluetoothService(bt1035::Bt1035Driver& driver);
|
||||
|
||||
/**
|
||||
* @brief refreshStatus — read module boot, pairing, and A2DP state.
|
||||
*
|
||||
* @dname refreshStatus
|
||||
* @return BluetoothStatus on success, or Bt1035Error from the driver.
|
||||
* @pubstate queries driver for A2DP state when booted.
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
[[nodiscard]] std::expected<core::BluetoothStatus, bt1035::Bt1035Error>
|
||||
refreshStatus();
|
||||
|
||||
/**
|
||||
* @brief startPairing — enter discoverable mode (AT+PAIR=1).
|
||||
*
|
||||
* @dname startPairing
|
||||
* @return Ok on success, or Bt1035Error.
|
||||
* @pubstate sets pairingActive_ on success.
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
[[nodiscard]] std::expected<void, bt1035::Bt1035Error> startPairing();
|
||||
|
||||
/**
|
||||
* @brief stopPairing — leave discoverable mode (AT+PAIR=0).
|
||||
*
|
||||
* @dname stopPairing
|
||||
* @return Ok on success, or Bt1035Error.
|
||||
* @pubstate clears pairingActive_ on success.
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
[[nodiscard]] std::expected<void, bt1035::Bt1035Error> stopPairing();
|
||||
|
||||
/**
|
||||
* @brief disconnect — release the active A2DP link (AT+A2DPDISC).
|
||||
*
|
||||
* @dname disconnect
|
||||
* @return Ok on success, or Bt1035Error.
|
||||
* @pubstate delegates to driver.
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-06
|
||||
*/
|
||||
[[nodiscard]] std::expected<void, bt1035::Bt1035Error> disconnect();
|
||||
|
||||
private:
|
||||
bt1035::Bt1035Driver& driver_;
|
||||
bool pairingActive_;
|
||||
};
|
||||
|
||||
} // namespace bluetooth
|
||||
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* @file BluetoothService.cpp
|
||||
* @brief BluetoothService 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 "bluetooth/BluetoothService.hpp"
|
||||
|
||||
namespace bluetooth {
|
||||
|
||||
BluetoothService::BluetoothService(bt1035::Bt1035Driver& driver)
|
||||
: driver_(driver)
|
||||
, pairingActive_(false)
|
||||
{
|
||||
}
|
||||
|
||||
std::expected<core::BluetoothStatus, bt1035::Bt1035Error>
|
||||
BluetoothService::refreshStatus()
|
||||
{
|
||||
core::BluetoothStatus status{
|
||||
.booted = driver_.isBooted(),
|
||||
.pairing = pairingActive_,
|
||||
.a2dpState = core::Bt1035A2dpState::Standby,
|
||||
};
|
||||
|
||||
if (!status.booted) {
|
||||
return status;
|
||||
}
|
||||
|
||||
auto a2dp = driver_.queryA2dpState();
|
||||
if (!a2dp) {
|
||||
return std::unexpected(a2dp.error());
|
||||
}
|
||||
status.a2dpState = *a2dp;
|
||||
return status;
|
||||
}
|
||||
|
||||
std::expected<void, bt1035::Bt1035Error> BluetoothService::startPairing()
|
||||
{
|
||||
if (auto result = driver_.enterPairingMode(); !result) {
|
||||
return result;
|
||||
}
|
||||
pairingActive_ = true;
|
||||
return {};
|
||||
}
|
||||
|
||||
std::expected<void, bt1035::Bt1035Error> BluetoothService::stopPairing()
|
||||
{
|
||||
if (auto result = driver_.leavePairingMode(); !result) {
|
||||
return result;
|
||||
}
|
||||
pairingActive_ = false;
|
||||
return {};
|
||||
}
|
||||
|
||||
std::expected<void, bt1035::Bt1035Error> BluetoothService::disconnect()
|
||||
{
|
||||
return driver_.disconnectA2dp();
|
||||
}
|
||||
|
||||
} // namespace bluetooth
|
||||
@@ -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