Add internet radio streaming with runtime API, modernize web UI, remove auto-tune/beep at boot

Streaming (main feature this session):
- New WebRadioConfig/WebRadioJson core types, ISecureStore-backed persistence
- New webradio::WebRadioService (thread-safe live config) + GET/POST /api/streaming
- web_radio_stream task now runtime-toggleable (no reboot), no hardcoded URL
- Content-Type diagnostic: warns clearly when a URL is a webpage, not an audio stream

Boot cleanup:
- Removed boot-time auto FM/DAB tune, auto-beep, and the (now-concluded) Si4684
  crystal IBIAS/CTUN empirical sweep from main.cpp — tuning/beep are on-demand
  via the existing REST API only

Web UI:
- Modernized styling (cards, gradients, toggle switches, light/dark theme)
- New Stream tab wired to /api/streaming

Fixes found via real idf.py build (not just clangd):
- Restored wrongly-removed si4684/Si4684Tuner.hpp include in main.cpp
- Fixed MP3Decode() argument types in web_radio_stream.cpp (unsigned char**/int*)

Quality-gate fixes:
- Host-test stub headers (esp_log.h, freertos/*) so TunerService.cpp's
  scanForStation logging/pacing compiles for station_service_test /
  integration_service_test instead of running stale binaries
- Added WifiScanner and WebRadioService manual sections; filled in missing
  Doxygen docs on BluetoothService, i2s_sdata_probe, test_firmware, Bt1035At
- Ignore clangd's .cache/ index directory

Also includes prior uncommitted work carried in the tree: Wi-Fi/Bluetooth
device scan REST API and UI (WifiScanner, BT scan), SigmaStudio TCP bridge,
and the current ADAU1701 SigmaStudio DSP program export.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-08 21:15:22 +02:00
co-authored by Claude Sonnet 5
parent 8a8523515d
commit 6f7b6dd12c
131 changed files with 20309 additions and 1702 deletions
@@ -16,6 +16,7 @@
#include "core/Bt1035At.hpp"
#include <cstdint>
#include <expected>
#include <string>
#include <string_view>
@@ -36,8 +37,6 @@ namespace bt1035 {
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).
};
@@ -48,7 +47,7 @@ struct Bt1035Pins {
* @dname Bt1035Driver
* @return n/a (type)
* @pubstate Owns UART port after boot(). booted_ true after init sequence
* including AT+AUXCFG=3 and AT+I2SCFG=67 (I2S slave from ADAU1701).
* including AT+AUXCFG=3 and AT+I2SCFG=35 (I2S slave from ADAU1701).
*
* @author Michele Bigi
* @date 2026-07-06
@@ -159,6 +158,22 @@ public:
[[nodiscard]] std::expected<core::Bt1035A2dpState, Bt1035Error>
queryA2dpState();
/**
* @brief queryA2dpEncoder — read negotiated A2DP codec (AT+A2DPENC).
*
* @dname queryA2dpEncoder
* @return Parsed codec on success, or Bt1035Error. Failing here while
* queryA2dpState() reports Streaming means the module has a
* link but is not actually encoding audio — a real fault,
* not just a quiet source.
* @pubstate writes UART; parses +A2DPENC from the reply.
*
* @author Michele Bigi
* @date 2026-08-07
*/
[[nodiscard]] std::expected<core::Bt1035A2dpCodec, Bt1035Error>
queryA2dpEncoder();
/**
* @brief disconnectA2dp — release the active A2DP session (AT+A2DPDISC).
*
@@ -236,15 +251,115 @@ public:
[[nodiscard]] std::expected<std::vector<core::Bt1035PairedDevice>, Bt1035Error>
queryPairedList();
/**
* @brief scanNearbyBrEdr — inquiry for nearby A2DP sink devices.
*
* @dname scanNearbyBrEdr
* @param scanSeconds BR/EDR scan duration 1255 (default 20 in service).
* @return Parsed +SCAN entries, or Bt1035Error.
* @pubstate sends AT+SCAN=1,n and collects until +SCAN=E.
*
* @author Michele Bigi
* @date 2026-08-05
*/
[[nodiscard]] std::expected<std::vector<core::Bt1035ScannedDevice>, Bt1035Error>
scanNearbyBrEdr(std::uint8_t scanSeconds = 20U);
/**
* @brief stopScan — abort an active AT+SCAN inquiry.
*
* @dname stopScan
* @return Ok on success, or Bt1035Error.
* @pubstate sends AT+SCAN=0.
*
* @author Michele Bigi
* @date 2026-08-05
*/
[[nodiscard]] std::expected<void, Bt1035Error> stopScan();
/**
* @brief prepareForOutgoingConnect — disable auto-link before A2DPCONN.
*
* @dname prepareForOutgoingConnect
* @pubstate sends LINKCFG/AUTOCONN off; does not clear PLIST.
*
* @author Michele Bigi
* @date 2026-08-05
*/
void prepareForOutgoingConnect();
/**
* @brief waitForA2dpConnected — poll until A2DP link is up.
*
* @dname waitForA2dpConnected
* @param timeoutMs Maximum wait in milliseconds.
* @return true when Connected/Streaming/Paused, false on timeout.
* @pubstate polls AT+A2DPSTAT.
*
* @author Michele Bigi
* @date 2026-08-05
*/
[[nodiscard]] bool waitForA2dpConnected(int timeoutMs);
/**
* @brief startA2dpAudio — send AT+A2DPAUDIO=1 (Feasycom §5.3.6).
*
* @dname startA2dpAudio
* @return Ok on module OK, or Bt1035Error.
* @pubstate writes UART.
*
* @author Michele Bigi
* @date 2026-08-05
*/
[[nodiscard]] std::expected<void, Bt1035Error> startA2dpAudio();
/**
* @brief waitForA2dpStreaming — poll until +A2DPSTAT=4 (Streaming).
*
* @dname waitForA2dpStreaming
* @param timeoutMs Maximum wait in milliseconds.
* @return true when Streaming, false on timeout.
* @pubstate polls AT+A2DPSTAT; sends A2DPAUDIO=1 when stuck at Connected.
*
* @author Michele Bigi
* @date 2026-08-05
*/
[[nodiscard]] bool waitForA2dpStreaming(int timeoutMs);
/**
* @brief connectA2dp — pair/connect to a remote by MAC (AT+A2DPCONN).
*
* @dname connectA2dp
* @param mac 12-char ASCII MAC from scan results.
* @return Ok on success, or Bt1035Error.
* @pubstate may take several seconds while the module pairs.
*
* @author Michele Bigi
* @date 2026-08-05
*/
[[nodiscard]] std::expected<void, Bt1035Error> connectA2dp(
std::string_view mac);
private:
[[nodiscard]] std::expected<void, Bt1035Error> ensureBooted() const;
[[nodiscard]] std::expected<void, Bt1035Error> runInitSequence();
[[nodiscard]] std::expected<std::string, Bt1035Error> transmitAndCollect(
std::string_view commandLine, int timeoutMs = kResponseTimeoutMs);
[[nodiscard]] std::expected<std::string, Bt1035Error> transmitAndCollectUntil(
std::string_view commandLine, std::string_view endMarker, int timeoutMs,
std::uint8_t minScanSeconds = 0U);
[[nodiscard]] std::expected<void, Bt1035Error> transmitAndExpectOk(
std::string_view commandLine);
[[nodiscard]] std::expected<void, Bt1035Error> transmitAndExpectOkLogged(
std::string_view label, std::string_view commandLine);
void prepareForInquiryScan();
[[nodiscard]] bool waitForA2dpIdle(int timeoutMs);
[[nodiscard]] std::expected<std::vector<core::Bt1035ScannedDevice>, Bt1035Error>
runInquiryScan(std::uint8_t scanType, std::uint8_t scanSeconds);
static constexpr int kResponseTimeoutMs = 2000;
static constexpr int kConnectTimeoutMs = 15000;
Bt1035Pins pins_;
bool booted_;