Add Slice 4 tuner stack with AGENTS-compliant core types and HTTP API.

Introduces Si4684/ADAU1701 drivers, TunerService, FrequencyKHz,
SeekDirection, /api/tuner routes without file-scope globals, host tests,
and firmware blob tooling (binaries remain gitignored).

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-06 16:17:56 +02:00
co-authored by Cursor
parent c83c9bd765
commit 81d404a1df
68 changed files with 15411 additions and 139 deletions
@@ -0,0 +1,8 @@
idf_component_register(
SRCS
"src/TunerService.cpp"
INCLUDE_DIRS "include"
REQUIRES core
)
target_compile_features(${COMPONENT_LIB} PUBLIC cxx_std_23)
@@ -0,0 +1,173 @@
/**
* @file TunerService.hpp
* @brief Application service orchestrating tuner operations for UI/API.
*
* 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/ITuner.hpp"
#include "core/SeekDirection.hpp"
#include "core/TunerError.hpp"
#include "core/TunerStatus.hpp"
#include <cstdint>
#include <expected>
#include <vector>
namespace tuner {
/**
* @brief TunerService — intent-level tuner API for HTTP and future UI.
*
* @dname TunerService
* @return n/a (type)
* @pubstate Borrows core::ITuner for the process lifetime. Tracks last tune
* target and cached volume for status reporting. No public data
* members.
*
* Delegates hardware to core::ITuner; maps driver failures to TunerError
* without exposing SPI details to the shell.
*
* @author Michele Bigi
* @date 2026-07-06
*/
class TunerService {
public:
/**
* @brief TunerService — bind to a tuner driver for the process lifetime.
*
* @dname TunerService
* @param tuner Driver implementation (must outlive this service).
* @pubstate stores tuner reference; initialises last tune defaults.
*
* @author Michele Bigi
* @date 2026-07-06
*/
explicit TunerService(core::ITuner& tuner);
/**
* @brief refreshStatus — read a fresh tuner snapshot from the driver.
*
* @dname refreshStatus
* @return TunerStatus on success, or a TunerError from ITuner.
* @pubstate reads tuner_; updates cached volume_ on success.
*
* @author Michele Bigi
* @date 2026-07-06
*/
[[nodiscard]] std::expected<core::TunerStatus, core::TunerError>
refreshStatus();
/**
* @brief tuneDab — tune to a Band III ensemble index.
*
* @dname tuneDab
* @param freqIndex Ensemble index 037.
* @return Ok on success, or a TunerError from ITuner.
* @pubstate writes lastDabIndex_ on success.
*
* @author Michele Bigi
* @date 2026-07-06
*/
[[nodiscard]] std::expected<void, core::TunerError> tuneDab(
std::uint8_t freqIndex);
/**
* @brief tuneFm — tune to an FM centre frequency.
*
* @dname tuneFm
* @param frequency Validated FM centre frequency.
* @return Ok on success, or a TunerError from ITuner.
* @pubstate writes lastFmFrequency_ on success.
*
* @author Michele Bigi
* @date 2026-07-06
*/
[[nodiscard]] std::expected<void, core::TunerError> tuneFm(
core::FrequencyKHz frequency);
/**
* @brief seekFm — seek FM in the given direction.
*
* @dname seekFm
* @param direction Up or Down scan direction.
* @return New centre frequency, or a TunerError.
* @pubstate writes lastFmFrequency_ on success.
*
* @author Michele Bigi
* @date 2026-07-06
*/
[[nodiscard]] std::expected<core::FrequencyKHz, core::TunerError> seekFm(
core::SeekDirection direction);
/**
* @brief listDabServices — programmes available on the current ensemble.
*
* @dname listDabServices
* @return Service entries, or a TunerError from ITuner.
* @pubstate reads tuner_.
*
* @author Michele Bigi
* @date 2026-07-06
*/
[[nodiscard]] std::expected<std::vector<core::TunerServiceEntry>,
core::TunerError>
listDabServices();
/**
* @brief playDabService — start playback of a DAB programme.
*
* @dname playDabService
* @param serviceId Selected service identifier.
* @param componentId Audio component within the service.
* @return Ok on success, or a TunerError from ITuner.
* @pubstate delegates to tuner_.
*
* @author Michele Bigi
* @date 2026-07-06
*/
[[nodiscard]] std::expected<void, core::TunerError> playDabService(
std::uint32_t serviceId, std::uint32_t componentId);
/**
* @brief setVolume — set tuner output attenuation.
*
* @dname setVolume
* @param level Attenuator 063.
* @return Ok on success, or a TunerError from ITuner.
* @pubstate writes volume_ on success.
*
* @author Michele Bigi
* @date 2026-07-06
*/
[[nodiscard]] std::expected<void, core::TunerError> setVolume(
std::uint8_t level);
/**
* @brief tuner — borrow the underlying driver for diagnostics.
*
* @dname tuner
* @return Reference to the injected ITuner implementation.
* @pubstate reads tuner_.
*
* @author Michele Bigi
* @date 2026-07-06
*/
[[nodiscard]] core::ITuner& tuner() noexcept;
private:
core::ITuner& tuner_;
std::uint8_t lastDabIndex_;
core::FrequencyKHz lastFmFrequency_;
std::uint8_t volume_;
};
} // namespace tuner
@@ -0,0 +1,101 @@
/**
* @file TunerService.cpp
* @brief TunerService 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 "tuner/TunerService.hpp"
namespace tuner {
namespace {
[[nodiscard]] core::FrequencyKHz defaultFmFrequency()
{
return *core::FrequencyKHz::tryFromKhz(101500U);
}
} // namespace
TunerService::TunerService(core::ITuner& tuner)
: tuner_(tuner)
, lastDabIndex_(0U)
, lastFmFrequency_(defaultFmFrequency())
, volume_(40U)
{
}
std::expected<core::TunerStatus, core::TunerError> TunerService::refreshStatus()
{
auto status = tuner_.readStatus();
if (status) {
volume_ = status->volume;
}
return status;
}
std::expected<void, core::TunerError> TunerService::tuneDab(
std::uint8_t freqIndex)
{
if (auto result = tuner_.tuneDab(freqIndex); !result) {
return result;
}
lastDabIndex_ = freqIndex;
return {};
}
std::expected<void, core::TunerError> TunerService::tuneFm(
core::FrequencyKHz frequency)
{
if (auto result = tuner_.tuneFm(frequency); !result) {
return result;
}
lastFmFrequency_ = frequency;
return {};
}
std::expected<core::FrequencyKHz, core::TunerError> TunerService::seekFm(
core::SeekDirection direction)
{
auto result = tuner_.seekFm(direction);
if (result) {
lastFmFrequency_ = *result;
}
return result;
}
std::expected<std::vector<core::TunerServiceEntry>, core::TunerError>
TunerService::listDabServices()
{
return tuner_.listDabServices();
}
std::expected<void, core::TunerError> TunerService::playDabService(
std::uint32_t serviceId,
std::uint32_t componentId)
{
return tuner_.playDabService(serviceId, componentId);
}
std::expected<void, core::TunerError> TunerService::setVolume(std::uint8_t level)
{
if (auto result = tuner_.setVolume(level); !result) {
return result;
}
volume_ = level & 0x3FU;
return {};
}
core::ITuner& TunerService::tuner() noexcept
{
return tuner_;
}
} // namespace tuner