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
@@ -1,6 +1,14 @@
set(ADAU_FW_DIR "${CMAKE_CURRENT_LIST_DIR}/../../../Firmware/ADAU1701-Firmware")
idf_component_register(
SRCS "src/component_stub.cpp"
INCLUDE_DIRS "include"
SRCS
"src/Adau1701Driver.cpp"
"src/SigmaStudioFW.c"
"src/adau1701_program.c"
INCLUDE_DIRS
"include"
"${ADAU_FW_DIR}"
REQUIRES driver esp_driver_gpio esp_driver_i2c
)
target_compile_features(${COMPONENT_LIB} PUBLIC cxx_std_23)
@@ -0,0 +1,112 @@
/**
* @file Adau1701Driver.hpp
* @brief ADAU1701 SigmaDSP driver — RAM boot on every power-up.
*
* 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 "adau1701/Adau1701Error.hpp"
#include <expected>
namespace adau1701 {
/**
* @brief Adau1701Pins — board GPIO/I2C identifiers for the DSP.
*
* @dname Adau1701Pins
* @return n/a (type)
* @pubstate Immutable wiring snapshot from board_pins.hpp.
*
* @author Michele Bigi
* @date 2026-07-06
*/
struct Adau1701Pins {
int i2cSda; ///< I2C SDA GPIO.
int i2cScl; ///< I2C SCL GPIO.
int resetGpio; ///< DSP RESET# GPIO (active low).
int i2cAddr7; ///< 7-bit I2C address of the ADAU1701.
};
/**
* @brief Adau1701Driver — owns I2C + reset and loads SigmaStudio RAM.
*
* @dname Adau1701Driver
* @param pins Board wiring for I2C and RESET#.
* @return n/a (type)
* @pubstate Owns I2C bus/device handles (RAII). booted_ true after a
* successful default_download replay.
*
* Writes the SigmaStudio export from Firmware/ADAU1701-Firmware on every
* boot (no EEPROM self-boot on DigiRadio).
*
* @author Michele Bigi
* @date 2026-07-06
*/
class Adau1701Driver {
public:
/**
* @brief Adau1701Driver — construct with board pin map.
*
* @dname Adau1701Driver
* @param pins SDA/SCL/reset/address configuration.
* @pubstate stores pins_; not booted until boot().
*
* @author Michele Bigi
* @date 2026-07-06
*/
explicit Adau1701Driver(Adau1701Pins pins);
/**
* @brief ~Adau1701Driver — release I2C resources.
*
* @dname ~Adau1701Driver
* @pubstate deletes bus/device handles when created.
*
* @author Michele Bigi
* @date 2026-07-06
*/
~Adau1701Driver();
Adau1701Driver(const Adau1701Driver&) = delete;
Adau1701Driver& operator=(const Adau1701Driver&) = delete;
/**
* @brief boot — reset the DSP and replay the SigmaStudio download.
*
* @dname boot
* @return Ok on success, or Adau1701Error.
* @pubstate writes booted_ on success; uses embedded program data.
*
* @author Michele Bigi
* @date 2026-07-06
*/
[[nodiscard]] std::expected<void, Adau1701Error> boot();
/**
* @brief isBooted — query whether RAM download succeeded.
*
* @dname isBooted
* @return true after a successful boot().
* @pubstate reads booted_.
*
* @author Michele Bigi
* @date 2026-07-06
*/
[[nodiscard]] bool isBooted() const noexcept;
private:
Adau1701Pins pins_;
bool booted_;
void* i2cBus_;
void* i2cDev_;
};
} // namespace adau1701
@@ -0,0 +1,33 @@
/**
* @file Adau1701Error.hpp
* @brief Typed errors for ADAU1701 driver 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 adau1701 {
/**
* @brief Adau1701Error — failure causes for ADAU1701 bring-up.
*
* @dname Adau1701Error
* @return n/a (type)
* @pubstate n/a
*
* @author Michele Bigi
* @date 2026-07-06
*/
enum class Adau1701Error {
I2cInitFailed,
ResetFailed,
DownloadFailed,
};
} // namespace adau1701
@@ -0,0 +1,118 @@
/**
* @file Adau1701Driver.cpp
* @brief Adau1701Driver 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 "adau1701/Adau1701Driver.hpp"
#include "SigmaStudioFW.h"
#include "driver/gpio.h"
#include "driver/i2c_master.h"
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
extern "C" {
/** @brief SigmaStudio default program download (generated C, not part of the C++ API). */
void adau1701_run_default_download(void);
} // extern "C"
namespace adau1701 {
namespace {
constexpr char kTag[] = "Adau1701";
constexpr int kI2cPort = 0;
} // namespace
Adau1701Driver::Adau1701Driver(Adau1701Pins pins)
: pins_(pins)
, booted_(false)
, i2cBus_(nullptr)
, i2cDev_(nullptr)
{
}
Adau1701Driver::~Adau1701Driver()
{
auto* dev = static_cast<i2c_master_dev_handle_t>(i2cDev_);
auto* bus = static_cast<i2c_master_bus_handle_t>(i2cBus_);
if (dev != nullptr) {
i2c_master_bus_rm_device(dev);
}
if (bus != nullptr) {
i2c_del_master_bus(bus);
}
}
std::expected<void, Adau1701Error> Adau1701Driver::boot()
{
if (booted_) {
return {};
}
gpio_config_t resetCfg = {};
resetCfg.pin_bit_mask = 1ULL << pins_.resetGpio;
resetCfg.mode = GPIO_MODE_OUTPUT;
if (gpio_config(&resetCfg) != ESP_OK) {
return std::unexpected(Adau1701Error::ResetFailed);
}
gpio_set_level(static_cast<gpio_num_t>(pins_.resetGpio), 0);
vTaskDelay(pdMS_TO_TICKS(10));
gpio_set_level(static_cast<gpio_num_t>(pins_.resetGpio), 1);
vTaskDelay(pdMS_TO_TICKS(10));
i2c_master_bus_config_t busCfg = {};
busCfg.i2c_port = static_cast<i2c_port_num_t>(kI2cPort);
busCfg.sda_io_num = static_cast<gpio_num_t>(pins_.i2cSda);
busCfg.scl_io_num = static_cast<gpio_num_t>(pins_.i2cScl);
busCfg.clk_source = I2C_CLK_SRC_DEFAULT;
busCfg.glitch_ignore_cnt = 7;
busCfg.flags.enable_internal_pullup = true;
i2c_master_bus_handle_t bus = nullptr;
if (i2c_new_master_bus(&busCfg, &bus) != ESP_OK) {
ESP_LOGE(kTag, "i2c_new_master_bus failed");
return std::unexpected(Adau1701Error::I2cInitFailed);
}
i2cBus_ = bus;
i2c_device_config_t devCfg = {};
devCfg.dev_addr_length = I2C_ADDR_BIT_LEN_7;
devCfg.device_address = static_cast<uint16_t>(pins_.i2cAddr7);
devCfg.scl_speed_hz = 100000;
i2c_master_dev_handle_t dev = nullptr;
if (i2c_master_bus_add_device(bus, &devCfg, &dev) != ESP_OK) {
ESP_LOGE(kTag, "i2c_master_bus_add_device failed");
return std::unexpected(Adau1701Error::I2cInitFailed);
}
i2cDev_ = dev;
sigma_studio_bind_i2c(kI2cPort, static_cast<unsigned char>(pins_.i2cAddr7));
sigma_studio_set_device(dev);
adau1701_run_default_download();
booted_ = true;
ESP_LOGI(kTag, "SigmaStudio program loaded");
return {};
}
bool Adau1701Driver::isBooted() const noexcept
{
return booted_;
}
} // namespace adau1701
@@ -0,0 +1,60 @@
/**
* @file SigmaStudioFW.c
* @brief SigmaStudio SIGMA_WRITE_REGISTER_BLOCK for ESP-IDF I2C.
*
* 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 "SigmaStudioFW.h"
#include "driver/i2c_master.h"
#include <string.h>
static i2c_master_dev_handle_t s_dev = NULL;
void sigma_studio_bind_i2c(int port, unsigned char addr7)
{
(void)port;
(void)addr7;
}
void sigma_studio_set_device(void* i2cDevHandle)
{
s_dev = (i2c_master_dev_handle_t)i2cDevHandle;
}
void SIGMA_WRITE_REGISTER_BLOCK(unsigned char devAddress,
unsigned int address,
unsigned char length,
ADI_REG_TYPE* pData)
{
(void)devAddress;
if (s_dev == NULL || pData == NULL || length == 0U) {
return;
}
enum { kChunk = 64U };
unsigned int addr = address;
unsigned char remaining = length;
ADI_REG_TYPE* cursor = pData;
while (remaining > 0U) {
const unsigned char chunk =
remaining > kChunk ? kChunk : remaining;
unsigned char buf[2U + 64U];
buf[0] = (unsigned char)((addr >> 8) & 0xFFU);
buf[1] = (unsigned char)(addr & 0xFFU);
memcpy(buf + 2U, cursor, chunk);
i2c_master_transmit(s_dev, buf, (size_t)(2U + chunk), 1000);
addr += chunk;
cursor += chunk;
remaining = (unsigned char)(remaining - chunk);
}
}
@@ -0,0 +1,20 @@
/**
* @file adau1701_program.c
* @brief SigmaStudio default download for DigiRadio ADAU1701.
*
* 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 "SigmaStudioFW.h"
#include "DigiRadio_IC_1.h"
void adau1701_run_default_download(void)
{
default_download_IC_1();
}
@@ -1,33 +0,0 @@
/**
* @file component_stub.cpp
* @brief ADAU1701 driver component placeholder (Slice 5).
*
* DigiRadio firmware — https://github.com/manvalan/DigiRadio
*
* Copyright 2026 Michele Bigi
* 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
* @date 2026-07-06
*/
namespace adau1701::detail {
/**
* @brief adau1701ComponentLinked — ensures the driver component links.
*
* @dname adau1701ComponentLinked
* @return n/a (type)
* @pubstate n/a
*
* @author Michele Bigi
* @date 2026-07-06
*/
void adau1701ComponentLinked() noexcept {}
} // namespace adau1701::detail
@@ -1,6 +1,23 @@
set(SI4684_FW_DIR "${CMAKE_CURRENT_LIST_DIR}/../../../Firmware/Si4684-Firmware")
if(NOT EXISTS "${SI4684_FW_DIR}/fm_firmware.bin")
message(FATAL_ERROR
"Missing ${SI4684_FW_DIR}/fm_firmware.bin — run:\n"
" python3 tools/fetch_si4684_firmware.py --from-ugreen-radio-cli <Files_v16.zip>\n"
" python3 tools/fetch_si4684_firmware.py --si46xx-dir <si46xx_firmware>")
endif()
idf_component_register(
SRCS "src/component_stub.cpp"
SRCS
"src/Si4684Driver.cpp"
"src/Si4684EmbeddedImages.cpp"
"src/Si4684Tuner.cpp"
INCLUDE_DIRS "include"
REQUIRES core driver esp_driver_gpio esp_driver_spi
EMBED_FILES
"${SI4684_FW_DIR}/rom_patch_016.bin"
"${SI4684_FW_DIR}/dab_firmware.bin"
"${SI4684_FW_DIR}/fm_firmware.bin"
)
target_compile_features(${COMPONENT_LIB} PUBLIC cxx_std_23)
@@ -0,0 +1,32 @@
/**
* @file Si4684Band.hpp
* @brief Tuner band selection for Si4684 firmware images.
*
* 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 si4684 {
/**
* @brief Si4684Band — application image loaded after ROM patch.
*
* @dname Si4684Band
* @return n/a (type)
* @pubstate n/a
*
* @author Michele Bigi
* @date 2026-07-06
*/
enum class Si4684Band {
Dab,
Fm,
};
} // namespace si4684
@@ -0,0 +1,370 @@
/**
* @file Si4684Driver.hpp
* @brief Si4684 DAB+/FM tuner — boot, tuning, status, and service control.
*
* 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/IFirmwareBlobReader.hpp"
#include "core/SeekDirection.hpp"
#include "si4684/Si4684Band.hpp"
#include "si4684/Si4684Error.hpp"
#include "si4684/Si4684Types.hpp"
#include <cstdint>
#include <expected>
#include <span>
#include <vector>
namespace si4684 {
/**
* @brief Si4684Pins — board GPIO/SPI identifiers for the tuner.
*
* @dname Si4684Pins
* @return n/a (type)
* @pubstate Immutable wiring snapshot from board_pins.hpp.
*
* @author Michele Bigi
* @date 2026-07-06
*/
struct Si4684Pins {
int spiHost; ///< ESP32 SPI host peripheral index.
int csGpio; ///< Chip-select GPIO.
int misoGpio; ///< MISO GPIO.
int mosiGpio; ///< MOSI GPIO.
int sclkGpio; ///< SCLK GPIO.
int rstbGpio; ///< RESET# GPIO (active low).
int intbGpio; ///< INTB GPIO (optional status interrupt).
};
/**
* @brief Si4684Driver — full Si4684 control over SPI (AN649 / PE5PVB).
*
* @dname Si4684Driver
* @return n/a (type)
* @pubstate Owns the SPI link and boot state. Register-level command bytes
* stay private; callers use intent-level methods only.
*
* Implements the documented boot sequence plus band-specific tuning, property
* access, RSQ/DIGRAD reads, and DAB service selection.
*
* @author Michele Bigi
* @date 2026-07-06
*/
class Si4684Driver {
public:
/**
* @brief Si4684Driver — construct with board pins and firmware blobs.
*
* @dname Si4684Driver
* @param pins Board SPI and GPIO wiring.
* @param patch ROM patch blob reader for HOST_LOAD.
* @param dabImage DAB application image reader.
* @param fmImage FM application image reader.
* @pubstate stores pin map and blob references; not booted until boot().
*
* @author Michele Bigi
* @date 2026-07-06
*/
Si4684Driver(Si4684Pins pins,
const core::IFirmwareBlobReader& patch,
const core::IFirmwareBlobReader& dabImage,
const core::IFirmwareBlobReader& fmImage);
/**
* @brief ~Si4684Driver — release SPI resources.
*
* @dname ~Si4684Driver
* @pubstate removes SPI device and bus when active.
*
* @author Michele Bigi
* @date 2026-07-06
*/
~Si4684Driver();
Si4684Driver(const Si4684Driver&) = delete;
Si4684Driver& operator=(const Si4684Driver&) = delete;
/**
* @brief boot — cold-start: load patch, image, and configure I/O.
*
* @dname boot
* @param band DAB or FM application to load.
* @return Ok on success, or Si4684Error.
* @pubstate writes booted_ and loadedBand_ on success.
*
* @author Michele Bigi
* @date 2026-07-06
*/
[[nodiscard]] std::expected<void, Si4684Error> boot(Si4684Band band);
/**
* @brief isBooted — query whether boot completed successfully.
*
* @dname isBooted
* @return true after a successful boot().
* @pubstate reads booted_.
*
* @author Michele Bigi
* @date 2026-07-06
*/
[[nodiscard]] bool isBooted() const noexcept;
/**
* @brief loadedBand — read the active application band.
*
* @dname loadedBand
* @return Si4684Band loaded by the last successful boot().
* @pubstate reads loadedBand_.
*
* @author Michele Bigi
* @date 2026-07-06
*/
[[nodiscard]] Si4684Band loadedBand() const noexcept;
/**
* @brief getPartInfo — read chip identity and firmware revision.
*
* @dname getPartInfo
* @return Si4684PartInfo on success, or Si4684Error.
* @pubstate reads chip via GET_PART_INFO (AN649).
*
* @author Michele Bigi
* @date 2026-07-06
*/
[[nodiscard]] std::expected<Si4684PartInfo, Si4684Error> getPartInfo();
/**
* @brief getSysState — read the running application state.
*
* @dname getSysState
* @return Si4684SysState on success, or Si4684Error.
* @pubstate reads chip via GET_SYS_STATE.
*
* @author Michele Bigi
* @date 2026-07-06
*/
[[nodiscard]] std::expected<Si4684SysState, Si4684Error> getSysState();
/**
* @brief setProperty — write a Skyworks property (SET_PROPERTY 0x13).
*
* @dname setProperty
* @param propertyId 16-bit property address.
* @param value 16-bit property value.
* @return Ok on success, or Si4684Error.
* @pubstate writes property via SPI command.
*
* @author Michele Bigi
* @date 2026-07-06
*/
[[nodiscard]] std::expected<void, Si4684Error> setProperty(
std::uint16_t propertyId, std::uint16_t value);
/**
* @brief setVolume — set audio attenuator 063 (property 0x0300).
*
* @dname setVolume
* @param level Attenuator level 063.
* @return Ok on success, or Si4684Error.
* @pubstate writes AUDIO_VOLUME property.
*
* @author Michele Bigi
* @date 2026-07-06
*/
[[nodiscard]] std::expected<void, Si4684Error> setVolume(std::uint8_t level);
/**
* @brief tuneFm — tune FM to a validated centre frequency.
*
* @dname tuneFm
* @param frequency FM centre frequency in kHz.
* @return Ok on success, or Si4684Error::WrongBand / TuneFailed.
* @pubstate sends FM_TUNE_FREQ and waits for STC (AN649).
*
* @author Michele Bigi
* @date 2026-07-06
*/
[[nodiscard]] std::expected<void, Si4684Error> tuneFm(
core::FrequencyKHz frequency);
/**
* @brief seekFm — seek FM in the given direction.
*
* @dname seekFm
* @param direction Up or Down scan direction.
* @param wrap Band wrap behaviour for FM_SEEK_START.
* @return New centre frequency on success, or Si4684Error.
* @pubstate sends FM_SEEK_START and waits for STC.
*
* @author Michele Bigi
* @date 2026-07-06
*/
[[nodiscard]] std::expected<core::FrequencyKHz, Si4684Error> seekFm(
core::SeekDirection direction, SeekBandWrap wrap);
/**
* @brief readFmRsq — read FM signal quality metrics.
*
* @dname readFmRsq
* @return Si4684FmRsq on success, or Si4684Error.
* @pubstate reads FM_RSQ_STATUS response bytes.
*
* @author Michele Bigi
* @date 2026-07-06
*/
[[nodiscard]] std::expected<Si4684FmRsq, Si4684Error> readFmRsq();
/**
* @brief readFmRds — read a raw RDS group snapshot.
*
* @dname readFmRds
* @return Si4684FmRdsStatus on success, or Si4684Error.
* @pubstate reads FM_RDS_STATUS response bytes.
*
* @author Michele Bigi
* @date 2026-07-06
*/
[[nodiscard]] std::expected<Si4684FmRdsStatus, Si4684Error> readFmRds();
/**
* @brief installDefaultDabFrequencyPlan — load Band III frequency list.
*
* @dname installDefaultDabFrequencyPlan
* @return Ok on success, or Si4684Error.
* @pubstate sends DAB_SET_FREQ_LIST with kDefaultDabFrequencyKhz.
*
* @author Michele Bigi
* @date 2026-07-06
*/
[[nodiscard]] std::expected<void, Si4684Error> installDefaultDabFrequencyPlan();
/**
* @brief tuneDab — tune to a Band III ensemble index.
*
* @dname tuneDab
* @param freqIndex Ensemble index 037.
* @return Ok on success, or Si4684Error.
* @pubstate sends DAB_TUNE_FREQ and waits for STC.
*
* @author Michele Bigi
* @date 2026-07-06
*/
[[nodiscard]] std::expected<void, Si4684Error> tuneDab(std::uint8_t freqIndex);
/**
* @brief readDabDigRadStatus — read ensemble lock metrics.
*
* @dname readDabDigRadStatus
* @return Si4684DabDigRadStatus on success, or Si4684Error.
* @pubstate reads DAB_DIGRAD_STATUS response bytes.
*
* @author Michele Bigi
* @date 2026-07-06
*/
[[nodiscard]] std::expected<Si4684DabDigRadStatus, Si4684Error>
readDabDigRadStatus();
/**
* @brief readDabEventStatus — read DAB event flags.
*
* @dname readDabEventStatus
* @return Si4684DabEventStatus on success, or Si4684Error.
* @pubstate reads DAB_GET_EVENT_STATUS response bytes.
*
* @author Michele Bigi
* @date 2026-07-06
*/
[[nodiscard]] std::expected<Si4684DabEventStatus, Si4684Error>
readDabEventStatus();
/**
* @brief fetchDabServiceList — retrieve programmes for the ensemble.
*
* @dname fetchDabServiceList
* @return Service rows on success, or Si4684Error.
* @pubstate reads GET_DIGITAL_SERVICE_LIST chunks from the chip.
*
* @author Michele Bigi
* @date 2026-07-06
*/
[[nodiscard]] std::expected<std::vector<Si4684DabService>, Si4684Error>
fetchDabServiceList();
/**
* @brief startDabService — start DAB audio for a programme.
*
* @dname startDabService
* @param serviceId Selected service identifier.
* @param componentId Audio component within the service.
* @param type Digital service type (audio by default).
* @return Ok on success, or Si4684Error.
* @pubstate sends START_DIGITAL_SERVICE.
*
* @author Michele Bigi
* @date 2026-07-06
*/
[[nodiscard]] std::expected<void, Si4684Error> startDabService(
std::uint32_t serviceId,
std::uint32_t componentId,
Si4684DigitalServiceType type = Si4684DigitalServiceType::Audio);
private:
enum class Command : std::uint8_t {
PowerUp = 0x01,
HostLoad = 0x04,
LoadInit = 0x06,
BootCmd = 0x07,
GetPartInfo = 0x08,
GetSysState = 0x09,
GetFuncInfo = 0x12,
SetProperty = 0x13,
FmTuneFreq = 0x30,
FmSeekStart = 0x31,
FmRsqStatus = 0x32,
FmRdsStatus = 0x34,
GetDigitalServiceList = 0x80,
StartDigitalService = 0x81,
GetDigitalServiceData = 0x84,
DabTuneFreq = 0xB0,
DabDigRadStatus = 0xB2,
DabGetEventStatus = 0xB3,
DabSetFreqList = 0xB8,
};
[[nodiscard]] std::expected<void, Si4684Error> ensureBooted() const;
[[nodiscard]] std::expected<void, Si4684Error> ensureBand(
Si4684Band band) const;
[[nodiscard]] std::expected<void, Si4684Error> waitCts();
[[nodiscard]] std::expected<void, Si4684Error> waitStc();
[[nodiscard]] std::expected<void, Si4684Error> sendCommand(
std::span<const std::uint8_t> bytes);
[[nodiscard]] std::expected<void, Si4684Error> readRaw(
std::span<std::uint8_t> buffer);
[[nodiscard]] std::expected<void, Si4684Error> writeCommand(
Command cmd, const std::uint8_t* payload, std::size_t length);
[[nodiscard]] std::expected<void, Si4684Error> hostLoadBlob(
const core::IFirmwareBlobReader& blob, std::size_t chunkPayload);
[[nodiscard]] std::expected<void, Si4684Error> configureAfterBoot(
Si4684Band band);
Si4684Pins pins_;
const core::IFirmwareBlobReader& patch_;
const core::IFirmwareBlobReader& dabImage_;
const core::IFirmwareBlobReader& fmImage_;
bool booted_;
Si4684Band loadedBand_;
bool spiBusActive_;
void* spiDevice_;
};
} // namespace si4684
@@ -0,0 +1,100 @@
/**
* @file Si4684EmbeddedImages.hpp
* @brief Embedded Si4684 ROM patch, DAB and FM firmware blob accessors.
*
* 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/EmbeddedBlobReader.hpp"
#include "si4684/Si4684Band.hpp"
namespace si4684 {
/**
* @brief Si4684EmbeddedImages — flash-backed Si4684 firmware blobs.
*
* @dname Si4684EmbeddedImages
* @return n/a (type)
* @pubstate Owns EmbeddedBlobReader views over EMBED_FILES symbols from
* Firmware/Si4684-Firmware/.
*
* @author Michele Bigi
* @date 2026-07-06
*/
class Si4684EmbeddedImages {
public:
/**
* @brief Si4684EmbeddedImages — bind linker-embedded binaries.
*
* @dname Si4684EmbeddedImages
* @pubstate constructs patch_, dab_, and fm_ readers from flash symbols.
*
* @author Michele Bigi
* @date 2026-07-06
*/
Si4684EmbeddedImages();
/**
* @brief romPatch — ROM patch blob for HOST_LOAD before main image.
*
* @dname romPatch
* @return Reader over rom_patch_016.bin.
* @pubstate reads patch_.
*
* @author Michele Bigi
* @date 2026-07-06
*/
[[nodiscard]] const core::IFirmwareBlobReader& romPatch() const noexcept;
/**
* @brief dabFirmware — DAB application image blob.
*
* @dname dabFirmware
* @return Reader over dab_firmware.bin.
* @pubstate reads dab_.
*
* @author Michele Bigi
* @date 2026-07-06
*/
[[nodiscard]] const core::IFirmwareBlobReader& dabFirmware() const noexcept;
/**
* @brief fmFirmware — FM application image blob.
*
* @dname fmFirmware
* @return Reader over fm_firmware.bin.
* @pubstate reads fm_.
*
* @author Michele Bigi
* @date 2026-07-06
*/
[[nodiscard]] const core::IFirmwareBlobReader& fmFirmware() const noexcept;
/**
* @brief applicationImage — map band to embedded application blob.
*
* @dname applicationImage
* @param band DAB or FM image selector.
* @return Reader for the selected application firmware.
* @pubstate reads dab_ or fm_.
*
* @author Michele Bigi
* @date 2026-07-06
*/
[[nodiscard]] const core::IFirmwareBlobReader& applicationImage(
Si4684Band band) const noexcept;
private:
core::EmbeddedBlobReader patch_;
core::EmbeddedBlobReader dab_;
core::EmbeddedBlobReader fm_;
};
} // namespace si4684
@@ -0,0 +1,44 @@
/**
* @file Si4684Error.hpp
* @brief Typed errors for Si4684 driver 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 si4684 {
/**
* @brief Si4684Error — failure causes for Si4684 bring-up and tuning.
*
* @dname Si4684Error
* @return n/a (type)
* @pubstate n/a
*
* @author Michele Bigi
* @date 2026-07-06
*/
enum class Si4684Error {
SpiInitFailed,
ResetFailed,
CtsTimeout,
PowerUpFailed,
PatchLoadFailed,
ImageLoadFailed,
BootFailed,
NotBooted,
WrongBand,
CommandFailed,
StcTimeout,
TuneFailed,
ReplyTooShort,
BufferTooSmall,
};
} // namespace si4684
@@ -0,0 +1,195 @@
/**
* @file Si4684Tuner.hpp
* @brief core::ITuner adapter over Si4684Driver.
*
* 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 "si4684/Si4684Driver.hpp"
namespace si4684 {
/**
* @brief Si4684Tuner — maps Si4684Driver to core::ITuner.
*
* @dname Si4684Tuner
* @param driver Borrowed Si4684Driver (must outlive this adapter).
* @return n/a (type)
* @pubstate Borrows driver_. Caches last DAB index, FM frequency, and volume
* for status reporting. No public data members.
*
* Translates domain calls into SPI commands and maps Si4684Error to
* core::TunerError at this boundary.
*
* @author Michele Bigi
* @date 2026-07-06
*/
class Si4684Tuner : public core::ITuner {
public:
/**
* @brief Si4684Tuner — bind to a booted or bootable driver instance.
*
* @dname Si4684Tuner
* @param driver Si4684 driver constructed by HardwareBootstrap.
* @pubstate stores driver reference; sets default tune targets.
*
* @author Michele Bigi
* @date 2026-07-06
*/
explicit Si4684Tuner(Si4684Driver& driver);
/**
* @brief boot — load the requested Si4684 application image.
*
* @dname boot
* @param band DAB or FM image to load.
* @return Ok on success, or a mapped TunerError.
* @pubstate delegates to driver_.boot().
*
* @author Michele Bigi
* @date 2026-07-06
*/
[[nodiscard]] std::expected<void, core::TunerError> boot(
core::TunerBand band) override;
/**
* @brief currentBand — read the loaded application band.
*
* @dname currentBand
* @return Active TunerBand, or TunerError::NotBooted.
* @pubstate reads driver_.loadedBand().
*
* @author Michele Bigi
* @date 2026-07-06
*/
[[nodiscard]] std::expected<core::TunerBand, core::TunerError> currentBand()
const override;
/**
* @brief readStatus — build a core TunerStatus from driver metrics.
*
* @dname readStatus
* @return TunerStatus on success, or a mapped TunerError.
* @pubstate reads driver_; refreshes cached tune targets from RSQ/DIGRAD.
*
* @author Michele Bigi
* @date 2026-07-06
*/
[[nodiscard]] std::expected<core::TunerStatus, core::TunerError> readStatus()
override;
/**
* @brief tuneDab — tune to a Band III ensemble index.
*
* @dname tuneDab
* @param freqIndex Ensemble index 037.
* @return Ok on success, or a mapped TunerError.
* @pubstate writes dabIndex_ on success.
*
* @author Michele Bigi
* @date 2026-07-06
*/
[[nodiscard]] std::expected<void, core::TunerError> tuneDab(
std::uint8_t freqIndex) override;
/**
* @brief tuneFm — tune to an FM centre frequency in kHz.
*
* @dname tuneFm
* @param frequency Validated FM centre frequency.
* @return Ok on success, or a mapped TunerError.
* @pubstate writes fmFrequency_ on success.
*
* @author Michele Bigi
* @date 2026-07-06
*/
[[nodiscard]] std::expected<void, core::TunerError> tuneFm(
core::FrequencyKHz frequency) override;
/**
* @brief seekFm — seek FM with band wrap.
*
* @dname seekFm
* @param direction Up or Down scan direction.
* @return New centre frequency, or a mapped TunerError.
* @pubstate writes fmFrequency_ on success.
*
* @author Michele Bigi
* @date 2026-07-06
*/
[[nodiscard]] std::expected<core::FrequencyKHz, core::TunerError> seekFm(
core::SeekDirection direction) override;
/**
* @brief listDabServices — fetch programmes for the current ensemble.
*
* @dname listDabServices
* @return Service entries, ServiceListEmpty, or a mapped TunerError.
* @pubstate reads driver_ service list when ready.
*
* @author Michele Bigi
* @date 2026-07-06
*/
[[nodiscard]] std::expected<std::vector<core::TunerServiceEntry>,
core::TunerError>
listDabServices() override;
/**
* @brief playDabService — start DAB audio output.
*
* @dname playDabService
* @param serviceId Selected service identifier.
* @param componentId Audio component within the service.
* @return Ok on success, or a mapped TunerError.
* @pubstate delegates to driver_.startDabService().
*
* @author Michele Bigi
* @date 2026-07-06
*/
[[nodiscard]] std::expected<void, core::TunerError> playDabService(
std::uint32_t serviceId, std::uint32_t componentId) override;
/**
* @brief setVolume — set Si4684 output attenuation.
*
* @dname setVolume
* @param level Attenuator 063.
* @return Ok on success, or a mapped TunerError.
* @pubstate writes volume_ on success.
*
* @author Michele Bigi
* @date 2026-07-06
*/
[[nodiscard]] std::expected<void, core::TunerError> setVolume(
std::uint8_t level) override;
private:
/**
* @brief mapError — translate Si4684Error to core::TunerError.
*
* @dname mapError
* @param error Driver-level failure cause.
* @return Equivalent TunerError for services and HTTP layer.
* @pubstate none
*
* @author Michele Bigi
* @date 2026-07-06
*/
[[nodiscard]] static core::TunerError mapError(Si4684Error error) noexcept;
Si4684Driver& driver_;
std::uint8_t dabIndex_;
core::FrequencyKHz fmFrequency_;
std::uint8_t volume_;
};
} // namespace si4684
@@ -0,0 +1,176 @@
/**
* @file Si4684Types.hpp
* @brief Domain types for Si4684 tuner operations (DAB/FM status, services).
*
* 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/SeekDirection.hpp"
#include <array>
#include <cstddef>
#include <cstdint>
namespace si4684 {
/**
* @brief Si4684DigitalServiceType — START_DIGITAL_SERVICE mode byte.
*
* @dname Si4684DigitalServiceType
* @return n/a (type)
* @pubstate n/a
*
* @author Michele Bigi
* @date 2026-07-06
*/
enum class Si4684DigitalServiceType : std::uint8_t {
Audio = 0x00,
Packet = 0x01,
};
/**
* @brief SeekBandWrap — FM seek band-wrap behaviour (FM_SEEK_START).
*
* @dname SeekBandWrap
* @return n/a (type)
* @pubstate n/a
*
* @author Michele Bigi
* @date 2026-07-06
*/
enum class SeekBandWrap { Wrap, NoWrap };
/**
* @brief Si4684PartInfo — chip identity from GET_PART_INFO (AN649).
*
* @dname Si4684PartInfo
* @return n/a (type)
* @pubstate Plain DTO from GET_PART_INFO response bytes.
*
* @author Michele Bigi
* @date 2026-07-06
*/
struct Si4684PartInfo {
std::uint16_t chipId; ///< Part identifier from the chip.
std::uint8_t firmwareMajor; ///< Loaded firmware major version.
std::uint8_t firmwareMinor; ///< Loaded firmware minor version.
std::uint8_t firmwareBuild; ///< Loaded firmware build number.
};
/**
* @brief Si4684SysState — running application after boot.
*
* @dname Si4684SysState
* @return n/a (type)
* @pubstate Plain DTO from GET_SYS_STATE.
*
* @author Michele Bigi
* @date 2026-07-06
*/
struct Si4684SysState {
std::uint8_t imageType; ///< Loaded image type byte from the chip.
};
/**
* @brief Si4684FmRsq — FM received-signal quality (FM_RSQ_STATUS).
*
* @dname Si4684FmRsq
* @return n/a (type)
* @pubstate Plain DTO from FM_RSQ_STATUS response bytes.
*
* @author Michele Bigi
* @date 2026-07-06
*/
struct Si4684FmRsq {
core::FrequencyKHz frequency; ///< Tuned centre frequency in kHz.
std::int8_t rssiDbuV; ///< RSSI in dBµV.
std::int8_t snrDb; ///< SNR in dB.
bool valid; ///< RSQ valid flag from the chip.
bool stereo; ///< Stereo pilot detected.
};
/**
* @brief Si4684FmRdsStatus — raw RDS group snapshot (FM_RDS_STATUS).
*
* @dname Si4684FmRdsStatus
* @return n/a (type)
* @pubstate Plain DTO from FM_RDS_STATUS response bytes.
*
* @author Michele Bigi
* @date 2026-07-06
*/
struct Si4684FmRdsStatus {
std::uint16_t blockA; ///< RDS block A.
std::uint16_t blockB; ///< RDS block B.
std::uint16_t blockC; ///< RDS block C.
std::uint16_t blockD; ///< RDS block D.
bool received; ///< Group received flag.
};
/**
* @brief Si4684DabDigRadStatus — ensemble lock metrics (DAB_DIGRAD_STATUS).
*
* @dname Si4684DabDigRadStatus
* @return n/a (type)
* @pubstate Plain DTO from DAB_DIGRAD_STATUS response bytes.
*
* @author Michele Bigi
* @date 2026-07-06
*/
struct Si4684DabDigRadStatus {
std::uint8_t ficQuality; ///< FIC quality 0100.
std::uint8_t cnrDb; ///< CNR in dB.
bool acquired; ///< Ensemble acquired flag.
bool valid; ///< DIGRAD valid flag.
};
/**
* @brief Si4684DabEventStatus — DAB event flags (DAB_GET_EVENT_STATUS).
*
* @dname Si4684DabEventStatus
* @return n/a (type)
* @pubstate Plain DTO from DAB_GET_EVENT_STATUS response bytes.
*
* @author Michele Bigi
* @date 2026-07-06
*/
struct Si4684DabEventStatus {
bool serviceListReady; ///< Service list available for fetch.
bool reconfig; ///< Ensemble reconfiguration event.
};
/**
* @brief Si4684DabService — one row from GET_DIGITAL_SERVICE_LIST.
*
* @dname Si4684DabService
* @return n/a (type)
* @pubstate Plain DTO parsed from a service-list chunk.
*
* @author Michele Bigi
* @date 2026-07-06
*/
struct Si4684DabService {
std::uint32_t serviceId; ///< DAB service identifier.
std::uint32_t componentId; ///< Audio component identifier.
std::array<char, 17> label; ///< Programme label, NUL-terminated.
std::uint8_t serviceType; ///< Service type byte from the list.
};
/** Band III DAB channel plan (kHz), PE5PVB / ETSI EN 300 401 Table 14. */
inline constexpr std::array<std::uint32_t, 38> kDefaultDabFrequencyKhz = {
174928, 176640, 178352, 180064, 181936, 183648, 185360, 187072, 188928,
190640, 192352, 194064, 195936, 197648, 199360, 201072, 202928, 204640,
206352, 208064, 209936, 211648, 213360, 215072, 216928, 218640, 220352,
222064, 223936, 225648, 227360, 229072, 230784, 232496, 234208, 235776,
237488, 239200,
};
} // namespace si4684
@@ -0,0 +1,776 @@
/**
* @file Si4684Driver.cpp
* @brief Si4684Driver 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 "si4684/Si4684Driver.hpp"
#include "driver/gpio.h"
#include "driver/spi_master.h"
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include <algorithm>
#include <array>
#include <cstring>
#include <span>
namespace si4684 {
namespace {
constexpr char kTag[] = "Si4684";
constexpr std::size_t kSpiBufferSize = 4096U;
constexpr int kCtsPollMs = 2;
constexpr int kCtsRetries = 200;
constexpr int kStcRetries = 250;
constexpr int kStcPollMs = 20;
constexpr std::uint16_t kPropDigitalIoOutputSelect = 0x0200U;
constexpr std::uint16_t kPropDigitalIoSampleRate = 0x0201U;
constexpr std::uint16_t kPropPinConfigEnable = 0x0800U;
constexpr std::uint16_t kPropAudioVolume = 0x0300U;
constexpr std::uint16_t kPropFmRdsConfig = 0x3C02U;
constexpr std::uint16_t kPropDabTuneFeCfg = 0x1712U;
constexpr std::uint16_t kPropDabXpadEnable = 0xB400U;
constexpr std::uint16_t kPropDigitalServiceIntSource = 0x8100U;
std::uint16_t readLe16(const std::uint8_t* p)
{
return static_cast<std::uint16_t>(p[0] | (static_cast<std::uint16_t>(p[1]) << 8));
}
std::uint32_t readLe32(const std::uint8_t* p)
{
return static_cast<std::uint32_t>(p[0])
| (static_cast<std::uint32_t>(p[1]) << 8)
| (static_cast<std::uint32_t>(p[2]) << 16)
| (static_cast<std::uint32_t>(p[3]) << 24);
}
/** FM_TUNE_FREQ uses 10 kHz units (AN649); API uses kHz. */
[[nodiscard]] std::uint16_t kHzToChipFmFreq(std::uint32_t frequencyKhz)
{
return static_cast<std::uint16_t>(frequencyKhz / 10U);
}
[[nodiscard]] std::uint32_t chipFmFreqToKHz(std::uint16_t chipFreq)
{
return static_cast<std::uint32_t>(chipFreq) * 10U;
}
} // namespace
Si4684Driver::Si4684Driver(Si4684Pins pins,
const core::IFirmwareBlobReader& patch,
const core::IFirmwareBlobReader& dabImage,
const core::IFirmwareBlobReader& fmImage)
: pins_(pins)
, patch_(patch)
, dabImage_(dabImage)
, fmImage_(fmImage)
, booted_(false)
, loadedBand_(Si4684Band::Dab)
, spiBusActive_(false)
, spiDevice_(nullptr)
{
}
Si4684Driver::~Si4684Driver()
{
if (spiDevice_ != nullptr) {
spi_bus_remove_device(static_cast<spi_device_handle_t>(spiDevice_));
spiDevice_ = nullptr;
}
if (spiBusActive_) {
spi_bus_free(static_cast<spi_host_device_t>(pins_.spiHost));
spiBusActive_ = false;
}
}
std::expected<void, Si4684Error> Si4684Driver::ensureBooted() const
{
if (!booted_) {
return std::unexpected(Si4684Error::NotBooted);
}
return {};
}
std::expected<void, Si4684Error> Si4684Driver::ensureBand(
Si4684Band band) const
{
if (auto ready = ensureBooted(); !ready) {
return ready;
}
if (loadedBand_ != band) {
return std::unexpected(Si4684Error::WrongBand);
}
return {};
}
std::expected<void, Si4684Error> Si4684Driver::waitCts()
{
std::array<std::uint8_t, 5> pollTx = {};
std::array<std::uint8_t, 5> pollRx = {};
for (int attempt = 0; attempt < kCtsRetries; ++attempt) {
vTaskDelay(pdMS_TO_TICKS(kCtsPollMs));
spi_transaction_t txn = {};
txn.length = pollTx.size() * 8U;
txn.tx_buffer = pollTx.data();
txn.rx_buffer = pollRx.data();
if (spi_device_transmit(static_cast<spi_device_handle_t>(spiDevice_),
&txn) != ESP_OK) {
return std::unexpected(Si4684Error::SpiInitFailed);
}
if ((pollRx[1] & 0x80U) != 0U) {
return {};
}
}
return std::unexpected(Si4684Error::CtsTimeout);
}
std::expected<void, Si4684Error> Si4684Driver::waitStc()
{
std::array<std::uint8_t, 5> pollTx = {};
std::array<std::uint8_t, 5> pollRx = {};
for (int attempt = 0; attempt < kStcRetries; ++attempt) {
vTaskDelay(pdMS_TO_TICKS(kStcPollMs));
spi_transaction_t txn = {};
txn.length = pollTx.size() * 8U;
txn.tx_buffer = pollTx.data();
txn.rx_buffer = pollRx.data();
if (spi_device_transmit(static_cast<spi_device_handle_t>(spiDevice_),
&txn) != ESP_OK) {
return std::unexpected(Si4684Error::SpiInitFailed);
}
if ((pollRx[1] & 0x01U) != 0U) {
return {};
}
}
return std::unexpected(Si4684Error::StcTimeout);
}
std::expected<void, Si4684Error> Si4684Driver::sendCommand(
std::span<const std::uint8_t> bytes)
{
if (bytes.empty() || bytes.size() > kSpiBufferSize) {
return std::unexpected(Si4684Error::CommandFailed);
}
spi_transaction_t txn = {};
txn.length = bytes.size() * 8U;
txn.tx_buffer = bytes.data();
if (spi_device_transmit(static_cast<spi_device_handle_t>(spiDevice_),
&txn) != ESP_OK) {
return std::unexpected(Si4684Error::SpiInitFailed);
}
return waitCts();
}
std::expected<void, Si4684Error> Si4684Driver::readRaw(
std::span<std::uint8_t> buffer)
{
if (buffer.empty() || buffer.size() > kSpiBufferSize) {
return std::unexpected(Si4684Error::ReplyTooShort);
}
std::fill(buffer.begin(), buffer.end(), 0U);
spi_transaction_t txn = {};
txn.length = buffer.size() * 8U;
txn.tx_buffer = buffer.data();
txn.rx_buffer = buffer.data();
if (spi_device_transmit(static_cast<spi_device_handle_t>(spiDevice_),
&txn) != ESP_OK) {
return std::unexpected(Si4684Error::SpiInitFailed);
}
return {};
}
std::expected<void, Si4684Error> Si4684Driver::writeCommand(
Command cmd, const std::uint8_t* payload, std::size_t length)
{
if (length + 2U > kSpiBufferSize) {
return std::unexpected(Si4684Error::CommandFailed);
}
std::array<std::uint8_t, kSpiBufferSize> buffer = {};
buffer[0] = static_cast<std::uint8_t>(cmd);
buffer[1] = 0x00U;
if (payload != nullptr && length > 0U) {
std::memcpy(buffer.data() + 2U, payload, length);
}
return sendCommand({buffer.data(), 2U + length});
}
std::expected<void, Si4684Error> Si4684Driver::hostLoadBlob(
const core::IFirmwareBlobReader& blob, std::size_t chunkPayload)
{
std::array<std::byte, 2044> payload = {};
std::size_t offset = 0U;
while (offset < blob.size()) {
const std::size_t maxChunk = std::min(chunkPayload, payload.size());
const std::size_t copied =
blob.read(offset, std::span<std::byte>(payload.data(), maxChunk));
if (copied == 0U) {
return std::unexpected(Si4684Error::ImageLoadFailed);
}
std::array<std::uint8_t, kSpiBufferSize> tx = {};
tx[0] = static_cast<std::uint8_t>(Command::HostLoad);
tx[1] = 0x00U;
tx[2] = 0x00U;
tx[3] = 0x00U;
std::memcpy(tx.data() + 4U, payload.data(), copied);
spi_transaction_t txn = {};
txn.length = (4U + copied) * 8U;
txn.tx_buffer = tx.data();
if (spi_device_transmit(static_cast<spi_device_handle_t>(spiDevice_),
&txn) != ESP_OK) {
return std::unexpected(Si4684Error::ImageLoadFailed);
}
if (auto cts = waitCts(); !cts) {
return cts;
}
offset += copied;
}
return {};
}
std::expected<void, Si4684Error> Si4684Driver::setProperty(
std::uint16_t propertyId, std::uint16_t value)
{
if (auto ready = ensureBooted(); !ready) {
return ready;
}
const std::uint8_t args[] = {
static_cast<std::uint8_t>(propertyId & 0xFFU),
static_cast<std::uint8_t>(propertyId >> 8),
static_cast<std::uint8_t>(value & 0xFFU),
static_cast<std::uint8_t>(value >> 8),
};
if (auto cmd = writeCommand(Command::SetProperty, args, sizeof(args));
!cmd) {
return std::unexpected(Si4684Error::CommandFailed);
}
return {};
}
std::expected<void, Si4684Error> Si4684Driver::setVolume(std::uint8_t level)
{
return setProperty(kPropAudioVolume,
static_cast<std::uint16_t>(level & 0x3FU));
}
std::expected<Si4684PartInfo, Si4684Error> Si4684Driver::getPartInfo()
{
if (auto ready = ensureBooted(); !ready) {
return std::unexpected(ready.error());
}
if (auto cmd = writeCommand(Command::GetPartInfo, nullptr, 0U); !cmd) {
return std::unexpected(Si4684Error::CommandFailed);
}
std::array<std::uint8_t, 24> raw = {};
if (auto rd = readRaw(raw); !rd) {
return std::unexpected(rd.error());
}
if (auto fn = writeCommand(Command::GetFuncInfo, nullptr, 0U); !fn) {
return std::unexpected(Si4684Error::CommandFailed);
}
std::array<std::uint8_t, 13> fnRaw = {};
if (auto rd = readRaw(fnRaw); !rd) {
return std::unexpected(rd.error());
}
Si4684PartInfo info = {};
info.chipId = readLe16(raw.data() + 9);
info.firmwareMajor = fnRaw[5];
info.firmwareMinor = fnRaw[6];
info.firmwareBuild = fnRaw[7];
return info;
}
std::expected<Si4684SysState, Si4684Error> Si4684Driver::getSysState()
{
if (auto ready = ensureBooted(); !ready) {
return std::unexpected(ready.error());
}
if (auto cmd = writeCommand(Command::GetSysState, nullptr, 0U); !cmd) {
return std::unexpected(Si4684Error::CommandFailed);
}
std::array<std::uint8_t, 7> raw = {};
if (auto rd = readRaw(raw); !rd) {
return std::unexpected(rd.error());
}
Si4684SysState state = {};
state.imageType = raw[5];
return state;
}
std::expected<void, Si4684Error> Si4684Driver::configureAfterBoot(
Si4684Band band)
{
if (band == Si4684Band::Dab) {
if (auto plan = installDefaultDabFrequencyPlan(); !plan) {
return plan;
}
static constexpr std::uint16_t kDabProps[][2] = {
{0x0202U, 0x1600U},
{0x1710U, 0xFC4AU},
{0x1711U, 0x00F8U},
{0x8101U, 0x0064U},
{0xB200U, 0x0000U},
{0xB201U, 0x0080U},
{0xB301U, 0x0000U},
{0xB302U, 0x0000U},
{0xB303U, 0x0000U},
{0xB401U, 0x0002U},
{0xB500U, 0x0000U},
};
for (const auto& prop : kDabProps) {
if (auto set = setProperty(prop[0], prop[1]); !set) {
return set;
}
}
if (auto xpad = setProperty(kPropDabXpadEnable, 0x0097U); !xpad) {
return xpad;
}
} else {
if (auto rds = setProperty(kPropFmRdsConfig, 0x0001U); !rds) {
return rds;
}
}
if (auto i2s = setProperty(kPropDigitalIoOutputSelect, 0x8000U); !i2s) {
return i2s;
}
if (auto rate = setProperty(kPropDigitalIoSampleRate, 0xAC44U); !rate) {
return rate;
}
if (auto pins = setProperty(kPropPinConfigEnable, 0x0003U); !pins) {
return pins;
}
if (auto dabFe = setProperty(kPropDabTuneFeCfg, 0x0001U); !dabFe) {
return dabFe;
}
if (auto dsrv = setProperty(kPropDigitalServiceIntSource, 0x0001U);
!dsrv) {
return dsrv;
}
return {};
}
std::expected<void, Si4684Error> Si4684Driver::boot(Si4684Band band)
{
if (booted_ && loadedBand_ == band) {
return {};
}
if (booted_) {
booted_ = false;
if (spiDevice_ != nullptr) {
spi_bus_remove_device(static_cast<spi_device_handle_t>(spiDevice_));
spiDevice_ = nullptr;
}
}
const core::IFirmwareBlobReader& image =
(band == Si4684Band::Fm) ? fmImage_ : dabImage_;
gpio_config_t rstCfg = {};
rstCfg.pin_bit_mask = 1ULL << pins_.rstbGpio;
rstCfg.mode = GPIO_MODE_OUTPUT;
if (gpio_config(&rstCfg) != ESP_OK) {
return std::unexpected(Si4684Error::ResetFailed);
}
gpio_set_level(static_cast<gpio_num_t>(pins_.rstbGpio), 0);
vTaskDelay(pdMS_TO_TICKS(5));
gpio_set_level(static_cast<gpio_num_t>(pins_.rstbGpio), 1);
vTaskDelay(pdMS_TO_TICKS(3));
if (!spiBusActive_) {
spi_bus_config_t busCfg = {};
busCfg.miso_io_num = pins_.misoGpio;
busCfg.mosi_io_num = pins_.mosiGpio;
busCfg.sclk_io_num = pins_.sclkGpio;
busCfg.quadwp_io_num = -1;
busCfg.quadhd_io_num = -1;
busCfg.max_transfer_sz = static_cast<int>(kSpiBufferSize);
if (spi_bus_initialize(static_cast<spi_host_device_t>(pins_.spiHost),
&busCfg, SPI_DMA_CH_AUTO) != ESP_OK) {
return std::unexpected(Si4684Error::SpiInitFailed);
}
spiBusActive_ = true;
}
if (spiDevice_ == nullptr) {
spi_device_interface_config_t devCfg = {};
devCfg.clock_speed_hz = 10 * 1000 * 1000;
devCfg.mode = 0;
devCfg.spics_io_num = pins_.csGpio;
devCfg.queue_size = 1;
spi_device_handle_t dev = nullptr;
if (spi_bus_add_device(static_cast<spi_host_device_t>(pins_.spiHost),
&devCfg, &dev) != ESP_OK) {
return std::unexpected(Si4684Error::SpiInitFailed);
}
spiDevice_ = dev;
}
if (auto st = writeCommand(Command::GetSysState, nullptr, 0U); !st) {
return st;
}
const std::uint8_t powerUp[] = {
0x17, 0x48, 0x00, 0xf8, 0x24, 0x01, 0x1F, 0x10,
0x00, 0x00, 0x00, 0x18, 0x00, 0x00,
};
if (auto pu = writeCommand(Command::PowerUp, powerUp, sizeof(powerUp));
!pu) {
return std::unexpected(Si4684Error::PowerUpFailed);
}
vTaskDelay(pdMS_TO_TICKS(1));
if (auto li = writeCommand(Command::LoadInit, nullptr, 0U); !li) {
return std::unexpected(Si4684Error::PatchLoadFailed);
}
if (auto patch = hostLoadBlob(patch_, 124U); !patch) {
return std::unexpected(Si4684Error::PatchLoadFailed);
}
vTaskDelay(pdMS_TO_TICKS(4));
if (auto li2 = writeCommand(Command::LoadInit, nullptr, 0U); !li2) {
return std::unexpected(Si4684Error::ImageLoadFailed);
}
if (auto fw = hostLoadBlob(image, 2044U); !fw) {
return std::unexpected(Si4684Error::ImageLoadFailed);
}
if (auto bootCmd = writeCommand(Command::BootCmd, nullptr, 0U); !bootCmd) {
return std::unexpected(Si4684Error::BootFailed);
}
booted_ = true;
loadedBand_ = band;
if (auto cfg = configureAfterBoot(band); !cfg) {
booted_ = false;
return cfg;
}
ESP_LOGI(kTag, "%s firmware booted",
band == Si4684Band::Fm ? "FM" : "DAB");
return {};
}
bool Si4684Driver::isBooted() const noexcept
{
return booted_;
}
Si4684Band Si4684Driver::loadedBand() const noexcept
{
return loadedBand_;
}
std::expected<void, Si4684Error> Si4684Driver::tuneFm(
core::FrequencyKHz frequency)
{
if (auto band = ensureBand(Si4684Band::Fm); !band) {
return band;
}
const std::uint16_t chipFreq = kHzToChipFmFreq(frequency.value());
const std::uint8_t args[] = {
0x00U,
static_cast<std::uint8_t>(chipFreq & 0xFFU),
static_cast<std::uint8_t>(chipFreq >> 8),
0x00U,
0x00U,
};
if (auto cmd = writeCommand(Command::FmTuneFreq, args, sizeof(args));
!cmd) {
return std::unexpected(Si4684Error::TuneFailed);
}
if (auto stc = waitStc(); !stc) {
return stc;
}
return {};
}
std::expected<core::FrequencyKHz, Si4684Error> Si4684Driver::seekFm(
core::SeekDirection direction, SeekBandWrap wrap)
{
if (auto band = ensureBand(Si4684Band::Fm); !band) {
return std::unexpected(band.error());
}
const bool seekUp = direction == core::SeekDirection::Up;
const bool wrapBand = wrap == SeekBandWrap::Wrap;
const std::uint8_t args[] = {
0x10U,
static_cast<std::uint8_t>(((seekUp ? 1U : 0U) << 1U) | (wrapBand ? 1U : 0U)),
0x00U,
0x00U,
0x00U,
};
if (auto cmd = writeCommand(Command::FmSeekStart, args, sizeof(args));
!cmd) {
return std::unexpected(Si4684Error::TuneFailed);
}
if (auto stc = waitStc(); !stc) {
return std::unexpected(stc.error());
}
auto rsq = readFmRsq();
if (!rsq) {
return std::unexpected(rsq.error());
}
return rsq->frequency;
}
std::expected<Si4684FmRsq, Si4684Error> Si4684Driver::readFmRsq()
{
if (auto band = ensureBand(Si4684Band::Fm); !band) {
return std::unexpected(band.error());
}
const std::uint8_t args[] = {0x00U};
if (auto cmd = writeCommand(Command::FmRsqStatus, args, sizeof(args));
!cmd) {
return std::unexpected(Si4684Error::CommandFailed);
}
std::array<std::uint8_t, 23> raw = {};
if (auto rd = readRaw(raw); !rd) {
return std::unexpected(rd.error());
}
Si4684FmRsq rsq = {};
const auto khz = chipFmFreqToKHz(readLe16(raw.data() + 6));
if (auto freq = core::FrequencyKHz::tryFromKhz(khz); freq) {
rsq.frequency = *freq;
} else {
return std::unexpected(Si4684Error::CommandFailed);
}
rsq.valid = (raw[4] & 0x01U) != 0U;
rsq.stereo = (raw[4] & 0x02U) != 0U;
rsq.rssiDbuV = static_cast<std::int8_t>(raw[8]);
rsq.snrDb = static_cast<std::int8_t>(raw[9]);
return rsq;
}
std::expected<Si4684FmRdsStatus, Si4684Error> Si4684Driver::readFmRds()
{
if (auto band = ensureBand(Si4684Band::Fm); !band) {
return std::unexpected(band.error());
}
const std::uint8_t args[] = {0x01U};
if (auto cmd = writeCommand(Command::FmRdsStatus, args, sizeof(args));
!cmd) {
return std::unexpected(Si4684Error::CommandFailed);
}
std::array<std::uint8_t, 21> raw = {};
if (auto rd = readRaw(raw); !rd) {
return std::unexpected(rd.error());
}
Si4684FmRdsStatus rds = {};
rds.received = (raw[4] & 0x01U) != 0U;
rds.blockA = readLe16(raw.data() + 12);
rds.blockB = readLe16(raw.data() + 14);
rds.blockC = readLe16(raw.data() + 16);
rds.blockD = readLe16(raw.data() + 18);
return rds;
}
std::expected<void, Si4684Error> Si4684Driver::installDefaultDabFrequencyPlan()
{
if (auto band = ensureBand(Si4684Band::Dab); !band) {
return band;
}
std::array<std::uint8_t, 4U + kDefaultDabFrequencyKhz.size() * 4U> cmd =
{};
cmd[0] = static_cast<std::uint8_t>(Command::DabSetFreqList);
cmd[1] = static_cast<std::uint8_t>(kDefaultDabFrequencyKhz.size());
cmd[2] = 0x00U;
cmd[3] = 0x00U;
for (std::size_t i = 0; i < kDefaultDabFrequencyKhz.size(); ++i) {
const std::uint32_t hz = kDefaultDabFrequencyKhz[i];
const std::size_t off = 4U + i * 4U;
cmd[off] = static_cast<std::uint8_t>(hz & 0xFFU);
cmd[off + 1] = static_cast<std::uint8_t>((hz >> 8) & 0xFFU);
cmd[off + 2] = static_cast<std::uint8_t>((hz >> 16) & 0xFFU);
cmd[off + 3] = static_cast<std::uint8_t>(hz >> 24);
}
return sendCommand(cmd);
}
std::expected<void, Si4684Error> Si4684Driver::tuneDab(std::uint8_t freqIndex)
{
if (auto band = ensureBand(Si4684Band::Dab); !band) {
return band;
}
if (freqIndex >= kDefaultDabFrequencyKhz.size()) {
return std::unexpected(Si4684Error::TuneFailed);
}
const std::uint8_t args[] = {0x00U, freqIndex, 0x00U, 0x00U, 0x00U};
if (auto cmd = writeCommand(Command::DabTuneFreq, args, sizeof(args));
!cmd) {
return std::unexpected(Si4684Error::TuneFailed);
}
if (auto stc = waitStc(); !stc) {
return stc;
}
return {};
}
std::expected<Si4684DabDigRadStatus, Si4684Error>
Si4684Driver::readDabDigRadStatus()
{
if (auto band = ensureBand(Si4684Band::Dab); !band) {
return std::unexpected(band.error());
}
const std::uint8_t args[] = {0x01U};
if (auto cmd =
writeCommand(Command::DabDigRadStatus, args, sizeof(args));
!cmd) {
return std::unexpected(Si4684Error::CommandFailed);
}
std::array<std::uint8_t, 20> raw = {};
if (auto rd = readRaw(raw); !rd) {
return std::unexpected(rd.error());
}
Si4684DabDigRadStatus status = {};
status.ficQuality = raw[9];
status.cnrDb = raw[10];
status.acquired = (raw[4] & 0x08U) != 0U;
status.valid = status.ficQuality > 0U;
return status;
}
std::expected<Si4684DabEventStatus, Si4684Error>
Si4684Driver::readDabEventStatus()
{
if (auto band = ensureBand(Si4684Band::Dab); !band) {
return std::unexpected(band.error());
}
const std::uint8_t args[] = {0x00U};
if (auto cmd =
writeCommand(Command::DabGetEventStatus, args, sizeof(args));
!cmd) {
return std::unexpected(Si4684Error::CommandFailed);
}
std::array<std::uint8_t, 9> raw = {};
if (auto rd = readRaw(raw); !rd) {
return std::unexpected(rd.error());
}
Si4684DabEventStatus events = {};
events.serviceListReady = (raw[4] & 0x01U) != 0U;
events.reconfig = (raw[4] & 0x02U) != 0U;
return events;
}
std::expected<std::vector<Si4684DabService>, Si4684Error>
Si4684Driver::fetchDabServiceList()
{
if (auto band = ensureBand(Si4684Band::Dab); !band) {
return std::unexpected(band.error());
}
const std::uint8_t args[] = {0x00U};
if (auto cmd = writeCommand(Command::GetDigitalServiceList, args,
sizeof(args));
!cmd) {
return std::unexpected(Si4684Error::CommandFailed);
}
std::array<std::uint8_t, 9> header = {};
if (auto rd = readRaw(header); !rd) {
return std::unexpected(rd.error());
}
const std::uint16_t payloadSize = readLe16(header.data() + 5);
if (payloadSize == 0U || payloadSize + 6U > kSpiBufferSize) {
return std::unexpected(Si4684Error::ReplyTooShort);
}
std::vector<std::uint8_t> body(payloadSize + 6U, 0U);
if (auto rd = readRaw(body); !rd) {
return std::unexpected(rd.error());
}
const std::uint8_t serviceCount = body[9];
std::vector<Si4684DabService> services;
services.reserve(serviceCount);
std::size_t offset = 13U;
for (std::uint8_t i = 0; i < serviceCount; ++i) {
if (offset + 24U > body.size()) {
break;
}
Si4684DabService entry = {};
entry.serviceId = readLe32(body.data() + offset);
offset += 4U;
entry.serviceType = body[offset] & 0x3FU;
const std::uint8_t componentCount = body[offset + 1] & 0x0FU;
offset += 4U;
std::memcpy(entry.label.data(), body.data() + offset, 16U);
entry.label[16] = '\0';
offset += 16U;
if (componentCount > 0U && offset + 4U <= body.size()) {
entry.componentId = readLe32(body.data() + offset);
offset += 4U;
if (offset < body.size()) {
++offset;
}
}
services.push_back(entry);
}
return services;
}
std::expected<void, Si4684Error> Si4684Driver::startDabService(
std::uint32_t serviceId,
std::uint32_t componentId,
Si4684DigitalServiceType type)
{
if (auto band = ensureBand(Si4684Band::Dab); !band) {
return band;
}
const std::uint8_t args[] = {
static_cast<std::uint8_t>(type),
0x00U,
0x00U,
static_cast<std::uint8_t>(serviceId & 0xFFU),
static_cast<std::uint8_t>((serviceId >> 8) & 0xFFU),
static_cast<std::uint8_t>((serviceId >> 16) & 0xFFU),
static_cast<std::uint8_t>(serviceId >> 24),
static_cast<std::uint8_t>(componentId & 0xFFU),
static_cast<std::uint8_t>((componentId >> 8) & 0xFFU),
static_cast<std::uint8_t>((componentId >> 16) & 0xFFU),
static_cast<std::uint8_t>(componentId >> 24),
};
if (auto cmd =
writeCommand(Command::StartDigitalService, args, sizeof(args));
!cmd) {
return std::unexpected(Si4684Error::CommandFailed);
}
return {};
}
} // namespace si4684
@@ -0,0 +1,89 @@
/**
* @file Si4684EmbeddedImages.cpp
* @brief Si4684EmbeddedImages 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 "si4684/Si4684EmbeddedImages.hpp"
#include <cstddef>
#include <cstdint>
/** @cond linker_symbols */
extern "C" {
extern const std::uint8_t rom_patch_016_bin_start[]
asm("_binary_rom_patch_016_bin_start");
extern const std::uint8_t rom_patch_016_bin_end[]
asm("_binary_rom_patch_016_bin_end");
extern const std::uint8_t dab_firmware_bin_start[]
asm("_binary_dab_firmware_bin_start");
extern const std::uint8_t dab_firmware_bin_end[]
asm("_binary_dab_firmware_bin_end");
extern const std::uint8_t fm_firmware_bin_start[]
asm("_binary_fm_firmware_bin_start");
extern const std::uint8_t fm_firmware_bin_end[]
asm("_binary_fm_firmware_bin_end");
}
/** @endcond */
namespace si4684 {
namespace {
const std::byte* asBytes(const std::uint8_t* ptr)
{
return reinterpret_cast<const std::byte*>(ptr);
}
std::size_t embeddedSize(const std::uint8_t* start, const std::uint8_t* end)
{
return static_cast<std::size_t>(end - start);
}
} // namespace
Si4684EmbeddedImages::Si4684EmbeddedImages()
: patch_(asBytes(rom_patch_016_bin_start),
embeddedSize(rom_patch_016_bin_start, rom_patch_016_bin_end))
, dab_(asBytes(dab_firmware_bin_start),
embeddedSize(dab_firmware_bin_start, dab_firmware_bin_end))
, fm_(asBytes(fm_firmware_bin_start),
embeddedSize(fm_firmware_bin_start, fm_firmware_bin_end))
{
}
const core::IFirmwareBlobReader& Si4684EmbeddedImages::romPatch() const noexcept
{
return patch_;
}
const core::IFirmwareBlobReader& Si4684EmbeddedImages::dabFirmware() const noexcept
{
return dab_;
}
const core::IFirmwareBlobReader& Si4684EmbeddedImages::fmFirmware() const noexcept
{
return fm_;
}
const core::IFirmwareBlobReader& Si4684EmbeddedImages::applicationImage(
Si4684Band band) const noexcept
{
switch (band) {
case Si4684Band::Dab:
return dab_;
case Si4684Band::Fm:
return fm_;
}
return dab_;
}
} // namespace si4684
@@ -0,0 +1,186 @@
/**
* @file Si4684Tuner.cpp
* @brief Si4684Tuner 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 "si4684/Si4684Tuner.hpp"
#include "si4684/Si4684Band.hpp"
namespace si4684 {
namespace {
[[nodiscard]] core::FrequencyKHz defaultFmFrequency()
{
return *core::FrequencyKHz::tryFromKhz(101500U);
}
} // namespace
Si4684Tuner::Si4684Tuner(Si4684Driver& driver)
: driver_(driver)
, dabIndex_(0U)
, fmFrequency_(defaultFmFrequency())
, volume_(40U)
{
}
core::TunerError Si4684Tuner::mapError(Si4684Error error) noexcept
{
switch (error) {
case Si4684Error::NotBooted:
return core::TunerError::NotBooted;
case Si4684Error::WrongBand:
return core::TunerError::WrongBand;
case Si4684Error::TuneFailed:
case Si4684Error::StcTimeout:
return core::TunerError::TuneFailed;
default:
return core::TunerError::HardwareFailed;
}
}
std::expected<void, core::TunerError> Si4684Tuner::boot(core::TunerBand band)
{
const Si4684Band hwBand =
(band == core::TunerBand::Fm) ? Si4684Band::Fm : Si4684Band::Dab;
if (auto result = driver_.boot(hwBand); !result) {
return std::unexpected(mapError(result.error()));
}
return {};
}
std::expected<core::TunerBand, core::TunerError> Si4684Tuner::currentBand() const
{
if (!driver_.isBooted()) {
return std::unexpected(core::TunerError::NotBooted);
}
return driver_.loadedBand() == Si4684Band::Fm ? core::TunerBand::Fm
: core::TunerBand::Dab;
}
std::expected<core::TunerStatus, core::TunerError> Si4684Tuner::readStatus()
{
if (!driver_.isBooted()) {
return std::unexpected(core::TunerError::NotBooted);
}
core::TunerStatus status = {};
status.booted = true;
status.volume = volume_;
status.band = driver_.loadedBand() == Si4684Band::Fm ? core::TunerBand::Fm
: core::TunerBand::Dab;
if (status.band == core::TunerBand::Dab) {
status.dabFreqIndex = dabIndex_;
if (auto dig = driver_.readDabDigRadStatus(); dig) {
status.locked = dig->valid;
status.dabFicQuality = dig->ficQuality;
status.dabCnrDb = dig->cnrDb;
} else {
return std::unexpected(mapError(dig.error()));
}
} else {
status.fmFrequency = fmFrequency_;
if (auto rsq = driver_.readFmRsq(); rsq) {
status.locked = rsq->valid;
status.fmFrequency = rsq->frequency;
status.fmRssiDbuV = rsq->rssiDbuV;
status.fmSnrDb = rsq->snrDb;
status.fmStereo = rsq->stereo;
fmFrequency_ = rsq->frequency;
} else {
return std::unexpected(mapError(rsq.error()));
}
}
return status;
}
std::expected<void, core::TunerError> Si4684Tuner::tuneDab(
std::uint8_t freqIndex)
{
if (auto result = driver_.tuneDab(freqIndex); !result) {
return std::unexpected(mapError(result.error()));
}
dabIndex_ = freqIndex;
return {};
}
std::expected<void, core::TunerError> Si4684Tuner::tuneFm(
core::FrequencyKHz frequency)
{
if (auto result = driver_.tuneFm(frequency); !result) {
return std::unexpected(mapError(result.error()));
}
fmFrequency_ = frequency;
return {};
}
std::expected<core::FrequencyKHz, core::TunerError> Si4684Tuner::seekFm(
core::SeekDirection direction)
{
if (auto result = driver_.seekFm(direction, SeekBandWrap::Wrap); !result) {
return std::unexpected(mapError(result.error()));
}
fmFrequency_ = *result;
return *result;
}
std::expected<std::vector<core::TunerServiceEntry>, core::TunerError>
Si4684Tuner::listDabServices()
{
if (auto events = driver_.readDabEventStatus(); events) {
if (!events->serviceListReady) {
return std::unexpected(core::TunerError::ServiceListEmpty);
}
} else {
return std::unexpected(mapError(events.error()));
}
if (auto list = driver_.fetchDabServiceList(); list) {
std::vector<core::TunerServiceEntry> out;
out.reserve(list->size());
for (const auto& item : *list) {
core::TunerServiceEntry entry = {};
entry.serviceId = item.serviceId;
entry.componentId = item.componentId;
entry.label = item.label;
out.push_back(entry);
}
if (out.empty()) {
return std::unexpected(core::TunerError::ServiceListEmpty);
}
return out;
}
return std::unexpected(mapError(list.error()));
}
std::expected<void, core::TunerError> Si4684Tuner::playDabService(
std::uint32_t serviceId,
std::uint32_t componentId)
{
if (auto result = driver_.startDabService(serviceId, componentId); !result) {
return std::unexpected(mapError(result.error()));
}
return {};
}
std::expected<void, core::TunerError> Si4684Tuner::setVolume(std::uint8_t level)
{
if (auto result = driver_.setVolume(level); !result) {
return std::unexpected(mapError(result.error()));
}
volume_ = level & 0x3FU;
return {};
}
} // namespace si4684
@@ -1,33 +0,0 @@
/**
* @file component_stub.cpp
* @brief Si4684 driver component placeholder (Slice 4).
*
* DigiRadio firmware — https://github.com/manvalan/DigiRadio
*
* Copyright 2026 Michele Bigi
* 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
* @date 2026-07-06
*/
namespace si4684::detail {
/**
* @brief si4684ComponentLinked — ensures the driver component links.
*
* @dname si4684ComponentLinked
* @return n/a (type)
* @pubstate n/a
*
* @author Michele Bigi
* @date 2026-07-06
*/
void si4684ComponentLinked() noexcept {}
} // namespace si4684::detail