Files
micheleandClaude Sonnet 5 6f7b6dd12c 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>
2026-08-08 21:15:22 +02:00

261 lines
12 KiB
TeX

% ============================================================
% DigiRadio — Manual section: Firmware Architecture
% Drop this into the manual as a \section (or \chapter).
%
% Required packages (add to the preamble if not present):
% \usepackage{tikz}
% \usetikzlibrary{positioning, arrows.meta, fit, backgrounds}
% \usepackage{booktabs} % for the interface table
%
% The environments used here are standard. Replace itemize / framed
% blocks with your own box taxonomy where you prefer.
% ============================================================
\chapter{Firmware Architecture}
\label{ch:firmware}
\label{sec:firmware-architecture}
The DigiRadio firmware runs on the ESP32-S3 and orchestrates three
companion chips into a single high-fidelity audio path: the Si4684
DAB+/FM tuner, the ADAU1701 SigmaDSP, and the FSC-BT1035 Bluetooth
transmitter. This section describes how the firmware is organised, how
audio flows through the system, and how each chip is controlled. The
day-to-day coding conventions (style, documentation, review rules) are
intentionally kept out of this manual and live in the repository's
\texttt{CONTRIBUTING.md} and \texttt{AGENTS.md}.
\section{Design principles}
\label{sec:fw-principles}
Two ideas shape the whole codebase:
\begin{itemize}
\item \textbf{Strong typing.} Domain quantities (frequency, gain,
station identifier, band) are dedicated types rather than raw
integers or floats. Invalid states are made unrepresentable: input
from the network, UART, or flash is validated once at the boundary
into a domain type, and trusted thereafter.
\item \textbf{Functional core, imperative shell.} All hardware-free
logic --- coefficient math, boot-image framing, configuration
parsing, station-list operations --- lives in a pure \emph{core}
that compiles and is unit-tested on the host, with no dependency on
ESP-IDF. Every access to the hardware (I\textsuperscript{2}C, SPI,
UART, flash) lives in a thin \emph{shell} around that core. This
keeps the logic testable without a board attached.
\end{itemize}
\section{Layered structure}
\label{sec:fw-layers}
The firmware is organised in three layers, with dependencies pointing
inward only (Figure~\ref{fig:fw-layers}). The outer shell may depend on
the services and the core; the pure core depends on nothing outside
itself.
\begin{figure}[htbp]
\centering
\begin{tikzpicture}[
layer/.style={draw, rounded corners, minimum width=10cm,
minimum height=1.5cm, align=center, font=\small},
node distance=4mm]
\node[layer, fill=black!5] (shell) {%
\textbf{Imperative shell}\\[2pt]
chip drivers \textbullet\ web server \textbullet\ NVS \textbullet\
FreeRTOS tasks \textbullet\ ISRs};
\node[layer, fill=black!8, below=of shell] (services) {%
\textbf{Application services}\\[2pt]
TunerService \textbullet\ AudioService \textbullet\
StationService \textbullet\ BluetoothService \textbullet\
IntegrationService};
\node[layer, fill=black!12, below=of services] (core) {%
\textbf{Domain core (pure, host-tested)}\\[2pt]
Station \textbullet\ Frequency \textbullet\ EqProfile \textbullet\
MixerState \textbullet\ Credential \textbullet\ validation};
\draw[-{Latex}] (shell) -- (services);
\draw[-{Latex}] (services) -- (core);
\end{tikzpicture}
\caption{Firmware layering. Dependencies point inward; the pure core
has no hardware dependencies.}
\label{fig:fw-layers}
\end{figure}
Application services take driver \emph{interfaces}
(\texttt{ITuner}, \texttt{IDsp}, \texttt{IBtModule},
\texttt{ISecureStore}) injected at construction. Because the services
depend on abstractions rather than concrete hardware, the same logic can
be exercised against fakes in host tests and against real silicon on the
device.
\section{Audio signal path}
\label{sec:fw-audio}
The audio chain is built around sound quality. The Si4684 produces the
DAB+/FM audio stream; the ADAU1701 applies equalisation and mixes it
with the ESP32 audio source; the FSC-BT1035 streams the result over
Bluetooth with aptX Adaptive (Figure~\ref{fig:fw-audio}).
\begin{figure}[htbp]
\centering
\begin{tikzpicture}[
block/.style={draw, rounded corners, minimum height=1cm,
minimum width=2.4cm, align=center, font=\small},
>={Latex}, node distance=8mm]
\node[block] (si) {Si4684\\\scriptsize DAB+/FM};
\node[block, right=16mm of si] (dsp) {ADAU1701\\\scriptsize EQ + mixer};
\node[block, right=16mm of dsp] (bt) {FSC-BT1035\\\scriptsize aptX Adaptive};
\node[block, below=12mm of dsp] (esp) {ESP32-S3\\\scriptsize audio src};
\draw[->] (si) -- (dsp) node[midway, above, font=\scriptsize]{audio};
\draw[->] (dsp) -- (bt) node[midway, above, font=\scriptsize]{out};
\draw[->] (esp) -- (dsp) node[midway, right, font=\scriptsize]{mix in};
\end{tikzpicture}
\caption{Audio signal path. The ADAU1701 input mixer selects/blends
the Si4684 and ESP32 sources; equalisation is applied before the
Bluetooth output stage.}
\label{fig:fw-audio}
\end{figure}
Equalisation and input mixing are exposed to the rest of the firmware as
typed operations --- setting an EQ band from a gain, centre frequency,
and Q, or setting the mix level of a given source --- never as raw DSP
cell addresses. The biquad coefficient math runs in the pure core and is
verified against reference values in host tests.
\section{Chip control and boot}
\label{sec:fw-chips}
\paragraph{Si4684 (tuner).}
At power-up the driver follows the documented boot sequence: power on,
load the patch/bootloader, load the firmware image (FM or DAB), then
boot. Firmware images are large and are streamed to the chip in bounded
chunks from flash rather than buffered whole in RAM. See
Chapter~\ref{ch:si4684} for blob sources, uGreen extraction, and the full
\texttt{Si4684Driver} API (tuning, RSQ, DAB service list).
\paragraph{ADAU1701 (DSP).}
The board carries no self-boot EEPROM. Instead, the ESP32 writes the
SigmaStudio-exported program into the DSP's RAM at \emph{every} boot.
Runtime changes to equalisation and mixing use the ADAU1701 safeload
mechanism, so parameter updates are click-free while audio is playing.
Which blocks are runtime-controllable (source faders, EQ bands 2--6,
master volume) versus fixed at export (high-pass, limiters) is listed in
Chapter~\ref{ch:sigmastudio}, Section~\ref{sec:ss-runtime}. The full driver
stack, parameter map, \texttt{AudioService}, and HTTP routes are documented
in Chapter~\ref{ch:adau1701}.
\begin{drcaution}[Safeload]
Writing DSP parameter cells directly while audio is running produces
audible clicks. All runtime EQ and mixer changes must go through the
safeload registers.
\end{drcaution}
\paragraph{FSC-BT1035 (Bluetooth).}
The module is controlled by AT commands over UART with hardware flow control.
The initialisation sequence includes enabling I\textsuperscript{2}S slave mode
(\texttt{AT+AUXCFG=3} and \texttt{AT+I2SCFG=35}), which is required for the
wired digital audio path from the ADAU1701 to the module.
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}
Network provisioning is implemented as an explicit state machine
(\texttt{net::NetState}): on first boot (or when STA join fails) the
device opens a setup SoftAP (\texttt{DigiRadio-setup}) and serves a
minimal gzipped web UI from flash. After the user submits credentials via
\texttt{POST /api/wifi}, values are validated in the pure core, persisted
through \texttt{core::ISecureStore}, and the device reboots into STA mode
on the next boot. The HTTP endpoints and JSON schemas are documented in
Chapter~\ref{ch:api}.
\paragraph{Implemented (fw 0.8.3).}
\begin{itemize}
\item \textbf{Network} --- \texttt{GET /api/health}, \texttt{POST /api/wifi},
SoftAP/STA state machine (\texttt{NetState}), tabbed gzipped web UI.
\item \textbf{Tuner} --- \texttt{/api/tuner/*} (FM/DAB tune, seek, services,
play); RDS and DAB dynamic labels in status JSON.
\item \textbf{Audio} --- \texttt{/api/audio/*} (profile, reset, stereo/bass
enhance); six-band EQ via \texttt{AudioService}.
\item \textbf{Bluetooth} --- \texttt{/api/bluetooth/*} (pair, stop, disconnect,
status).
\item \textbf{Presets} --- \texttt{/api/stations/*} including reorder and
integrated recall via \texttt{IntegrationService}.
\item \textbf{Storage} --- \texttt{NvsSecureStore}, \texttt{NvsAudioProfileStore},
encrypted NVS + flash encryption (development mode); keys in
\texttt{nvs\_keys} partition; init via \texttt{initEncryptedStorage()}.
\end{itemize}
\paragraph{Legacy note (Slices 1--2).}
The first slices introduced health, Wi-Fi provisioning, and the secure-store
boundary; the bullets above supersede the original slice-scoped list.
Sensitive data uses \texttt{core::Secret} where applicable so values cannot
be logged or implicitly converted to a string; buffers are cleared on
destruction. Firmware~0.8.3 enables NVS and flash encryption at rest via
\texttt{secure\_store::initEncryptedStorage()} (see
\texttt{docs/security-flash-nvs.md} and Chapter~\ref{sec:api-storage}).
\section{Companion-chip boot at power-up}
\label{sec:fw-chip-boot}
The ESP32-S3 loads both audio companion chips from assets under
\texttt{Software/Firmware/} during \texttt{app\_main}, before network
bring-up:
\begin{enumerate}
\item \textbf{Si4684} (SPI): ROM patch \texttt{rom\_patch\_016.bin}, then either
DAB image \texttt{dab\_firmware.bin} or FM image \texttt{fm\_firmware.bin},
following AN649 (Chapter~\ref{ch:si4684}). DAB blob default from PE5PVB; FM
from eval pack or uGreen \texttt{radio\_cli}. Blobs are local-only
(\texttt{.gitignore}); embedded via ESP-IDF \texttt{EMBED\_FILES}.
\item \textbf{ADAU1701} (I\textsuperscript{2}C): SigmaStudio export
(\texttt{DigiRadio\_IC\_1.h}) replayed through
\texttt{SIGMA\_WRITE\_REGISTER\_BLOCK} after hardware reset. The DSP
program lives in RAM only; download runs on every boot
(Chapter~\ref{ch:adau1701}, Section~\ref{sec:adau1701-boot}).
\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 I\textsuperscript{2}S slave mode (\texttt{AT+AUXCFG=3},
\texttt{AT+I2SCFG=35}) 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
before starting Wi-Fi (fail-closed bring-up).
\section{Error-handling model}
\label{sec:fw-errors}
Every operation that can fail returns a typed result
(\texttt{std::expected<T, Error>}); nothing fails silently and every
timeout is an explicit error value. Errors propagate to the layer that
can act on them --- a driver reports, a service decides (retry, degrade,
or surface to the UI), and the top level logs. C++ exceptions are
disabled.
\section{Toolchain}
\label{sec:fw-toolchain}
\begin{table}[htbp]
\centering
\begin{tabular}{@{}L{4.2cm}L{9.2cm}@{}}
\toprule
\textbf{Item} & \textbf{Choice} \\
\midrule
Framework & ESP-IDF v5.5.x (native) \\
Language & C++23 (\texttt{-std=gnu++23}) \\
Error model & \texttt{std::expected}; exceptions off \\
Hardware licence & CERN-OHL-S v2 \\
Firmware licence & Apache-2.0 \\
\bottomrule
\end{tabular}
\caption{Firmware toolchain and licensing summary.}
\label{tab:fw-toolchain}
\end{table}
The firmware source, build instructions, and development conventions are
maintained in the \texttt{Software/} directory of the project
repository.