Add BT1035 driver with manual chapter and expand Si4684 tuning docs.

Implement Bt1035Driver AT init (AT+AUXCFG=1), core AT parser with host tests,
wire boot into HardwareBootstrap, and document DAB/FM tuning workflows in ch-si4684.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-06 17:07:37 +02:00
co-authored by Cursor
parent d6fa41e944
commit d49b2011dd
22 changed files with 979 additions and 48 deletions
+1
View File
@@ -22,6 +22,7 @@ idf_component_register(
"src/EnhancementsDesign.cpp" "src/EnhancementsDesign.cpp"
"src/AudioProfile.cpp" "src/AudioProfile.cpp"
"src/AudioProfileJson.cpp" "src/AudioProfileJson.cpp"
"src/Bt1035At.cpp"
INCLUDE_DIRS "include" INCLUDE_DIRS "include"
) )
@@ -0,0 +1,99 @@
/**
* @file Bt1035At.hpp
* @brief Typed AT command builder and response parser for FSC-BT1035.
*
* 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 <array>
#include <cstddef>
#include <expected>
#include <string>
#include <string_view>
namespace core {
/**
* @brief Bt1035AtCommand — supported AT commands (enumerated subset).
*
* @dname Bt1035AtCommand
* @return n/a (type)
* @pubstate Each variant maps to one line via buildBt1035AtLine().
*
* @author Michele Bigi
* @date 2026-07-06
*/
enum class Bt1035AtCommand {
Ping, ///< AT — link check.
AuxLineIn, ///< AT+AUXCFG=1 — wired Line-In from ADAU1701 (mandatory).
};
/**
* @brief Bt1035AtResponseKind — parsed module reply class.
*
* @dname Bt1035AtResponseKind
* @return n/a (type)
* @pubstate Unknown payloads map to Unexpected, never ignored.
*
* @author Michele Bigi
* @date 2026-07-06
*/
enum class Bt1035AtResponseKind {
Ok,
Error,
Unexpected,
};
/** Number of commands in bootInitSequence(). */
inline constexpr std::size_t kBt1035BootInitCommandCount = 2U;
/**
* @brief buildBt1035AtLine — serialise a command with CRLF terminator.
*
* @dname buildBt1035AtLine
* @param command Typed AT command.
* @return Full line including \r\n suffix.
* @pubstate none
*
* @author Michele Bigi
* @date 2026-07-06
*/
[[nodiscard]] std::string buildBt1035AtLine(Bt1035AtCommand command);
/**
* @brief bootInitSequence — mandatory bring-up commands in order.
*
* @dname bootInitSequence
* @return Ping then AuxLineIn (AT+AUXCFG=1).
* @pubstate none
*
* @author Michele Bigi
* @date 2026-07-06
*/
[[nodiscard]] std::array<Bt1035AtCommand, kBt1035BootInitCommandCount>
bootInitSequence() noexcept;
/**
* @brief parseBt1035AtResponse — classify a single response line.
*
* @dname parseBt1035AtResponse
* @param line Untrusted UART payload (may include whitespace).
* @return Ok, Error, or Unexpected.
* @pubstate none
*
* @author Michele Bigi
* @date 2026-07-06
*/
[[nodiscard]] Bt1035AtResponseKind parseBt1035AtResponse(
std::string_view line) noexcept;
} // namespace core
+68
View File
@@ -0,0 +1,68 @@
/**
* @file Bt1035At.cpp
* @brief Bt1035At 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 "core/Bt1035At.hpp"
namespace core {
namespace {
[[nodiscard]] std::string_view trimAscii(std::string_view text) noexcept
{
while (!text.empty()
&& (text.front() == ' ' || text.front() == '\r'
|| text.front() == '\n' || text.front() == '\t')) {
text.remove_prefix(1U);
}
while (!text.empty()
&& (text.back() == ' ' || text.back() == '\r' || text.back() == '\n'
|| text.back() == '\t')) {
text.remove_suffix(1U);
}
return text;
}
} // namespace
std::string buildBt1035AtLine(Bt1035AtCommand command)
{
switch (command) {
case Bt1035AtCommand::Ping:
return "AT\r\n";
case Bt1035AtCommand::AuxLineIn:
return "AT+AUXCFG=1\r\n";
}
return "AT\r\n";
}
std::array<Bt1035AtCommand, kBt1035BootInitCommandCount> bootInitSequence() noexcept
{
return std::array<Bt1035AtCommand, kBt1035BootInitCommandCount>{
Bt1035AtCommand::Ping,
Bt1035AtCommand::AuxLineIn,
};
}
Bt1035AtResponseKind parseBt1035AtResponse(std::string_view line) noexcept
{
const std::string_view trimmed = trimAscii(line);
if (trimmed == "OK") {
return Bt1035AtResponseKind::Ok;
}
if (trimmed == "ERROR" || trimmed.starts_with("ERROR")) {
return Bt1035AtResponseKind::Error;
}
return Bt1035AtResponseKind::Unexpected;
}
} // namespace core
@@ -34,6 +34,7 @@ add_library(digiradio_core STATIC
"${CORE_SRC_DIR}/EnhancementsDesign.cpp" "${CORE_SRC_DIR}/EnhancementsDesign.cpp"
"${CORE_SRC_DIR}/AudioProfile.cpp" "${CORE_SRC_DIR}/AudioProfile.cpp"
"${CORE_SRC_DIR}/AudioProfileJson.cpp" "${CORE_SRC_DIR}/AudioProfileJson.cpp"
"${CORE_SRC_DIR}/Bt1035At.cpp"
) )
target_include_directories(digiradio_core PUBLIC target_include_directories(digiradio_core PUBLIC
"${CMAKE_CURRENT_SOURCE_DIR}/../include" "${CMAKE_CURRENT_SOURCE_DIR}/../include"
@@ -70,3 +71,7 @@ add_test(NAME audio_profile_json_test COMMAND audio_profile_json_test)
add_executable(enhancements_design_test enhancements_design_test.cpp) add_executable(enhancements_design_test enhancements_design_test.cpp)
target_link_libraries(enhancements_design_test PRIVATE digiradio_core) target_link_libraries(enhancements_design_test PRIVATE digiradio_core)
add_test(NAME enhancements_design_test COMMAND enhancements_design_test) add_test(NAME enhancements_design_test COMMAND enhancements_design_test)
add_executable(bt1035_at_test bt1035_at_test.cpp)
target_link_libraries(bt1035_at_test PRIVATE digiradio_core)
add_test(NAME bt1035_at_test COMMAND bt1035_at_test)
@@ -0,0 +1,71 @@
/**
* @file bt1035_at_test.cpp
* @brief Host tests for FSC-BT1035 AT command builder and parser.
*
* 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 "core/Bt1035At.hpp"
#include <cstdlib>
#include <iostream>
#include <string>
namespace {
[[nodiscard]] int runInitSequenceTest()
{
const auto sequence = core::bootInitSequence();
if (sequence.size() != core::kBt1035BootInitCommandCount) {
std::cerr << "init sequence size mismatch\n";
return EXIT_FAILURE;
}
if (sequence[1U] != core::Bt1035AtCommand::AuxLineIn) {
std::cerr << "AUXCFG=1 must be in init sequence\n";
return EXIT_FAILURE;
}
const std::string aux = core::buildBt1035AtLine(core::Bt1035AtCommand::AuxLineIn);
if (aux != "AT+AUXCFG=1\r\n") {
std::cerr << "AUXCFG command line mismatch\n";
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
[[nodiscard]] int runParseTest()
{
if (core::parseBt1035AtResponse("OK\r\n") != core::Bt1035AtResponseKind::Ok) {
std::cerr << "OK parse failed\n";
return EXIT_FAILURE;
}
if (core::parseBt1035AtResponse("ERROR\r\n")
!= core::Bt1035AtResponseKind::Error) {
std::cerr << "ERROR parse failed\n";
return EXIT_FAILURE;
}
if (core::parseBt1035AtResponse("garbage")
!= core::Bt1035AtResponseKind::Unexpected) {
std::cerr << "unexpected parse failed\n";
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
} // namespace
int main()
{
if (runInitSequenceTest() != EXIT_SUCCESS) {
return EXIT_FAILURE;
}
if (runParseTest() != EXIT_SUCCESS) {
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
@@ -1,6 +1,7 @@
idf_component_register( idf_component_register(
SRCS "src/component_stub.cpp" SRCS "src/Bt1035Driver.cpp"
INCLUDE_DIRS "include" INCLUDE_DIRS "include"
REQUIRES core driver esp_driver_gpio esp_driver_uart
) )
target_compile_features(${COMPONENT_LIB} PUBLIC cxx_std_23) target_compile_features(${COMPONENT_LIB} PUBLIC cxx_std_23)
@@ -0,0 +1,134 @@
/**
* @file Bt1035Driver.hpp
* @brief FSC-BT1035 Bluetooth transmitter — UART AT 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 "bt1035/Bt1035Error.hpp"
#include "core/Bt1035At.hpp"
#include <expected>
namespace bt1035 {
/**
* @brief Bt1035Pins — board GPIO/UART identifiers for the module.
*
* @dname Bt1035Pins
* @return n/a (type)
* @pubstate Immutable wiring snapshot from board_pins.hpp.
*
* @author Michele Bigi
* @date 2026-07-06
*/
struct Bt1035Pins {
int uartTx; ///< ESP32 TX -> module RX.
int uartRx; ///< ESP32 RX <- module TX.
int rtsGpio; ///< RTS (flow control).
int ctsGpio; ///< CTS (flow control).
int resetGpio; ///< Module RESET (active level per schematic).
int sysCtlGpio; ///< SYS_CTL (optional module enable).
};
/**
* @brief Bt1035Driver — owns UART + reset, runs mandatory AT init.
*
* @dname Bt1035Driver
* @return n/a (type)
* @pubstate Owns UART port after boot(). booted_ true after init sequence
* including AT+AUXCFG=1 (Line-In from ADAU1701).
*
* @author Michele Bigi
* @date 2026-07-06
*/
class Bt1035Driver {
public:
/**
* @brief Bt1035Driver — construct with board pin map.
*
* @dname Bt1035Driver
* @param pins UART, flow control, reset, SYS_CTL wiring.
* @pubstate stores pins_; not booted until boot().
*
* @author Michele Bigi
* @date 2026-07-06
*/
explicit Bt1035Driver(Bt1035Pins pins);
/**
* @brief ~Bt1035Driver — release UART resources.
*
* @dname ~Bt1035Driver
* @pubstate deletes UART driver when installed.
*
* @author Michele Bigi
* @date 2026-07-06
*/
~Bt1035Driver();
Bt1035Driver(const Bt1035Driver&) = delete;
Bt1035Driver& operator=(const Bt1035Driver&) = delete;
/**
* @brief boot — reset module and run mandatory AT init sequence.
*
* @dname boot
* @return Ok on success, or Bt1035Error.
* @pubstate sets booted_ after Ping + AT+AUXCFG=1 both return OK.
*
* Sequence: hardware reset, UART @ 115200 with RTS/CTS, then
* core::bootInitSequence() (Chapter~\ref{ch:bt1035}).
*
* @author Michele Bigi
* @date 2026-07-06
*/
[[nodiscard]] std::expected<void, Bt1035Error> boot();
/**
* @brief isBooted — query whether Line-In init succeeded.
*
* @dname isBooted
* @return true after successful boot().
* @pubstate reads booted_.
*
* @author Michele Bigi
* @date 2026-07-06
*/
[[nodiscard]] bool isBooted() const noexcept;
/**
* @brief sendCommand — transmit one typed AT command and expect OK.
*
* @dname sendCommand
* @param command Enumerated AT command.
* @return Ok on OK response, or Bt1035Error.
* @pubstate writes UART; reads until OK/ERROR/timeout.
*
* @author Michele Bigi
* @date 2026-07-06
*/
[[nodiscard]] std::expected<void, Bt1035Error> sendCommand(
core::Bt1035AtCommand command);
private:
[[nodiscard]] std::expected<void, Bt1035Error> ensureBooted() const;
[[nodiscard]] std::expected<void, Bt1035Error> runInitSequence();
[[nodiscard]] std::expected<void, Bt1035Error> transmitAndExpectOk(
std::string_view commandLine);
Bt1035Pins pins_;
bool booted_;
bool uartInstalled_;
int uartPort_;
};
} // namespace bt1035
@@ -0,0 +1,36 @@
/**
* @file Bt1035Error.hpp
* @brief Typed errors for FSC-BT1035 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 bt1035 {
/**
* @brief Bt1035Error — failure causes for BT1035 bring-up and AT I/O.
*
* @dname Bt1035Error
* @return n/a (type)
* @pubstate n/a
*
* @author Michele Bigi
* @date 2026-07-06
*/
enum class Bt1035Error {
ResetFailed,
UartInitFailed,
NotBooted,
AtTimeout,
AtError,
UnexpectedResponse,
};
} // namespace bt1035
@@ -0,0 +1,193 @@
/**
* @file Bt1035Driver.cpp
* @brief Bt1035Driver 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 "bt1035/Bt1035Driver.hpp"
#include "driver/gpio.h"
#include "driver/uart.h"
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include <array>
#include <string>
namespace bt1035 {
namespace {
constexpr char kTag[] = "Bt1035";
constexpr int kUartPort = 2;
constexpr int kBaudRate = 115200;
constexpr int kUartRxBuffer = 512;
constexpr int kUartTxBuffer = 256;
constexpr int kResponseTimeoutMs = 2000;
constexpr int kPostResetMs = 300;
constexpr int kPostUartMs = 100;
} // namespace
Bt1035Driver::Bt1035Driver(Bt1035Pins pins)
: pins_(pins)
, booted_(false)
, uartInstalled_(false)
, uartPort_(kUartPort)
{
}
Bt1035Driver::~Bt1035Driver()
{
if (uartInstalled_) {
uart_driver_delete(static_cast<uart_port_t>(uartPort_));
uartInstalled_ = false;
}
}
bool Bt1035Driver::isBooted() const noexcept
{
return booted_;
}
std::expected<void, Bt1035Error> Bt1035Driver::ensureBooted() const
{
if (!booted_) {
return std::unexpected(Bt1035Error::NotBooted);
}
return {};
}
std::expected<void, Bt1035Error> Bt1035Driver::transmitAndExpectOk(
std::string_view commandLine)
{
const int written = uart_write_bytes(static_cast<uart_port_t>(uartPort_),
commandLine.data(),
commandLine.size());
if (written < 0
|| static_cast<std::size_t>(written) != commandLine.size()) {
return std::unexpected(Bt1035Error::UartInitFailed);
}
std::array<char, 128> buffer{};
std::string accumulated;
const TickType_t deadline =
xTaskGetTickCount() + pdMS_TO_TICKS(kResponseTimeoutMs);
while (xTaskGetTickCount() < deadline) {
const int received = uart_read_bytes(static_cast<uart_port_t>(uartPort_),
buffer.data(), buffer.size(),
pdMS_TO_TICKS(50));
if (received > 0) {
accumulated.append(buffer.data(), static_cast<std::size_t>(received));
const core::Bt1035AtResponseKind kind =
core::parseBt1035AtResponse(accumulated);
if (kind == core::Bt1035AtResponseKind::Ok) {
return {};
}
if (kind == core::Bt1035AtResponseKind::Error) {
return std::unexpected(Bt1035Error::AtError);
}
}
}
return std::unexpected(Bt1035Error::AtTimeout);
}
std::expected<void, Bt1035Error> Bt1035Driver::sendCommand(
core::Bt1035AtCommand command)
{
if (auto ready = ensureBooted(); !ready) {
return ready;
}
return transmitAndExpectOk(core::buildBt1035AtLine(command));
}
std::expected<void, Bt1035Error> Bt1035Driver::runInitSequence()
{
for (const core::Bt1035AtCommand command : core::bootInitSequence()) {
if (auto result = transmitAndExpectOk(core::buildBt1035AtLine(command));
!result) {
return result;
}
}
return {};
}
std::expected<void, Bt1035Error> Bt1035Driver::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(Bt1035Error::ResetFailed);
}
gpio_config_t sysCfg = {};
sysCfg.pin_bit_mask = 1ULL << pins_.sysCtlGpio;
sysCfg.mode = GPIO_MODE_OUTPUT;
if (gpio_config(&sysCfg) != ESP_OK) {
return std::unexpected(Bt1035Error::ResetFailed);
}
gpio_set_level(static_cast<gpio_num_t>(pins_.sysCtlGpio), 1);
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(kPostResetMs));
if (!uartInstalled_) {
const uart_config_t uartCfg = {
.baud_rate = kBaudRate,
.data_bits = UART_DATA_8_BITS,
.parity = UART_PARITY_DISABLE,
.stop_bits = UART_STOP_BITS_1,
.flow_ctrl = UART_HW_FLOWCTRL_CTS_RTS,
.rx_flow_ctrl_thresh = 122,
.source_clk = UART_SCLK_DEFAULT,
};
if (uart_driver_install(static_cast<uart_port_t>(uartPort_), kUartRxBuffer,
kUartTxBuffer, 0, nullptr, 0)
!= ESP_OK) {
return std::unexpected(Bt1035Error::UartInitFailed);
}
if (uart_param_config(static_cast<uart_port_t>(uartPort_), &uartCfg)
!= ESP_OK) {
return std::unexpected(Bt1035Error::UartInitFailed);
}
if (uart_set_pin(static_cast<uart_port_t>(uartPort_), pins_.uartTx,
pins_.uartRx, pins_.rtsGpio, pins_.ctsGpio)
!= ESP_OK) {
return std::unexpected(Bt1035Error::UartInitFailed);
}
uartInstalled_ = true;
}
uart_flush_input(static_cast<uart_port_t>(uartPort_));
vTaskDelay(pdMS_TO_TICKS(kPostUartMs));
if (auto init = runInitSequence(); !init) {
ESP_LOGE(kTag, "AT init failed");
return init;
}
booted_ = true;
ESP_LOGI(kTag, "Line-In mode enabled (AT+AUXCFG=1)");
return {};
}
} // namespace bt1035
@@ -1,34 +0,0 @@
/**
* @file component_stub.cpp
* @brief FSC-BT1035 driver component placeholder (Slice 6).
*
* 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 bt1035::detail {
/**
* @brief bt1035ComponentLinked — ensures the driver component links.
*
* @dname bt1035ComponentLinked
* @return n/a (type)
* @pubstate n/a
*
* @author Michele Bigi
* @date 2026-07-06
*/
void bt1035ComponentLinked() noexcept {}
} // namespace bt1035::detail
+3 -1
View File
@@ -98,7 +98,9 @@ persistence fails.
Returns a tuner snapshot serialised by Returns a tuner snapshot serialised by
\texttt{core::serializeTunerStatusJson()} from \texttt{core::TunerStatus}. \texttt{core::serializeTunerStatusJson()} from \texttt{core::TunerStatus}.
The handler calls \texttt{tuner::TunerService::refreshStatus()}. The handler calls \texttt{tuner::TunerService::refreshStatus()}. DAB and FM
tuning workflows are in Chapter~\ref{ch:si4684}, Sections~\ref{sec:si4684-dab-session}
and~\ref{sec:si4684-fm-session}.
\begin{drnote}[Response schema (DAB example)] \begin{drnote}[Response schema (DAB example)]
\begin{drcode}[JSON] \begin{drcode}[JSON]
+239
View File
@@ -0,0 +1,239 @@
% ============================================================
% DigiRadio — Manual chapter: FSC-BT1035 Bluetooth
% ============================================================
\chapter{FSC-BT1035 Bluetooth Transmitter}
\label{ch:bt1035}
The Feasycom FSC-BT1035 (Qualcomm QCC3056) is the wireless output stage of
DigiRadio: it receives PCM from the ADAU1701 over I\textsuperscript{2}S and
streams Bluetooth audio with aptX, aptX~HD, and aptX~Adaptive. This chapter
documents how the ESP32-S3 controls the module over UART (AT commands with
RTS/CTS), why Line-In mode is mandatory, and how \texttt{bt1035::Bt1035Driver}
implements the bring-up sequence.
\begin{drref}[Hardware context]
Board wiring (UART pins, I\textsuperscript{2}S to the module, flow control)
is in Chapter~\ref{ch:hardware}, Section~\ref{sec:hw-bt1035}. The ADAU1701
master clock and limiter settings that feed the module are in
Chapter~\ref{ch:adau1701} and Chapter~\ref{ch:sigmastudio}.
\end{drref}
\section{Role in the audio chain}
\label{sec:bt1035-role}
The BT1035 is an I\textsuperscript{2}S \textbf{slave} at 48\,kHz: LRCLK and
BCLK come from the ADAU1701; PCM arrives on \texttt{SDATA\_OUT0}
(ADAU MP6 $\rightarrow$ module pin~5). The ESP32 does not process audio
samples for Bluetooth; it only configures the module so the wired path is
accepted and encoded for transmission.
Without firmware init the module may stay in a default mode that ignores the
Line-In from the DSP. The mandatory \texttt{AT+AUXCFG=1} command selects
auxiliary/Line-In input --- omitting it silently breaks the entire wireless
output (Section~\ref{sec:bt1035-linein}).
\section{Control interface}
\label{sec:bt1035-uart}
\subsection{UART parameters}
\begin{table}[htbp]
\centering
\begin{tabular}{@{}ll@{}}
\drhead Setting & Value \\
\midrule
Port & UART2 (not the console UART) \\
Baud rate & 115200 \\
Data format & 8N1 \\
Flow control & Hardware RTS/CTS (required) \\
Reset & GPIO active-low pulse at boot \\
SYS\_CTL & Held active to enable the module \\
\bottomrule
\end{tabular}
\caption{FSC-BT1035 UART and control lines (\texttt{board\_pins.hpp}).}
\label{tab:bt1035-uart}
\end{table}
\subsection{Response handling}
Every command expects a module reply containing \texttt{OK} or
\texttt{ERROR}. The driver:
\begin{itemize}
\item builds lines in the pure core via \texttt{core::buildBt1035AtLine()};
\item classifies replies with \texttt{core::parseBt1035AtResponse()};
\item treats timeouts and unexpected payloads as errors (never ignored).
\end{itemize}
Host tests in \texttt{components/core/test/bt1035\_at\_test.cpp} lock the
init sequence (including \texttt{AT+AUXCFG=1}) and the parser.
\section{Mandatory Line-In mode}
\label{sec:bt1035-linein}
\begin{drcaution}[AT+AUXCFG=1 is not optional]
The documented init sequence must include \texttt{AT+AUXCFG=1} after a
successful \texttt{AT} ping. This tells the QCC3056 firmware to take audio
from the wired I\textsuperscript{2}S/Line-In port (the ADAU1701 output)
rather than an internal source. AGENTS.md and the hardware manual both treat
skipping this step as a production bug.
\end{drcaution}
\section{Boot sequence}
\label{sec:bt1035-boot}
At power-up \texttt{HardwareBootstrap::boot()} runs the Si4684 and ADAU1701
first, applies the saved audio profile, then initialises the BT1035 so the
Line-In path is ready before Wi-Fi starts.
\begin{figure}[htbp]
\centering
\begin{tikzpicture}[
step/.style={draw, rounded corners, minimum width=9.5cm,
minimum height=0.9cm, align=center, font=\small},
>={Latex}, node distance=3mm]
\node[step, fill=black!6] (sys) {SYS\_CTL high, RESET\# pulse};
\node[step, fill=black!6, below=of sys] (uart)
{Install UART2 @ 115200, RTS/CTS};
\node[step, fill=black!8, below=of uart] (at)
{Send \texttt{AT} --- expect OK};
\node[step, fill=black!10, below=of at] (aux)
{Send \texttt{AT+AUXCFG=1} --- expect OK (Line-In)};
\node[step, fill=black!6, below=of aux] (done)
{\texttt{Bt1035Driver::isBooted()} = true};
\foreach \a/\b in {sys/uart, uart/at, at/aux, aux/done} {
\draw[->] (\a) -- (\b);
}
\end{tikzpicture}
\caption{FSC-BT1035 AT init on DigiRadio.}
\label{fig:bt1035-boot}
\end{figure}
Pairing, codec selection, and volume over Bluetooth are handled by the
module's own firmware and NVS; DigiRadio firmware currently implements
only the Line-In bring-up required for the wired audio path.
\section{Software architecture}
\label{sec:bt1035-stack}
\begin{table}[htbp]
\centering
\small
\begin{tabular}{@{}llp{5.8cm}@{}}
\drhead Layer & Type & Responsibility \\
\midrule
Bootstrap & \texttt{HardwareBootstrap} &
Constructs driver, calls \texttt{boot()} after ADAU1701 \\
Driver & \texttt{bt1035::Bt1035Driver} &
UART, reset, AT init sequence \\
Pure core & \texttt{core::Bt1035AtCommand}, parser &
Host-testable command strings and OK/ERROR classification \\
\bottomrule
\end{tabular}
\caption{BT1035 control stack (Slice~6). Future HTTP/UI for pairing will
sit above the driver without changing the init contract.}
\label{tab:bt1035-stack}
\end{table}
\section{Supported AT command subset}
\label{sec:bt1035-at}
The firmware enumerates every command it sends. Extending the subset requires
updating \texttt{core::Bt1035AtCommand}, the manual, and a host test.
\begin{table}[htbp]
\centering
\begin{tabular}{@{}lll@{}}
\drhead Enum & Line sent & Purpose \\
\midrule
\texttt{Ping} & \texttt{AT} & Verify UART link \\
\texttt{AuxLineIn} & \texttt{AT+AUXCFG=1} & Enable Line-In from ADAU \\
\bottomrule
\end{tabular}
\caption{AT commands used at boot (\texttt{core::bootInitSequence()}).}
\label{tab:bt1035-at}
\end{table}
\section{Bt1035Driver API}
\label{sec:bt1035-driver}
\texttt{Bt1035Driver} owns UART and GPIO reset. All methods return
\texttt{std::expected<T, Bt1035Error>}.
\begin{table}[htbp]
\centering
\begin{tabular}{@{}ll@{}}
\toprule
\textbf{Method} & \textbf{Purpose} \\
\midrule
\texttt{boot()} & Reset, UART init, run \texttt{bootInitSequence()} \\
\texttt{isBooted()} & \texttt{true} after Line-In init succeeded \\
\texttt{sendCommand(cmd)} & Send one typed command, expect OK \\
\bottomrule
\end{tabular}
\caption{Public driver API.}
\label{tab:bt1035-api}
\end{table}
\subsection{Error codes}
\begin{table}[htbp]
\centering
\small
\begin{tabular}{@{}ll@{}}
\drhead \texttt{Bt1035Error} & Typical cause \\
\midrule
\texttt{ResetFailed} & GPIO configuration failure \\
\texttt{UartInitFailed} & \texttt{uart\_driver\_install} / pins \\
\texttt{NotBooted} & \texttt{sendCommand} before \texttt{boot()} \\
\texttt{AtTimeout} & No OK/ERROR within 2\,s \\
\texttt{AtError} & Module returned ERROR \\
\texttt{UnexpectedResponse} & Unrecognised payload (future use) \\
\bottomrule
\end{tabular}
\caption{Driver error enumeration.}
\label{tab:bt1035-errors}
\end{table}
\section{Integration at power-up}
\label{sec:bt1035-integration}
\texttt{main/hardware\_bootstrap.cpp} constructs a static
\texttt{Bt1035Driver} and calls \texttt{boot()} after
\texttt{AudioService::loadAndApply()}. Failure returns
\texttt{HardwareBootError::Bt1035BootFailed} and \texttt{app\_main} halts
before network bring-up (fail-closed, same as Si4684/ADAU1701).
Boot order:
\begin{enumerate}
\item Si4684 \texttt{boot(Dab)} --- tuner image in RAM.
\item ADAU1701 \texttt{boot()} --- SigmaStudio program in RAM.
\item \texttt{AudioService::loadAndApply()} --- user mixer/EQ profile.
\item BT1035 \texttt{boot()} --- Line-In enabled for wireless output.
\end{enumerate}
\section{Typical usage (firmware developer)}
\label{sec:bt1035-usage}
After a successful \texttt{HardwareBootstrap::boot()}, the module is ready;
no further calls are required for basic listening. To re-send Line-In config
after a module reset:
\begin{verbatim}
bt1035::Bt1035Driver& bt = ...;
if (auto r = bt.sendCommand(core::Bt1035AtCommand::AuxLineIn); !r) {
// handle Bt1035Error
}
\end{verbatim}
\section{Further reading}
\label{sec:bt1035-reading}
\begin{itemize}
\item Feasycom FSC-BT1035 AT command manual (vendor) --- full command set
for pairing, name, and codec options not yet wrapped by firmware.
\item Chapter~\ref{ch:hardware} --- pin map and I\textsuperscript{2}S routing.
\item Chapter~\ref{ch:adau1701} --- DSP output that feeds the module.
\item \texttt{components/core/test/bt1035\_at\_test.cpp} --- init sequence test.
\end{itemize}
+7 -1
View File
@@ -130,7 +130,13 @@ fetch, and \texttt{startDabService}. Property access (\texttt{setProperty},
\texttt{setVolume}) and diagnostics (\texttt{getPartInfo}, \texttt{getSysState}) \texttt{setVolume}) and diagnostics (\texttt{getPartInfo}, \texttt{getSysState})
are included. Wrong-band calls return \texttt{Si4684Error::WrongBand}. are included. Wrong-band calls return \texttt{Si4684Error::WrongBand}.
Register-level opcodes remain private; see \texttt{Si4684Types.hpp} for status Register-level opcodes remain private; see \texttt{Si4684Types.hpp} for status
DTOs. DTOs. Full DAB/FM tuning guide: Chapter~\ref{ch:si4684}.
\section{Bt1035Driver}\label{cls:Bt1035Driver}
UART driver for the FSC-BT1035 (Chapter~\ref{ch:bt1035}). \texttt{boot()}
pulses RESET\#, opens UART2 with RTS/CTS, and runs
\texttt{core::bootInitSequence()} (Ping + \texttt{AT+AUXCFG=1}). Returns
\texttt{Bt1035Error} on timeout, ERROR response, or UART failure.
\section{Adau1701Driver}\label{cls:Adau1701Driver} \section{Adau1701Driver}\label{cls:Adau1701Driver}
RAII I\textsuperscript{2}C driver for the ADAU1701 SigmaDSP RAII I\textsuperscript{2}C driver for the ADAU1701 SigmaDSP
+8 -4
View File
@@ -147,10 +147,11 @@ safeload registers.
\end{drcaution} \end{drcaution}
\paragraph{FSC-BT1035 (Bluetooth).} \paragraph{FSC-BT1035 (Bluetooth).}
The module is controlled by AT commands over UART. The initialisation The module is controlled by AT commands over UART with hardware flow control.
sequence includes enabling Line-In mode (\texttt{AT+AUXCFG=1}), which is The initialisation sequence includes enabling Line-In mode
required for the wired audio path from the DSP; command responses are (\texttt{AT+AUXCFG=1}), which is required for the wired audio path from the
parsed explicitly, with timeouts treated as errors. DSP; command responses are parsed explicitly, with timeouts treated as errors.
Full driver API, boot flow, and error codes are in Chapter~\ref{ch:bt1035}.
\section{Configuration, storage, and user interface} \section{Configuration, storage, and user interface}
\label{sec:fw-config} \label{sec:fw-config}
@@ -203,6 +204,9 @@ bring-up:
\item \textbf{Audio profile}: \texttt{audio::AudioService::loadAndApply()} \item \textbf{Audio profile}: \texttt{audio::AudioService::loadAndApply()}
restores the saved \texttt{core::AudioProfile} from NVS (or factory restores the saved \texttt{core::AudioProfile} from NVS (or factory
defaults) via ADAU1701 safeload before network bring-up. defaults) via ADAU1701 safeload before network bring-up.
\item \textbf{FSC-BT1035} (UART): \texttt{bt1035::Bt1035Driver::boot()}
enables Line-In (\texttt{AT+AUXCFG=1}) after the DSP path is configured
(Chapter~\ref{ch:bt1035}).
\end{enumerate} \end{enumerate}
If either driver returns an error, the firmware logs the failure and stops If either driver returns an error, the firmware logs the failure and stops
+2 -1
View File
@@ -131,7 +131,8 @@ in the module's own non-volatile memory.
\begin{drcaution}[Line-In mode] \begin{drcaution}[Line-In mode]
The initialisation sequence must enable Line-In mode (\texttt{AT+AUXCFG=1}) The initialisation sequence must enable Line-In mode (\texttt{AT+AUXCFG=1})
so the module accepts the wired audio coming from the DSP. Omitting it so the module accepts the wired audio coming from the DSP. Omitting it
silently breaks the audio path. silently breaks the audio path. Driver boot flow and AT subset are documented
in Chapter~\ref{ch:bt1035}.
\end{drcaution} \end{drcaution}
\section{The host: ESP32-S3} \section{The host: ESP32-S3}
+2 -1
View File
@@ -30,7 +30,8 @@ extending DigiRadio: the hardware at a block level
(Chapter~\ref{ch:hardware}), the firmware architecture (Chapter~\ref{ch:hardware}), the firmware architecture
(Chapter~\ref{ch:firmware}), dedicated companion-chip guides for the (Chapter~\ref{ch:firmware}), dedicated companion-chip guides for the
Si4684 tuner (Chapter~\ref{ch:si4684}) and ADAU1701 DSP Si4684 tuner (Chapter~\ref{ch:si4684}) and ADAU1701 DSP
(Chapter~\ref{ch:adau1701}), the HTTP JSON API exposed by the web UI (Chapter~\ref{ch:adau1701}), FSC-BT1035 Bluetooth
(Chapter~\ref{ch:bt1035}), the HTTP JSON API exposed by the web UI
(Chapter~\ref{ch:api}), the per-class design reference that grows with (Chapter~\ref{ch:api}), the per-class design reference that grows with
the code (Chapter~\ref{ch:classes}), and how to build and flash the code (Chapter~\ref{ch:classes}), and how to build and flash
(Chapter~\ref{ch:build}). Exact C++ signatures are generated by Doxygen (Chapter~\ref{ch:build}). Exact C++ signatures are generated by Doxygen
+88 -2
View File
@@ -224,18 +224,104 @@ after boot.
\end{table} \end{table}
\subsection{Typical DAB session} \subsection{Typical DAB session}
\label{sec:si4684-dab-session}
\begin{enumerate} \begin{enumerate}
\item \texttt{boot(Dab)} at power-up (already done in \item \texttt{boot(Dab)} at power-up (already done in
\texttt{HardwareBootstrap}). \texttt{HardwareBootstrap}).
\item \texttt{tuneDab(index)} for the desired ensemble. \item \texttt{tuneDab(index)} for the desired ensemble (Band III index
0--37, Table~\ref{tab:si4684-dab-plan}).
\item Poll \texttt{readDabDigRadStatus()} until FIC quality $> 0$. \item Poll \texttt{readDabDigRadStatus()} until FIC quality $> 0$.
\item \texttt{fetchDabServiceList()} when \item \texttt{fetchDabServiceList()} when
\texttt{readDabEventStatus().serviceListReady}. \texttt{readDabEventStatus().serviceListReady}.
\item \texttt{startDabService(serviceId, componentId)} for the chosen \item \texttt{startDabService(serviceId, componentId)} for the chosen
programme; audio appears on I\textsuperscript{2}S. programme; PCM appears on I\textsuperscript{2}S to the ADAU1701.
\end{enumerate} \end{enumerate}
\begin{drnote}[DAB, not DVB]
DigiRadio receives \textbf{DAB/DAB+} (Digital \emph{Audio} Broadcasting),
not DVB-T/T2 video. Tuning is by ensemble frequency index and digital
service/component IDs --- there is no transport-stream PAT/PMT scan like
DVB-T.
\end{drnote}
\subsection{Typical FM session}
\label{sec:si4684-fm-session}
FM requires the FM application image loaded at boot
(\texttt{boot(Si4684Band::Fm)}). DigiRadio defaults to DAB at power-up;
switching to FM reloads patch + \texttt{fm\_firmware.bin} (full HOST\_LOAD).
\begin{enumerate}
\item \texttt{boot(Fm)} --- resets the chip and loads the FM image.
\item \texttt{tuneFm(frequencyKhz)} --- e.g.\ 101500 for 101.5\,MHz;
waits for seek/tune complete (STC).
\item \texttt{readFmRsq()} --- RSSI, SNR, stereo flag, validity.
\item Optional: \texttt{seekFm(up, wrap)} --- scan to next station.
\item Optional: \texttt{readFmRds()} --- last RDS group (PI, PS, RT).
\end{enumerate}
FM audio is emitted on the same I\textsuperscript{2}S pins as DAB once the
FM image is running.
\subsection{Choosing DAB or FM}
\label{sec:si4684-band-choice}
Only one application image runs at a time. \texttt{Si4684Driver::boot(band)}
selects the blob:
\begin{table}[htbp]
\centering
\begin{tabular}{@{}lll@{}}
\drhead Band & Image loaded & Primary API \\
\midrule
\texttt{Si4684Band::Dab} & \texttt{dab\_firmware.bin} &
\texttt{tuneDab}, service list, \texttt{startDabService} \\
\texttt{Si4684Band::Fm} & \texttt{fm\_firmware.bin} &
\texttt{tuneFm}, \texttt{seekFm}, RSQ, RDS \\
\bottomrule
\end{tabular}
\caption{Band-specific firmware and driver entry points.}
\label{tab:si4684-band-api}
\end{table}
Calling an FM method while the DAB image is loaded (or vice versa) returns
\texttt{Si4684Error::WrongBand}. Switching band performs reset and a complete
HOST\_LOAD of patch + the other image ($\sim$1\,s).
\subsection{HTTP API mapping (web UI)}
\label{sec:si4684-http}
The setup UI and REST clients use \texttt{tuner::TunerService}, which wraps
\texttt{Si4684Tuner} (Chapter~\ref{ch:api}). Summary:
\begin{table}[htbp]
\centering
\small
\begin{tabular}{@{}llp{5.5cm}@{}}
\drhead Endpoint & Band & Action \\
\midrule
\texttt{POST /api/tuner/tune} &
DAB & \texttt{\{"band":"dab","freq\_index":0..37\}} \\
\texttt{POST /api/tuner/tune} &
FM & \texttt{\{"band":"fm","frequency\_khz":64000..108000\}} \\
\texttt{GET /api/tuner/services} & DAB only & Programme list for ensemble \\
\texttt{POST /api/tuner/play} & DAB only & Start \texttt{service\_id}/\texttt{component\_id} \\
\texttt{POST /api/tuner/seek} & FM only & Seek up, return new kHz \\
\texttt{GET /api/tuner/status} & both & Locked state, RSQ or DIGRAD \\
\bottomrule
\end{tabular}
\caption{Tuner HTTP routes vs band (full schemas in Chapter~\ref{ch:api}).}
\label{tab:si4684-http}
\end{table}
\begin{drnote}[FM tune via HTTP today]
\texttt{POST /api/tuner/tune} with \texttt{band:"fm"} calls
\texttt{TunerService::tuneFm}, which may trigger a band reload if the device
booted in DAB. Plan station presets and automatic band selection in a later
slice.
\end{drnote}
\section{Integration at power-up} \section{Integration at power-up}
\label{sec:si4684-integration} \label{sec:si4684-integration}
+1
View File
@@ -48,6 +48,7 @@
\include{ch-firmware} \include{ch-firmware}
\include{ch-si4684} \include{ch-si4684}
\include{ch-adau1701} \include{ch-adau1701}
\include{ch-bt1035}
\include{ch-api} \include{ch-api}
\include{ch-classes} \include{ch-classes}
\include{ch-build} \include{ch-build}
+1 -1
View File
@@ -3,5 +3,5 @@ idf_component_register(
"main.cpp" "main.cpp"
"hardware_bootstrap.cpp" "hardware_bootstrap.cpp"
INCLUDE_DIRS "." INCLUDE_DIRS "."
REQUIRES core net secure_store adau1701 si4684 tuner audio REQUIRES core net secure_store adau1701 si4684 tuner audio bt1035
) )
+16
View File
@@ -17,6 +17,7 @@
#include "adau1701/Adau1701Dsp.hpp" #include "adau1701/Adau1701Dsp.hpp"
#include "audio/AudioService.hpp" #include "audio/AudioService.hpp"
#include "board_pins.hpp" #include "board_pins.hpp"
#include "bt1035/Bt1035Driver.hpp"
#include "secure_store/NvsAudioProfileStore.hpp" #include "secure_store/NvsAudioProfileStore.hpp"
#include "si4684/Si4684Band.hpp" #include "si4684/Si4684Band.hpp"
#include "si4684/Si4684Driver.hpp" #include "si4684/Si4684Driver.hpp"
@@ -58,6 +59,16 @@ adau1701::Adau1701Dsp gAdau1701Dsp(gAdau1701);
secure_store::NvsAudioProfileStore gAudioStore; secure_store::NvsAudioProfileStore gAudioStore;
audio::AudioService gAudioService(gAdau1701Dsp, &gAudioStore); audio::AudioService gAudioService(gAdau1701Dsp, &gAudioStore);
bt1035::Bt1035Driver gBt1035(
bt1035::Bt1035Pins{
.uartTx = board::pins::Bt1035UartTx,
.uartRx = board::pins::Bt1035UartRx,
.rtsGpio = board::pins::Bt1035Rts,
.ctsGpio = board::pins::Bt1035Cts,
.resetGpio = board::pins::Bt1035Reset,
.sysCtlGpio = board::pins::Bt1035SysCtl,
});
bool gReady = false; bool gReady = false;
} // namespace } // namespace
@@ -84,6 +95,11 @@ std::expected<void, HardwareBootError> HardwareBootstrap::boot()
ESP_LOGW(kTag, "ADAU1701 profile apply failed"); ESP_LOGW(kTag, "ADAU1701 profile apply failed");
} }
if (auto btResult = gBt1035.boot(); !btResult) {
ESP_LOGE(kTag, "BT1035 boot failed");
return std::unexpected(HardwareBootError::Bt1035BootFailed);
}
gReady = true; gReady = true;
ESP_LOGI(kTag, "companion chips ready"); ESP_LOGI(kTag, "companion chips ready");
return {}; return {};
+2 -1
View File
@@ -37,6 +37,7 @@ namespace hardware {
enum class HardwareBootError { enum class HardwareBootError {
Si4684BootFailed, Si4684BootFailed,
Adau1701BootFailed, Adau1701BootFailed,
Bt1035BootFailed,
}; };
/** /**
@@ -57,7 +58,7 @@ public:
* *
* @dname boot * @dname boot
* @return Ok on success, or HardwareBootError. * @return Ok on success, or HardwareBootError.
* @pubstate constructs static Si4684 and ADAU1701 drivers on first call. * @pubstate constructs static Si4684, ADAU1701, and BT1035 drivers on first call.
* *
* Fail-closed: callers must not start Wi-Fi when this returns an error. * Fail-closed: callers must not start Wi-Fi when this returns an error.
* *
+1 -1
View File
@@ -47,7 +47,7 @@ void heartbeatTask(void* arg)
*/ */
extern "C" void app_main() extern "C" void app_main()
{ {
ESP_LOGI(kTag, "DigiRadio firmware boot — Slice 5"); ESP_LOGI(kTag, "DigiRadio firmware boot — Slice 6");
auto hwResult = hardware::HardwareBootstrap::boot(); auto hwResult = hardware::HardwareBootstrap::boot();
if (!hwResult) { if (!hwResult) {