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>
261 lines
12 KiB
TeX
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.
|