\chapter{HTTP API} \label{ch:api} The setup web interface is a thin client over a typed JSON REST API implemented in \texttt{SetupWebServer}. Request bodies are parsed into domain types in the pure core (\texttt{components/core}) before any persistence or driver call. Exact C++ signatures live in the generated Doxygen output under \texttt{docs/api/}; this chapter documents the wire protocol and behaviour as shipped in firmware~0.5.0 (Slices~1--5). \section{Transport and reachability} \begin{itemize} \item \textbf{Port:} TCP~80 on the ESP32-S3 HTTP server. \item \textbf{Setup mode (\texttt{NetState::SoftApSetup}):} join the SoftAP \texttt{DigiRadio-setup} (open network), then open \url{http://192.168.4.1/}. The root path serves a gzipped HTML page embedded from flash. \item \textbf{STA mode (\texttt{NetState::StaConnected}):} after successful provisioning and reboot, the same API is available at the device's DHCP address on the configured LAN. \end{itemize} All responses use \texttt{Content-Type: application/json} except \texttt{GET /}, which returns gzipped HTML with \texttt{Content-Encoding: gzip}. \section{Endpoints} \subsection{\texttt{GET /api/health}} \label{sec:api-health} Returns a health-check DTO serialised by \texttt{core::serializeHealthStatusJson()} from a \texttt{core::HealthStatus} value. \begin{drnote}[Response schema] \begin{drcode}[JSON] {"status":"ok","fw":"0.6.0", "chips":{"si4684":true,"adau1701":true,"bt1035":true}} \end{drcode} \begin{itemize} \item \texttt{status} --- coarse indicator; \texttt{ok} when the firmware is running normally. \item \texttt{fw} --- firmware release string (\texttt{core::FirmwareVersion}). \item \texttt{chips} --- companion-chip boot flags after \texttt{HardwareBootstrap::boot()} (Slice~8 integration). \end{itemize} \end{drnote} HTTP status: \textbf{200 OK} on success. \subsection{\texttt{POST /api/wifi}} \label{sec:api-wifi} Accepts Wi-Fi credentials for station (STA) join. The body is parsed by \texttt{core::parseWifiProvisionJson()} into a \texttt{core::WifiCredentials} value (SSID as \texttt{core::WifiSsid}, password as \texttt{core::Secret}). On success the credentials are persisted through \texttt{core::ISecureStore} (device implementation: \texttt{secure\_store::NvsSecureStore}) and the device schedules a reboot so the next boot can enter STA mode. \begin{drnote}[Request schema] \begin{drcode}[JSON] {"ssid":"MyNetwork","password":"secret1234"} \end{drcode} \begin{itemize} \item \texttt{ssid} --- required, 1--32 characters (802.11 SSID limits). \item \texttt{password} --- optional for open networks; for WPA-PSK, 8--63 characters. Validated by \texttt{core::WifiCredentials::isPasswordValid()}. \end{itemize} \end{drnote} \begin{drnote}[Success response] \begin{drcode}[JSON] {"status":"saved","reboot_in_sec":3} \end{drcode} Serialised by \texttt{core::serializeWifiProvisionSavedJson()}. The device reboots after the indicated delay. \end{drnote} \begin{drnote}[Error response] \begin{drcode}[JSON] {"status":"error","reason":"invalid_ssid"} \end{drcode} Serialised by \texttt{core::serializeWifiProvisionErrorJson()}. Reason tokens (never include secrets): \texttt{invalid\_json}, \texttt{missing\_field}, \texttt{invalid\_ssid}, \texttt{invalid\_password}, \texttt{store\_failed}. \end{drnote} HTTP status: \textbf{200 OK} on save success; \textbf{400 Bad Request} for parse/validation failures; \textbf{500 Internal Server Error} when NVS persistence fails. \subsection{\texttt{GET /api/tuner/status}} \label{sec:api-tuner-status} Returns a tuner snapshot serialised by \texttt{core::serializeTunerStatusJson()} from \texttt{core::TunerStatus}. The handler calls \texttt{tuner::TunerService::refreshStatus()}. DAB and FM tuning workflows are in Chapter~\ref{ch:si4684}, Sections~\ref{sec:si4684-dab-session} and~\ref{sec:si4684-fm-session}. \begin{drnote}[Response schema (DAB example)] \begin{drcode}[JSON] {"booted":true,"band":"dab","locked":true,"volume":63, "dab":{"freq_index":12,"fic_quality":80,"cnr_db":25},"fm":null} \end{drcode} For FM, \texttt{fm} carries \texttt{frequency\_khz}, \texttt{rssi\_dbuv}, \texttt{snr\_db}, and \texttt{stereo}; \texttt{dab} is \texttt{null}. \end{drnote} HTTP status: \textbf{200 OK}; \textbf{500} with \texttt{\{"status":"error","reason":...\}} on driver failure; \textbf{503} when the tuner service is unavailable. \subsection{\texttt{GET /api/tuner/services}} \label{sec:api-tuner-services} Lists DAB programmes for the current ensemble via \texttt{TunerService::listDabServices()}. \begin{drnote}[Response schema] \begin{drcode}[JSON] {"services":[{"service_id":12345,"component_id":1,"label":"BBC R1"}]} \end{drcode} \end{drnote} HTTP status: \textbf{200 OK}; \textbf{409 Conflict} when the list cannot be fetched (wrong band, empty ensemble, hardware error). \subsection{\texttt{POST /api/tuner/tune}} \label{sec:api-tuner-tune} Tunes to a DAB ensemble or FM frequency. Body parsed by \texttt{core::parseTunerTuneJson()}. \begin{drnote}[Request schema] \begin{drcode}[JSON] {"band":"dab","freq_index":12} \end{drcode} or \begin{drcode}[JSON] {"band":"fm","frequency_khz":101500} \end{drcode} DAB \texttt{freq\_index} is 0--37; FM \texttt{frequency\_khz} is 64\,000--108\,000. \end{drnote} On success returns the updated status JSON (same shape as \texttt{GET /api/tuner/status}). HTTP status: \textbf{200 OK}; \textbf{400} for invalid JSON; \textbf{409} for tune failures. \subsection{\texttt{POST /api/tuner/play}} \label{sec:api-tuner-play} Starts a DAB audio service. Body parsed by \texttt{core::parseTunerPlayJson()}. \begin{drnote}[Request schema] \begin{drcode}[JSON] {"service_id":12345,"component_id":1} \end{drcode} \end{drnote} Success response: \texttt{\{"status":"playing"\}}. HTTP status: \textbf{200 OK}; \textbf{409} when playback cannot start. \subsection{\texttt{POST /api/tuner/seek}} \label{sec:api-tuner-seek} Seeks FM upward (no request body). Returns \texttt{\{"frequency\_khz":...\}} on success. HTTP status: \textbf{200 OK}; \textbf{409} on seek failure. \subsection{\texttt{GET /api/audio/profile}} \label{sec:api-audio-profile-get} Returns the current ADAU1701 audio snapshot serialised by \texttt{core::serializeAudioProfileJson()} from \texttt{core::AudioProfile}. The handler reads \texttt{audio::AudioService::currentProfile()}. Driver behaviour, domain types, and persistence are documented in Chapter~\ref{ch:adau1701}. \begin{drnote}[Response schema (excerpt)] \begin{drcode}[JSON] {"mixer":{"si4684_left_db":0,"si4684_right_db":0,"esp32_left_db":0, "esp32_right_db":0,"mix_left_db":0,"mix_right_db":0}, "master":{"left_db":0,"right_db":0}, "eq":[{"gain_db":0,"center_hz":40,"q":1.414}, ...], "enhancements":{"stereo_level":0,"bass_level":0}} \end{drcode} Six EQ bands are always present (\texttt{eq} array length~6). Enhancement levels are 0--100; at 0 the base EQ band settings apply. \end{drnote} HTTP status: \textbf{200 OK}; \textbf{503} when the audio service is unavailable. \subsection{\texttt{PUT /api/audio/profile}} \label{sec:api-audio-profile-put} Applies a full audio profile. Body parsed by \texttt{core::parseAudioProfileJson()}; on success \texttt{audio::AudioService::applyProfile(..., persist=true)} safeloads the ADAU1701 and writes NVS key \texttt{audio\_profile\_json}. \begin{drnote}[Success response] \begin{drcode}[JSON] {"status":"saved"} \end{drcode} \end{drnote} HTTP status: \textbf{200 OK}; \textbf{400} for parse/validation failures; \textbf{500} when safeload or NVS persistence fails. \subsection{\texttt{POST /api/audio/reset}} \label{sec:api-audio-reset} Restores \texttt{AudioProfile::factoryDefault()} (flat EQ, 0\,dB gains), applies it to the DSP, and persists to NVS. Success response: \texttt{\{"status":"saved"\}}. HTTP status: \textbf{200 OK}; \textbf{500} on apply/persist failure. \subsection{\texttt{POST /api/audio/stereo-enhance}} \label{sec:api-audio-stereo-enhance} Adjusts stereo depth via a psychoacoustic PEQ overlay on bands 3--5 (1\,kHz / 3\,kHz / 8\,kHz). This is \emph{not} true M/S widening---there is no dedicated SigmaStudio widener block; see Section~\ref{sec:ss-enhancements}. \begin{drnote}[Request body] \begin{drcode}[JSON] {"level":50} \end{drcode} \texttt{level} is an integer 0--100 (0 = off, 100 = maximum). \end{drnote} Success response: \texttt{\{"status":"saved"\}}. HTTP status: \textbf{200 OK}; \textbf{400} for invalid JSON or level; \textbf{500} when safeload or NVS persistence fails. \subsection{\texttt{POST /api/audio/bass-enhance}} \label{sec:api-audio-bass-enhance} Adjusts bass emphasis via a PEQ overlay on bands 1--2 (100\,Hz / 400\,Hz). Request and response schema match \texttt{POST /api/audio/stereo-enhance} (Section~\ref{sec:api-audio-stereo-enhance}). \section{Boot and network state machine} \label{sec:api-boot-flow} At boot, \texttt{net::NetBootstrap::start(store, tuner, audio)} consults \texttt{ISecureStore::hasWifiCredentials()}: \begin{enumerate} \item \textbf{Credentials present} --- create STA netif, connect via \texttt{net::StaClient} (30\,s timeout). On success: \texttt{NetState::StaConnected} and HTTP on the LAN address. \item \textbf{No credentials, or STA join fails} --- fall back to SoftAP setup mode (\texttt{NetState::SoftApSetup}). \end{enumerate} This explicit \texttt{enum class NetState} replaces ad-hoc flags; see \texttt{net/NetState.hpp} in the source tree. \section{Credential storage (Slice~2)} \label{sec:api-storage} Wi-Fi credentials are stored in NVS namespace \texttt{digiradio}, keys \texttt{wifi\_ssid} and \texttt{wifi\_pwd}. Audio profiles (non-secret) use the same namespace, key \texttt{audio\_profile\_json}, via \texttt{secure\_store::NvsAudioProfileStore}. Passwords are wrapped in \texttt{core::Secret} in RAM and are never logged or returned by the API. \begin{drcaution}[Encryption at rest] Development builds use plain NVS. Production should enable NVS encryption using the reserved \texttt{nvs\_keys} partition (see \texttt{partitions.csv} and \texttt{sdkconfig.defaults} comments) per current ESP-IDF security guidance. \end{drcaution}