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
+26 -7
View File
@@ -27,6 +27,7 @@
#include "secure_store/NvsPlatformInit.hpp"
#include "station/StationService.hpp"
#include "tuner/TunerService.hpp"
#include "webradio/WebRadioService.hpp"
namespace net {
@@ -102,10 +103,12 @@ startSetupMode(core::ISecureStore& store, tuner::TunerService& tuner,
station::StationService& stations,
integration::IntegrationService& integration,
ota::OtaService& ota,
webradio::WebRadioService& webRadio,
core::CompanionChipStatus companionChips,
const core::DeviceIdentity& deviceIdentity)
{
esp_netif_create_default_wifi_ap();
esp_netif_create_default_wifi_sta();
SoftApHost softAp(SoftApConfig::forSsid(deviceIdentity.softApSsid()));
if (auto apResult = softAp.start(); !apResult) {
@@ -115,17 +118,23 @@ startSetupMode(core::ISecureStore& store, tuner::TunerService& tuner,
SetupWebServer webServer;
if (auto webResult =
webServer.start(store, NetState::SoftApSetup, tuner, audio,
bluetooth, stations, integration, ota,
bluetooth, stations, integration, ota, webRadio,
companionChips, deviceIdentity);
!webResult) {
return std::unexpected(webResult.error());
}
SigmaStudioTcpServer sigmaStudio;
if (auto sigmaResult = sigmaStudio.start(); !sigmaResult) {
ESP_LOGW(kTag, "SigmaStudio TCP bridge failed to start — continuing "
"without it");
}
ESP_LOGI(kTag, "setup mode ready — SSID %.*s",
static_cast<int>(deviceIdentity.softApSsid().size()),
deviceIdentity.softApSsid().data());
return NetBootstrap(std::move(softAp), std::nullopt, std::move(webServer),
NetState::SoftApSetup);
std::move(sigmaStudio), NetState::SoftApSetup);
}
/**
@@ -146,6 +155,7 @@ startStaMode(core::ISecureStore& store, tuner::TunerService& tuner,
station::StationService& stations,
integration::IntegrationService& integration,
ota::OtaService& ota,
webradio::WebRadioService& webRadio,
core::CompanionChipStatus companionChips,
const core::DeviceIdentity& deviceIdentity)
{
@@ -175,17 +185,23 @@ startStaMode(core::ISecureStore& store, tuner::TunerService& tuner,
SetupWebServer webServer;
if (auto webResult =
webServer.start(store, NetState::StaConnected, tuner, audio,
bluetooth, stations, integration, ota,
bluetooth, stations, integration, ota, webRadio,
companionChips, deviceIdentity);
!webResult) {
return std::unexpected(webResult.error());
}
SigmaStudioTcpServer sigmaStudio;
if (auto sigmaResult = sigmaStudio.start(); !sigmaResult) {
ESP_LOGW(kTag, "SigmaStudio TCP bridge failed to start — continuing "
"without it");
}
ESP_LOGI(kTag, "STA mode ready — hostname %.*s.local",
static_cast<int>(deviceIdentity.hostname().size()),
deviceIdentity.hostname().data());
return NetBootstrap(std::nullopt, std::move(sta), std::move(webServer),
NetState::StaConnected);
std::move(sigmaStudio), NetState::StaConnected);
}
} // namespace
@@ -197,6 +213,7 @@ NetBootstrap::start(core::ISecureStore& store, tuner::TunerService& tuner,
station::StationService& stations,
integration::IntegrationService& integration,
ota::OtaService& ota,
webradio::WebRadioService& webRadio,
core::CompanionChipStatus companionChips,
const core::DeviceIdentity& deviceIdentity)
{
@@ -210,8 +227,8 @@ NetBootstrap::start(core::ISecureStore& store, tuner::TunerService& tuner,
if (store.hasWifiCredentials()) {
auto staResult = startStaMode(store, tuner, audio, bluetooth, stations,
integration, ota, companionChips,
deviceIdentity);
integration, ota, webRadio,
companionChips, deviceIdentity);
if (staResult) {
return staResult;
}
@@ -223,16 +240,18 @@ NetBootstrap::start(core::ISecureStore& store, tuner::TunerService& tuner,
}
return startSetupMode(store, tuner, audio, bluetooth, stations, integration,
ota, companionChips, deviceIdentity);
ota, webRadio, companionChips, deviceIdentity);
}
NetBootstrap::NetBootstrap(std::optional<SoftApHost> softAp,
std::optional<StaClient> sta,
SetupWebServer webServer,
SigmaStudioTcpServer sigmaStudio,
NetState state)
: softAp_(std::move(softAp))
, sta_(std::move(sta))
, webServer_(std::move(webServer))
, sigmaStudio_(std::move(sigmaStudio))
, state_(state)
{
}