From d49b2011dd571ce8312d28164223e26ad443db3b Mon Sep 17 00:00:00 2001 From: Michele Bigi Date: Mon, 6 Jul 2026 17:07:37 +0200 Subject: [PATCH] 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 --- Software/components/core/CMakeLists.txt | 1 + .../components/core/include/core/Bt1035At.hpp | 99 ++++++++ Software/components/core/src/Bt1035At.cpp | 68 +++++ Software/components/core/test/CMakeLists.txt | 5 + .../components/core/test/bt1035_at_test.cpp | 71 ++++++ .../components/drivers/bt1035/CMakeLists.txt | 3 +- .../bt1035/include/bt1035/Bt1035Driver.hpp | 134 ++++++++++ .../bt1035/include/bt1035/Bt1035Error.hpp | 36 +++ .../drivers/bt1035/src/Bt1035Driver.cpp | 193 ++++++++++++++ .../drivers/bt1035/src/component_stub.cpp | 34 --- Software/docs/manual/ch-api.tex | 4 +- Software/docs/manual/ch-bt1035.tex | 239 ++++++++++++++++++ Software/docs/manual/ch-classes.tex | 8 +- Software/docs/manual/ch-firmware.tex | 12 +- Software/docs/manual/ch-hardware.tex | 3 +- Software/docs/manual/ch-intro.tex | 3 +- Software/docs/manual/ch-si4684.tex | 90 ++++++- Software/docs/manual/manual.tex | 1 + Software/main/CMakeLists.txt | 2 +- Software/main/hardware_bootstrap.cpp | 16 ++ Software/main/hardware_bootstrap.hpp | 3 +- Software/main/main.cpp | 2 +- 22 files changed, 979 insertions(+), 48 deletions(-) create mode 100644 Software/components/core/include/core/Bt1035At.hpp create mode 100644 Software/components/core/src/Bt1035At.cpp create mode 100644 Software/components/core/test/bt1035_at_test.cpp create mode 100644 Software/components/drivers/bt1035/include/bt1035/Bt1035Driver.hpp create mode 100644 Software/components/drivers/bt1035/include/bt1035/Bt1035Error.hpp create mode 100644 Software/components/drivers/bt1035/src/Bt1035Driver.cpp delete mode 100644 Software/components/drivers/bt1035/src/component_stub.cpp create mode 100644 Software/docs/manual/ch-bt1035.tex diff --git a/Software/components/core/CMakeLists.txt b/Software/components/core/CMakeLists.txt index 1794cec..219a11e 100644 --- a/Software/components/core/CMakeLists.txt +++ b/Software/components/core/CMakeLists.txt @@ -22,6 +22,7 @@ idf_component_register( "src/EnhancementsDesign.cpp" "src/AudioProfile.cpp" "src/AudioProfileJson.cpp" + "src/Bt1035At.cpp" INCLUDE_DIRS "include" ) diff --git a/Software/components/core/include/core/Bt1035At.hpp b/Software/components/core/include/core/Bt1035At.hpp new file mode 100644 index 0000000..bac0797 --- /dev/null +++ b/Software/components/core/include/core/Bt1035At.hpp @@ -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 +#include +#include +#include +#include + +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 +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 diff --git a/Software/components/core/src/Bt1035At.cpp b/Software/components/core/src/Bt1035At.cpp new file mode 100644 index 0000000..d303db9 --- /dev/null +++ b/Software/components/core/src/Bt1035At.cpp @@ -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 bootInitSequence() noexcept +{ + return std::array{ + 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 diff --git a/Software/components/core/test/CMakeLists.txt b/Software/components/core/test/CMakeLists.txt index dd1406a..34074ab 100644 --- a/Software/components/core/test/CMakeLists.txt +++ b/Software/components/core/test/CMakeLists.txt @@ -34,6 +34,7 @@ add_library(digiradio_core STATIC "${CORE_SRC_DIR}/EnhancementsDesign.cpp" "${CORE_SRC_DIR}/AudioProfile.cpp" "${CORE_SRC_DIR}/AudioProfileJson.cpp" + "${CORE_SRC_DIR}/Bt1035At.cpp" ) target_include_directories(digiradio_core PUBLIC "${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) target_link_libraries(enhancements_design_test PRIVATE digiradio_core) 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) diff --git a/Software/components/core/test/bt1035_at_test.cpp b/Software/components/core/test/bt1035_at_test.cpp new file mode 100644 index 0000000..ac17a40 --- /dev/null +++ b/Software/components/core/test/bt1035_at_test.cpp @@ -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 +#include +#include + +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; +} diff --git a/Software/components/drivers/bt1035/CMakeLists.txt b/Software/components/drivers/bt1035/CMakeLists.txt index 6bf8c99..e423c24 100644 --- a/Software/components/drivers/bt1035/CMakeLists.txt +++ b/Software/components/drivers/bt1035/CMakeLists.txt @@ -1,6 +1,7 @@ idf_component_register( - SRCS "src/component_stub.cpp" + SRCS "src/Bt1035Driver.cpp" INCLUDE_DIRS "include" + REQUIRES core driver esp_driver_gpio esp_driver_uart ) target_compile_features(${COMPONENT_LIB} PUBLIC cxx_std_23) diff --git a/Software/components/drivers/bt1035/include/bt1035/Bt1035Driver.hpp b/Software/components/drivers/bt1035/include/bt1035/Bt1035Driver.hpp new file mode 100644 index 0000000..d742ccc --- /dev/null +++ b/Software/components/drivers/bt1035/include/bt1035/Bt1035Driver.hpp @@ -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 + +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 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 sendCommand( + core::Bt1035AtCommand command); + +private: + [[nodiscard]] std::expected ensureBooted() const; + [[nodiscard]] std::expected runInitSequence(); + [[nodiscard]] std::expected transmitAndExpectOk( + std::string_view commandLine); + + Bt1035Pins pins_; + bool booted_; + bool uartInstalled_; + int uartPort_; +}; + +} // namespace bt1035 diff --git a/Software/components/drivers/bt1035/include/bt1035/Bt1035Error.hpp b/Software/components/drivers/bt1035/include/bt1035/Bt1035Error.hpp new file mode 100644 index 0000000..fc0e894 --- /dev/null +++ b/Software/components/drivers/bt1035/include/bt1035/Bt1035Error.hpp @@ -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 diff --git a/Software/components/drivers/bt1035/src/Bt1035Driver.cpp b/Software/components/drivers/bt1035/src/Bt1035Driver.cpp new file mode 100644 index 0000000..d7e05e7 --- /dev/null +++ b/Software/components/drivers/bt1035/src/Bt1035Driver.cpp @@ -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 +#include + +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(uartPort_)); + uartInstalled_ = false; + } +} + +bool Bt1035Driver::isBooted() const noexcept +{ + return booted_; +} + +std::expected Bt1035Driver::ensureBooted() const +{ + if (!booted_) { + return std::unexpected(Bt1035Error::NotBooted); + } + return {}; +} + +std::expected Bt1035Driver::transmitAndExpectOk( + std::string_view commandLine) +{ + const int written = uart_write_bytes(static_cast(uartPort_), + commandLine.data(), + commandLine.size()); + if (written < 0 + || static_cast(written) != commandLine.size()) { + return std::unexpected(Bt1035Error::UartInitFailed); + } + + std::array buffer{}; + std::string accumulated; + const TickType_t deadline = + xTaskGetTickCount() + pdMS_TO_TICKS(kResponseTimeoutMs); + + while (xTaskGetTickCount() < deadline) { + const int received = uart_read_bytes(static_cast(uartPort_), + buffer.data(), buffer.size(), + pdMS_TO_TICKS(50)); + if (received > 0) { + accumulated.append(buffer.data(), static_cast(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 Bt1035Driver::sendCommand( + core::Bt1035AtCommand command) +{ + if (auto ready = ensureBooted(); !ready) { + return ready; + } + return transmitAndExpectOk(core::buildBt1035AtLine(command)); +} + +std::expected Bt1035Driver::runInitSequence() +{ + for (const core::Bt1035AtCommand command : core::bootInitSequence()) { + if (auto result = transmitAndExpectOk(core::buildBt1035AtLine(command)); + !result) { + return result; + } + } + return {}; +} + +std::expected 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(pins_.sysCtlGpio), 1); + gpio_set_level(static_cast(pins_.resetGpio), 0); + vTaskDelay(pdMS_TO_TICKS(10)); + gpio_set_level(static_cast(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(uartPort_), kUartRxBuffer, + kUartTxBuffer, 0, nullptr, 0) + != ESP_OK) { + return std::unexpected(Bt1035Error::UartInitFailed); + } + + if (uart_param_config(static_cast(uartPort_), &uartCfg) + != ESP_OK) { + return std::unexpected(Bt1035Error::UartInitFailed); + } + + if (uart_set_pin(static_cast(uartPort_), pins_.uartTx, + pins_.uartRx, pins_.rtsGpio, pins_.ctsGpio) + != ESP_OK) { + return std::unexpected(Bt1035Error::UartInitFailed); + } + + uartInstalled_ = true; + } + + uart_flush_input(static_cast(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 diff --git a/Software/components/drivers/bt1035/src/component_stub.cpp b/Software/components/drivers/bt1035/src/component_stub.cpp deleted file mode 100644 index f69f2ef..0000000 --- a/Software/components/drivers/bt1035/src/component_stub.cpp +++ /dev/null @@ -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 diff --git a/Software/docs/manual/ch-api.tex b/Software/docs/manual/ch-api.tex index 1a0ed40..adc2a26 100644 --- a/Software/docs/manual/ch-api.tex +++ b/Software/docs/manual/ch-api.tex @@ -98,7 +98,9 @@ persistence fails. Returns a tuner snapshot serialised by \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{drcode}[JSON] diff --git a/Software/docs/manual/ch-bt1035.tex b/Software/docs/manual/ch-bt1035.tex new file mode 100644 index 0000000..b500a62 --- /dev/null +++ b/Software/docs/manual/ch-bt1035.tex @@ -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}. + +\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} diff --git a/Software/docs/manual/ch-classes.tex b/Software/docs/manual/ch-classes.tex index 20240d6..db70c9a 100644 --- a/Software/docs/manual/ch-classes.tex +++ b/Software/docs/manual/ch-classes.tex @@ -130,7 +130,13 @@ fetch, and \texttt{startDabService}. Property access (\texttt{setProperty}, \texttt{setVolume}) and diagnostics (\texttt{getPartInfo}, \texttt{getSysState}) are included. Wrong-band calls return \texttt{Si4684Error::WrongBand}. 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} RAII I\textsuperscript{2}C driver for the ADAU1701 SigmaDSP diff --git a/Software/docs/manual/ch-firmware.tex b/Software/docs/manual/ch-firmware.tex index 72beff5..e398b4d 100644 --- a/Software/docs/manual/ch-firmware.tex +++ b/Software/docs/manual/ch-firmware.tex @@ -147,10 +147,11 @@ safeload registers. \end{drcaution} \paragraph{FSC-BT1035 (Bluetooth).} -The module is controlled by AT commands over UART. The initialisation -sequence includes enabling Line-In mode (\texttt{AT+AUXCFG=1}), which is -required for the wired audio path from the DSP; command responses are -parsed explicitly, with timeouts treated as errors. +The module is controlled by AT commands over UART with hardware flow control. +The initialisation sequence includes enabling Line-In mode +(\texttt{AT+AUXCFG=1}), which is required for the wired audio path from the +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} \label{sec:fw-config} @@ -203,6 +204,9 @@ bring-up: \item \textbf{Audio profile}: \texttt{audio::AudioService::loadAndApply()} restores the saved \texttt{core::AudioProfile} from NVS (or factory 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} If either driver returns an error, the firmware logs the failure and stops diff --git a/Software/docs/manual/ch-hardware.tex b/Software/docs/manual/ch-hardware.tex index b5e1905..d4d542b 100644 --- a/Software/docs/manual/ch-hardware.tex +++ b/Software/docs/manual/ch-hardware.tex @@ -131,7 +131,8 @@ in the module's own non-volatile memory. \begin{drcaution}[Line-In mode] 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 -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} \section{The host: ESP32-S3} diff --git a/Software/docs/manual/ch-intro.tex b/Software/docs/manual/ch-intro.tex index 12f8673..7beca88 100644 --- a/Software/docs/manual/ch-intro.tex +++ b/Software/docs/manual/ch-intro.tex @@ -30,7 +30,8 @@ extending DigiRadio: the hardware at a block level (Chapter~\ref{ch:hardware}), the firmware architecture (Chapter~\ref{ch:firmware}), dedicated companion-chip guides for the 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 the code (Chapter~\ref{ch:classes}), and how to build and flash (Chapter~\ref{ch:build}). Exact C++ signatures are generated by Doxygen diff --git a/Software/docs/manual/ch-si4684.tex b/Software/docs/manual/ch-si4684.tex index fc10b4d..454cbf5 100644 --- a/Software/docs/manual/ch-si4684.tex +++ b/Software/docs/manual/ch-si4684.tex @@ -224,18 +224,104 @@ after boot. \end{table} \subsection{Typical DAB session} +\label{sec:si4684-dab-session} \begin{enumerate} \item \texttt{boot(Dab)} at power-up (already done in \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 \texttt{fetchDabServiceList()} when \texttt{readDabEventStatus().serviceListReady}. \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} +\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} \label{sec:si4684-integration} diff --git a/Software/docs/manual/manual.tex b/Software/docs/manual/manual.tex index cde6f13..da66b00 100644 --- a/Software/docs/manual/manual.tex +++ b/Software/docs/manual/manual.tex @@ -48,6 +48,7 @@ \include{ch-firmware} \include{ch-si4684} \include{ch-adau1701} +\include{ch-bt1035} \include{ch-api} \include{ch-classes} \include{ch-build} diff --git a/Software/main/CMakeLists.txt b/Software/main/CMakeLists.txt index efa14df..ead264f 100644 --- a/Software/main/CMakeLists.txt +++ b/Software/main/CMakeLists.txt @@ -3,5 +3,5 @@ idf_component_register( "main.cpp" "hardware_bootstrap.cpp" INCLUDE_DIRS "." - REQUIRES core net secure_store adau1701 si4684 tuner audio + REQUIRES core net secure_store adau1701 si4684 tuner audio bt1035 ) diff --git a/Software/main/hardware_bootstrap.cpp b/Software/main/hardware_bootstrap.cpp index 5cd5b7b..18ef927 100644 --- a/Software/main/hardware_bootstrap.cpp +++ b/Software/main/hardware_bootstrap.cpp @@ -17,6 +17,7 @@ #include "adau1701/Adau1701Dsp.hpp" #include "audio/AudioService.hpp" #include "board_pins.hpp" +#include "bt1035/Bt1035Driver.hpp" #include "secure_store/NvsAudioProfileStore.hpp" #include "si4684/Si4684Band.hpp" #include "si4684/Si4684Driver.hpp" @@ -58,6 +59,16 @@ adau1701::Adau1701Dsp gAdau1701Dsp(gAdau1701); secure_store::NvsAudioProfileStore 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; } // namespace @@ -84,6 +95,11 @@ std::expected HardwareBootstrap::boot() 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; ESP_LOGI(kTag, "companion chips ready"); return {}; diff --git a/Software/main/hardware_bootstrap.hpp b/Software/main/hardware_bootstrap.hpp index 720051d..6b19ab8 100644 --- a/Software/main/hardware_bootstrap.hpp +++ b/Software/main/hardware_bootstrap.hpp @@ -37,6 +37,7 @@ namespace hardware { enum class HardwareBootError { Si4684BootFailed, Adau1701BootFailed, + Bt1035BootFailed, }; /** @@ -57,7 +58,7 @@ public: * * @dname boot * @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. * diff --git a/Software/main/main.cpp b/Software/main/main.cpp index c285eb9..e0a149c 100644 --- a/Software/main/main.cpp +++ b/Software/main/main.cpp @@ -47,7 +47,7 @@ void heartbeatTask(void* arg) */ 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(); if (!hwResult) {