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
@@ -23,6 +23,7 @@
#include "net/NetError.hpp"
#include "net/NetState.hpp"
#include "net/SetupWebServer.hpp"
#include "net/SigmaStudioTcpServer.hpp"
#include "net/SoftApHost.hpp"
#include "net/StaClient.hpp"
@@ -53,6 +54,10 @@ namespace tuner {
class TunerService;
} // namespace tuner
namespace webradio {
class WebRadioService;
} // namespace webradio
namespace net {
/**
@@ -60,8 +65,9 @@ namespace net {
*
* @dname NetBootstrap
* @return n/a (type)
* @pubstate Owns optional softAp_, optional sta_, and webServer_. Must
* outlive app_main; keep one instance alive for process lifetime.
* @pubstate Owns optional softAp_, optional sta_, webServer_, and
* sigmaStudio_. Must outlive app_main; keep one instance alive
* for process lifetime.
*
* @author Michele Bigi
* @date 2026-07-06
@@ -79,6 +85,7 @@ public:
* @param stations Station preset service for REST routes.
* @param integration Application orchestration for preset recall.
* @param ota Firmware OTA service for POST /api/system/ota.
* @param webRadio Streaming config for GET/POST /api/streaming.
* @param companionChips Boot flags exposed on GET /api/health.
* @param deviceIdentity EEPROM-derived SSID, hostname, and serial.
* @return NetBootstrap on success, or a NetError.
@@ -93,6 +100,7 @@ public:
station::StationService& stations,
integration::IntegrationService& integration,
ota::OtaService& ota,
webradio::WebRadioService& webRadio,
core::CompanionChipStatus companionChips,
const core::DeviceIdentity& deviceIdentity);
@@ -106,12 +114,14 @@ public:
* @param softAp Optional SoftAP mode host.
* @param sta Optional STA client instance.
* @param webServer HTTP server instance.
* @param sigmaStudio SigmaStudio TCP:8086 bridge instance.
* @param state Initial network state.
* @pubstate Transfers ownership of optional network resources.
*/
NetBootstrap(std::optional<SoftApHost> softAp,
std::optional<StaClient> sta,
SetupWebServer webServer,
SigmaStudioTcpServer sigmaStudio,
NetState state);
/**
@@ -119,7 +129,7 @@ public:
*
* @dname NetBootstrap
* @param other Source instance; left empty after the move.
* @pubstate transfers softAp_, sta_, and webServer_ from other.
* @pubstate transfers softAp_, sta_, webServer_, and sigmaStudio_ from other.
*
* @author Michele Bigi
* @date 2026-07-06
@@ -132,7 +142,7 @@ public:
* @dname operator=
* @param other Source instance; left empty after the move.
* @return Reference to this instance.
* @pubstate transfers softAp_, sta_, and webServer_ from other.
* @pubstate transfers softAp_, sta_, webServer_, and sigmaStudio_ from other.
*
* @author Michele Bigi
* @date 2026-07-06
@@ -143,7 +153,7 @@ public:
* @brief ~NetBootstrap — tear down network subsystems.
*
* @dname ~NetBootstrap
* @pubstate destroys softAp_, sta_, and webServer_.
* @pubstate destroys softAp_, sta_, webServer_, and sigmaStudio_.
*
* @author Michele Bigi
* @date 2026-07-06
@@ -166,6 +176,7 @@ private:
std::optional<SoftApHost> softAp_;
std::optional<StaClient> sta_;
SetupWebServer webServer_;
SigmaStudioTcpServer sigmaStudio_;
NetState state_;
};
@@ -37,10 +37,12 @@ enum class NetError {
WifiConfigFailed,
WifiStartFailed,
HttpServerStartFailed,
TcpServerStartFailed,
StaConnectTimeout,
StaConnectFailed,
StoreSaveFailed,
CredentialsNotFound,
WifiScanFailed,
};
} // namespace net
@@ -50,6 +50,10 @@ namespace tuner {
class TunerService;
} // namespace tuner
namespace webradio {
class WebRadioService;
} // namespace webradio
struct httpd_handle;
namespace net {
@@ -73,6 +77,7 @@ struct HttpRouteContext {
station::StationService* stations; ///< Preset list REST routes.
integration::IntegrationService* integration; ///< Preset recall orchestration.
ota::OtaService* ota; ///< Firmware OTA streaming.
webradio::WebRadioService* webRadio; ///< Streaming config REST routes.
core::CompanionChipStatus companionChips; ///< Boot flags for /api/health.
core::DeviceIdentity deviceIdentity; ///< EEPROM-derived unit identity.
};
@@ -152,6 +157,7 @@ public:
* @param stations Station preset service for list REST routes.
* @param integration Application orchestration for preset recall.
* @param ota Firmware OTA service for POST /api/system/ota.
* @param webRadio Streaming config for GET/POST /api/streaming.
* @param companionChips Boot flags for GET /api/health.
* @param deviceIdentity Unit identity for /api/health serialNumber.
* @return Ok on success, or NetError::HttpServerStartFailed.
@@ -167,6 +173,7 @@ public:
station::StationService& stations,
integration::IntegrationService& integration,
ota::OtaService& ota,
webradio::WebRadioService& webRadio,
core::CompanionChipStatus companionChips,
const core::DeviceIdentity& deviceIdentity);
@@ -179,7 +186,6 @@ private:
bluetooth::BluetoothService* bluetooth_;
station::StationService* stations_;
integration::IntegrationService* integration_;
HttpRouteContext routeContext_;
};
} // namespace net
@@ -0,0 +1,133 @@
/**
* @file SigmaStudioTcpServer.hpp
* @brief TCP bridge exposing the ADAU1701 to SigmaStudio's Remote Connection.
*
* 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
*
* Implements the subset of SigmaStudio's TCPi wire protocol needed for
* Connect, Link Compile Download, register read/write, and runtime
* safeload parameter updates, ported from the ADAU1701-TCPi-ESP32
* reference project (https://github.com/rarranzb/ADAU1701-TCPi-ESP32,
* MIT) onto DigiRadio's existing I2C plumbing
* (components/drivers/adau1701/src/SigmaStudioFW.c). A completed Link
* Compile Download is persisted to the `dsp` flash partition via
* adau1701::FlashDspProgramSource::storeBlob(), so it becomes the
* program DigiRadio boots with next time — same mechanism as
* POST /api/dsp/program.
*
* @author Michele Bigi
* @date 2026-08-07
*/
#pragma once
#include "net/NetError.hpp"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include <expected>
namespace net {
/**
* @brief SigmaStudioTcpServer — port 8086 bridge to the ADAU1701.
*
* @dname SigmaStudioTcpServer
* @return n/a (type)
* @pubstate Owns a listening socket and a FreeRTOS task for the process
* lifetime once started(); stop()/destructor tear both down.
* Requires adau1701::Adau1701Driver::boot() to have already
* bound the I2C device handle via sigma_studio_set_device().
* Only one instance may be started at a time: the accept task
* reads the listen fd from a process-lifetime singleton (see
* activeListenFd() in the .cpp) rather than a captured `this`,
* since instances are constructed as locals and move-relocated
* into NetBootstrap — same rationale as SetupWebServer's
* routeContextStorage().
*
* @author Michele Bigi
* @date 2026-08-07
*/
class SigmaStudioTcpServer {
public:
/**
* @brief SigmaStudioTcpServer — construct an unstarted server.
*
* @dname SigmaStudioTcpServer
* @pubstate clears listenFd_ and task_.
*
* @author Michele Bigi
* @date 2026-08-07
*/
SigmaStudioTcpServer();
/**
* @brief ~SigmaStudioTcpServer — stop the server if running.
*
* @dname ~SigmaStudioTcpServer
* @pubstate deletes task_ and closes listenFd_ when started.
*
* @author Michele Bigi
* @date 2026-08-07
*/
~SigmaStudioTcpServer();
SigmaStudioTcpServer(const SigmaStudioTcpServer&) = delete;
SigmaStudioTcpServer& operator=(const SigmaStudioTcpServer&) = delete;
/**
* @brief SigmaStudioTcpServer — move-construct, transferring ownership.
*
* @dname SigmaStudioTcpServer
* @param other Source server; left stopped after the move.
* @pubstate takes ownership of other.listenFd_ and other.task_.
*
* @author Michele Bigi
* @date 2026-08-07
*/
SigmaStudioTcpServer(SigmaStudioTcpServer&& other) noexcept;
/**
* @brief operator= — move-assign, transferring ownership.
*
* @dname operator=
* @param other Source server; left stopped after the move.
* @return Reference to this instance.
* @pubstate takes ownership of other.listenFd_ and other.task_.
*
* @author Michele Bigi
* @date 2026-08-07
*/
SigmaStudioTcpServer& operator=(SigmaStudioTcpServer&& other) noexcept;
/**
* @brief start — open the listening socket and spawn the server task.
*
* @dname start
* @return Ok on success, or NetError::TcpServerStartFailed.
* @pubstate writes listenFd_ and task_ on success.
*
* @author Michele Bigi
* @date 2026-08-07
*/
[[nodiscard]] std::expected<void, NetError> start();
private:
void stop() noexcept;
/** @brief FreeRTOS task entry: accepts and serves one client at a time. */
static void acceptLoopTask(void* arg);
int listenFd_;
TaskHandle_t task_;
};
} // namespace net
@@ -20,6 +20,8 @@
#include "core/WifiCredentials.hpp"
#include "net/NetError.hpp"
#include "esp_event.h"
#include <expected>
#include <string_view>
@@ -107,6 +109,8 @@ public:
private:
bool connected_;
esp_event_handler_instance_t wifiHandler_;
esp_event_handler_instance_t ipHandler_;
};
} // namespace net
@@ -0,0 +1,50 @@
/**
* @file WifiScanner.hpp
* @brief Blocking Wi-Fi scan wrapper for the setup web UI.
*
* DigiRadio firmware — https://github.com/manvalan/DigiRadio
*
* Copyright 2026 Michele Bigi
* SPDX-License-Identifier: Apache-2.0
*
* @author Michele Bigi
* @date 2026-08-05
*/
#pragma once
#include "core/WifiScannedNetwork.hpp"
#include "net/NetError.hpp"
#include <expected>
#include <vector>
namespace net {
/**
* @brief WifiScanner — runs esp_wifi scan and maps results to core DTOs.
*
* @dname WifiScanner
* @return n/a (type)
* @pubstate Stateless shell helper; assumes esp_wifi_init() already ran.
*
* @author Michele Bigi
* @date 2026-08-05
*/
class WifiScanner {
public:
/**
* @brief scanNearby — list visible access points sorted by RSSI.
*
* @dname scanNearby
* @return Deduped networks on success, or NetError::WifiScanFailed.
* @pubstate Temporarily switches AP-only mode to APSTA when required.
*
* @author Michele Bigi
* @date 2026-08-05
*/
[[nodiscard]] static std::expected<std::vector<core::WifiScannedNetwork>,
NetError>
scanNearby();
};
} // namespace net