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:
@@ -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
|
||||||
@@ -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
|
|
||||||
@@ -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]
|
||||||
|
|||||||
@@ -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}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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 {};
|
||||||
|
|||||||
@@ -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.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user