BT1035Driver::boot() had zero retry on the reset+AT-init sequence — a
single hardware RESET# pulse followed immediately by AT commands,
with no second attempt if the module didn't come up in time. This is
the most plausible explanation for the intermittent "no spontaneous
UART bytes after hardware reset" / "AT init failed" boot failures
logged in docs/si4684-rf-investigation-report.md and observed again
live this morning on otherwise-identical hardware/wiring — classic
power-up timing jitter, not a permanent fault. Extracted the reset+
init sequence into resetAndInitOnce() and wrapped it in a 3-attempt
retry loop with a short delay between attempts; the one-time GPIO
config and UART driver install stay outside the loop since they don't
need repeating. Root cause of the underlying jitter is still open.
BluetoothJson.hpp was the only *Json.hpp module in the core with zero
host test coverage (status/scan/paired serialisation, auto-reconnect/
connect/speaker parsing) — every sibling module already has one.
Added bluetooth_json_test.cpp following the existing tuner_json_test
pattern; ctest now covers 20 suites instead of 19.
Documentation catch-up, found doing a full firmware re-review at the
user's request:
- POST /api/tuner/calibrate-antenna and the antcap field on
POST /api/tuner/tune (added in a previous commit, never documented)
are now in ch-api.tex.
- kFirmwareVersion was still hardcoded "0.8.5" despite the RF fixes,
BLE provisioning, phone streaming, antenna calibration, and generic
DSP param API landed since that version's actual release commit
(0a1188a). Bumped to 0.9.0 everywhere it's mentioned (health JSON,
the manual's title page, intro, classes, and API chapters).
- instructions.md and docs/TODO.md still described the firmware as
frozen at 0.8.5 awaiting hardware-in-the-loop testing that has since
happened extensively; docs/TODO.md's H5 verdict specifically still
said "suspect U6 RF ground (re-open PCBWay)" for a bug that turned
out to be firmware, not hardware — actively misleading, corrected.
Both files now summarise the post-0.8.5 HIL findings and current
open items (BT1035 root cause, intermittent HTTP unresponsiveness
under load, antenna-limited signal quality, possibly-undersized 24 KB
nvs partition).
Verified: idf.py build, doxygen (0 warnings), check-manual-sync,
check_si4684_blobs, ctest (20/20), two-pass xelatex manual build all
green. Flashed and confirmed live: fw reports 0.9.0, BT1035 booted on
the first attempt post-flash.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0178rASQ6ZETPMUamvpoR2KR
780 lines
29 KiB
TeX
780 lines
29 KiB
TeX
\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.9.0.
|
|
|
|
\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}
|
|
|
|
\apiendpoint{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.9.0","serialNumber":"0004A3123456",
|
|
"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{serialNumber} --- canonical EEPROM serial, or
|
|
\texttt{unknown} when the 24AA025E48 read fails.
|
|
\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.
|
|
|
|
\apiendpoint{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.
|
|
|
|
\apiendpoint{POST}{/api/wifi/scan}
|
|
\label{sec:api-wifi-scan}
|
|
|
|
Lists nearby access points via \texttt{net::WifiScanner::scanNearby()} (a
|
|
blocking \texttt{esp\_wifi} scan). Empty request body.
|
|
|
|
\begin{drnote}[Response schema]
|
|
\begin{drcode}[JSON]
|
|
{"networks":[{"ssid":"MyNetwork","rssi_dbm":-52,"auth":"wpa2","channel":6}]}
|
|
\end{drcode}
|
|
Deduplicated and sorted by signal strength; \texttt{ssid} may be empty for
|
|
hidden networks.
|
|
\end{drnote}
|
|
|
|
HTTP status: \textbf{200 OK}; \textbf{500} with
|
|
\texttt{\{"status":"error","reason":"scan\_failed"\}} on driver failure.
|
|
|
|
\apiendpoint{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,
|
|
"playing_service_id":42,"playing_component_id":7,
|
|
"dynamic_label":"Live show"},
|
|
"fm":null}
|
|
\end{drcode}
|
|
For FM, \texttt{fm} carries \texttt{frequency\_khz}, \texttt{rssi\_dbuv},
|
|
\texttt{snr\_db}, \texttt{stereo}, optional \texttt{station\_name} (RDS PS),
|
|
and \texttt{radiotext} (RDS RT); \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. FM responses may include
|
|
\texttt{station\_name} and \texttt{radiotext}; DAB responses may include
|
|
\texttt{dynamic\_label} when a programme is playing.
|
|
|
|
\apiendpoint{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).
|
|
|
|
\apiendpoint{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. FM accepts an optional \texttt{antcap} (0--128):
|
|
overrides the front-end antenna varactor for this one tune, e.g. while
|
|
running a calibration sweep. Omit it (the normal case) to use the board's
|
|
saved calibration --- see
|
|
\texttt{POST /api/tuner/calibrate-antenna}
|
|
(Section~\ref{sec:api-tuner-calibrate-antenna}) --- or hardware auto-tune
|
|
if never calibrated. Ignored for \texttt{"dab"}.
|
|
\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.
|
|
|
|
\apiendpoint{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.
|
|
|
|
\apiendpoint{POST}{/api/tuner/seek}
|
|
\label{sec:api-tuner-seek}
|
|
|
|
Seeks FM in the requested direction. Optional JSON body parsed by
|
|
\texttt{core::parseTunerSeekJson()}; an empty body defaults to upward seek.
|
|
Returns \texttt{\{"frequency\_khz":...\}} on success. HTTP status:
|
|
\textbf{200 OK}; \textbf{400} for invalid \texttt{direction}; \textbf{409} on
|
|
seek failure.
|
|
|
|
\begin{drnote}[Request schema]
|
|
\begin{drcode}[JSON]
|
|
{"direction":"up"}
|
|
\end{drcode}
|
|
Use \texttt{"down"} for downward seek. Omit the body or send an empty JSON
|
|
object (\texttt{\{\}}) for upward seek (backward compatible).
|
|
\end{drnote}
|
|
|
|
\apiendpoint{POST}{/api/tuner/scan}
|
|
\label{sec:api-tuner-scan}
|
|
|
|
Automatic search for a single station: repeats hardware seeks (FM) or
|
|
walks ensemble indices (DAB) via \texttt{TunerService::scanForStation()},
|
|
stopping early on the first usable hit, optionally filtered by name. This
|
|
blocks the HTTP connection for the whole search (up to tens of seconds).
|
|
|
|
\begin{drnote}[Request schema]
|
|
\begin{drcode}[JSON]
|
|
{"band":"fm","max_steps":45,"name":"BBC"}
|
|
\end{drcode}
|
|
\texttt{band} is required (\texttt{"fm"} or \texttt{"dab"}).
|
|
\texttt{max\_steps} is optional, 1--255 (default 45 for FM, 38 for DAB).
|
|
\texttt{name} is an optional case-insensitive substring match against the
|
|
RDS PS name (FM) or DAB service label.
|
|
\end{drnote}
|
|
|
|
\begin{drnote}[Response schema]
|
|
\begin{drcode}[JSON]
|
|
{"status":"found","band":"fm","steps":12,"frequency_khz":97500,
|
|
"station_name":"BBC R1"}
|
|
\end{drcode}
|
|
\texttt{status} is \texttt{"found"} or \texttt{"not\_found"}. On a DAB hit,
|
|
\texttt{freq\_index}, \texttt{service\_id}, and \texttt{component\_id}
|
|
appear instead of \texttt{frequency\_khz}.
|
|
\end{drnote}
|
|
|
|
HTTP status: \textbf{200 OK} (including \texttt{"not\_found"} outcomes);
|
|
\textbf{400} for invalid JSON; \textbf{409} on a hardware/driver failure
|
|
mid-scan.
|
|
|
|
\apiendpoint{POST}{/api/tuner/scan/full}
|
|
\label{sec:api-tuner-scan-full}
|
|
|
|
Sweeps the entire FM band with repeated hardware seeks
|
|
(\texttt{TunerService::scanFullFmBand()}) and returns every usable station
|
|
found, in ascending frequency order. Does \emph{not} save results as
|
|
presets --- this only returns the list; use
|
|
\texttt{POST /api/stations} (Section~\ref{sec:api-stations-post}) to save
|
|
individual hits. Empty request body; blocks for the whole sweep (tens of
|
|
seconds).
|
|
|
|
\begin{drnote}[Response schema]
|
|
\begin{drcode}[JSON]
|
|
{"stations":[{"frequency_khz":97500,"rssi_dbuv":58,"snr_db":21,
|
|
"station_name":"BBC R1"},
|
|
{"frequency_khz":101500,"rssi_dbuv":44,"snr_db":15}]}
|
|
\end{drcode}
|
|
\texttt{station\_name} (RDS PS) is present only when decoded before the
|
|
per-station poll budget ran out.
|
|
\end{drnote}
|
|
|
|
HTTP status: \textbf{200 OK}; \textbf{409} on a hardware/driver failure
|
|
mid-sweep; \textbf{503} when the tuner service is unavailable.
|
|
|
|
\apiendpoint{POST}{/api/tuner/calibrate-antenna}
|
|
\label{sec:api-tuner-calibrate-antenna}
|
|
|
|
Commits an FM ANTCAP value (AN851 Appendix A front-end calibration) as the
|
|
board's permanent default, found by sweeping \texttt{antcap} on
|
|
\texttt{POST /api/tuner/tune} (Section~\ref{sec:api-tuner-tune}) across
|
|
0--128 and comparing \texttt{rssi\_dbuv}/\texttt{snr\_db}. Persists to the
|
|
24AA025E48 EEPROM's user-writable region (separate from the factory-locked
|
|
EUI-48) and takes effect immediately on the live tuner --- no reboot
|
|
required, though it also survives one since \texttt{HardwareBootstrap::boot()}
|
|
reloads it at every boot. Once saved, every ordinary FM tune (seek, scan,
|
|
station recall, live UI) uses this value automatically instead of the
|
|
chip's own front-end auto-tune, unless a request explicitly overrides
|
|
\texttt{antcap} for that one call.
|
|
|
|
\begin{drnote}[Request schema]
|
|
\begin{drcode}[JSON]
|
|
{"antcap":102}
|
|
\end{drcode}
|
|
\texttt{antcap} is required, 0--128.
|
|
\end{drnote}
|
|
|
|
Success response: \texttt{\{"status":"saved","antcap":102\}}. HTTP status:
|
|
\textbf{200 OK}; \textbf{400} for invalid/missing \texttt{antcap};
|
|
\textbf{500} on an EEPROM write failure; \textbf{503} when the tuner
|
|
service is unavailable.
|
|
|
|
\apiendpoint{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.
|
|
|
|
\apiendpoint{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.
|
|
|
|
\apiendpoint{POST}{/api/dsp/program}
|
|
\label{sec:api-dsp-program}
|
|
|
|
Uploads a framed ADAU1701 RAM download blob (\texttt{DRAD} v1, see
|
|
\texttt{core::parseDspProgramBlob()}). The body is raw bytes
|
|
(\texttt{application/octet-stream} or any content type). On success the blob
|
|
is written to the \texttt{dsp} flash partition and the device reboots; the
|
|
next boot replays the new program before \texttt{loadAndApply()}.
|
|
|
|
\begin{drnote}[Success response]
|
|
\begin{drcode}[JSON]
|
|
{"status":"stored","reboot_sec":3}
|
|
\end{drcode}
|
|
\end{drnote}
|
|
|
|
HTTP status: \textbf{200 OK}; \textbf{400} with \texttt{\{"error":"..."\}}
|
|
for invalid/truncated/CRC failures; \textbf{413} when the body exceeds
|
|
200\,KiB; \textbf{500} on flash write failure. Pack blobs with
|
|
\texttt{tools/pack\_dsp\_program.py} or \texttt{core::serializeDspProgramBlob()}.
|
|
|
|
\apiendpoint{POST}{/api/system/ota}
|
|
\label{sec:api-system-ota}
|
|
|
|
Streams a raw ESP-IDF application \texttt{.bin} into the inactive OTA slot
|
|
(\texttt{ota\_0}/\texttt{ota\_1}). The body is raw bytes; the server validates
|
|
the embedded \texttt{esp\_app\_desc\_t} at offset~0x20 (magic and
|
|
\texttt{project\_name == "digiradio"}) while streaming. On success the new
|
|
slot is selected and the device reboots; the first healthy boot after Wi-Fi
|
|
and HTTP are up calls
|
|
\texttt{ota::OtaService::confirmBoot()} to cancel rollback.
|
|
|
|
\begin{drnote}[Success response]
|
|
\begin{drcode}[JSON]
|
|
{"status":"stored","reboot_sec":3}
|
|
\end{drcode}
|
|
\end{drnote}
|
|
|
|
HTTP status: \textbf{200 OK}; \textbf{400} with \texttt{\{"error":"..."\}}
|
|
for invalid project/magic or flash write failures;
|
|
\textbf{413} when the body exceeds 1.7\,MiB (\texttt{0x1B0000});
|
|
\textbf{500} on \texttt{esp\_ota\_end}/set-boot failures. Push with
|
|
\texttt{curl --data-binary @build/digiradio.bin}.
|
|
|
|
\apiendpoint{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.
|
|
|
|
\apiendpoint{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.
|
|
|
|
\apiendpoint{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}).
|
|
|
|
\apiendpoint{POST}{/api/audio/beep}
|
|
\label{sec:api-audio-beep}
|
|
|
|
Toggles the ADAU1701's own Beep1 tone generator cell directly (live
|
|
safeload only --- not part of \texttt{AudioProfile}, never persisted to
|
|
NVS). Used for signal-path bring-up/diagnostics without an ESP32-side
|
|
tone source.
|
|
|
|
\begin{drnote}[Request schema]
|
|
\begin{drcode}[JSON]
|
|
{"enabled":true}
|
|
\end{drcode}
|
|
\end{drnote}
|
|
|
|
Success response: \texttt{\{"status":"saved"\}}. HTTP status:
|
|
\textbf{200 OK}; \textbf{400} for invalid/missing \texttt{enabled};
|
|
\textbf{500} on safeload failure; \textbf{503} when the audio service is
|
|
unavailable.
|
|
|
|
\apiendpoint{GET}{/api/dsp/params}
|
|
\label{sec:api-dsp-params}
|
|
|
|
Lists every named Parameter RAM cell in the compiled SigmaStudio program
|
|
(\texttt{adau1701::kAdau1701ParamTable}, generated from
|
|
\texttt{DigiRadio\_IC\_1\_PARAM.h}). Read-only, no I/O with the chip.
|
|
|
|
\begin{drnote}[Response schema (excerpt)]
|
|
\begin{drcode}[JSON]
|
|
{"params":[{"name":"DspGain1algorithm0dB1","address":0},
|
|
{"name":"Beep1osc1algorithm0freq","address":16}]}
|
|
\end{drcode}
|
|
\end{drnote}
|
|
|
|
HTTP status: \textbf{200 OK}.
|
|
|
|
\apiendpoint{PUT}{/api/dsp/param}
|
|
\label{sec:api-dsp-param}
|
|
|
|
Writes one Parameter RAM cell by name, looked up against the same table as
|
|
\texttt{GET /api/dsp/params} and safeloaded via
|
|
\texttt{audio::AudioService::writeRawParam()}. This is a live-only escape
|
|
hatch onto \emph{any} SigmaStudio-exported cell --- the same trust level as
|
|
SigmaStudio's own Remote Connection panel: no domain validation on the
|
|
value, and it is never persisted or folded into \texttt{AudioProfile}. A
|
|
reboot or \texttt{POST /api/audio/reset} discards the change.
|
|
|
|
\begin{drnote}[Request schema]
|
|
\begin{drcode}[JSON]
|
|
{"name":"DspGain1algorithm0dB1","value":-6.0}
|
|
\end{drcode}
|
|
\texttt{value} is the SigmaStudio floating coefficient, converted to
|
|
8.23 fixed-point by \texttt{core::floatToFixpoint823()} before the
|
|
safeload write.
|
|
\end{drnote}
|
|
|
|
Success response: \texttt{\{"status":"saved"\}}. HTTP status:
|
|
\textbf{200 OK}; \textbf{400} for invalid JSON; \textbf{404} when
|
|
\texttt{name} is not in the compiled program's cell table; \textbf{500}
|
|
on safeload failure; \textbf{503} when the audio service is unavailable.
|
|
|
|
\apiendpoint{PUT}{/api/stream/phone}
|
|
\label{sec:api-stream-phone}
|
|
|
|
Accepts a live audio push from a phone app and writes it straight to the
|
|
shared I2S sink for as long as the connection stays open --- the same
|
|
physical wire \texttt{POST /api/streaming}'s web radio player uses, guarded
|
|
so only one producer holds it at a time. The body is raw interleaved
|
|
16-bit little-endian PCM, stereo, 48\,kHz, with \emph{no} header or
|
|
framing --- just samples. Send with chunked transfer encoding (no
|
|
\texttt{Content-Length} required) and close the connection to stop
|
|
streaming; the format is deliberately unencoded so the firmware side stays
|
|
minimal and the encoding choice lives entirely in the app.
|
|
|
|
HTTP status: \textbf{503} if the sink is unavailable; \textbf{409 Conflict}
|
|
if the web radio stream (or another phone stream) currently owns the I2S
|
|
sink; otherwise the connection is held open and a \textbf{200 OK} (empty
|
|
body) is sent once the client closes it, or \textbf{500} if a write to the
|
|
sink fails mid-stream.
|
|
|
|
\apiendpoint{GET}{/api/streaming}
|
|
\label{sec:api-streaming-get}
|
|
|
|
Returns the current internet radio streaming configuration
|
|
(\texttt{webradio::WebRadioService::config()}).
|
|
|
|
\begin{drnote}[Response schema]
|
|
\begin{drcode}[JSON]
|
|
{"enabled":true,"url":"http://stream.example.com/radio.mp3"}
|
|
\end{drcode}
|
|
\end{drnote}
|
|
|
|
HTTP status: \textbf{200 OK}; \textbf{503} when the web radio service is
|
|
unavailable.
|
|
|
|
\apiendpoint{POST}{/api/streaming}
|
|
\label{sec:api-streaming-post}
|
|
|
|
Sets the internet radio stream URL and enables/disables playback. Applied
|
|
immediately to the live streaming task and persisted to NVS --- no reboot
|
|
required. The stream is MP3 over HTTP, decoded on-device via libhelix and
|
|
written to the same shared I2S sink as \texttt{PUT /api/stream/phone}.
|
|
|
|
\begin{drnote}[Request schema]
|
|
\begin{drcode}[JSON]
|
|
{"enabled":true,"url":"http://stream.example.com/radio.mp3"}
|
|
\end{drcode}
|
|
\texttt{url} must be \texttt{http://}, non-empty, and at most 200 bytes.
|
|
\end{drnote}
|
|
|
|
On success, returns the same shape as \texttt{GET /api/streaming}. HTTP
|
|
status: \textbf{200 OK}; \textbf{400} for invalid JSON or URL;
|
|
\textbf{500} when NVS persistence fails; \textbf{503} when the web radio
|
|
service is unavailable.
|
|
|
|
% ------------------------------------------------------------------
|
|
% Bluetooth (Slice 7)
|
|
% ------------------------------------------------------------------
|
|
|
|
\apiendpoint{GET}{/api/bluetooth/status}
|
|
\label{sec:api-bluetooth-status}
|
|
|
|
Returns BT1035 boot flag, whether discoverable mode was requested, the
|
|
last read A2DP state, module friendly name, and auto-reconnect count.
|
|
Serialised by \texttt{core::serializeBluetoothStatusJson()}.
|
|
|
|
\begin{drnote}[Response schema]
|
|
\begin{drcode}[JSON]
|
|
{"booted":true,"pairing":false,"a2dp":"standby",
|
|
"device_name":"DigiRadio-A1B2","auto_reconnect":3}
|
|
\end{drcode}
|
|
\end{drnote}
|
|
|
|
\apiendpoint{POST}{/api/bluetooth/pair}
|
|
\label{sec:api-bluetooth-pair}
|
|
|
|
Enters discoverable mode (\texttt{AT+PAIR=1}). Success:
|
|
\texttt{\{"status":"pairing"\}}.
|
|
|
|
\apiendpoint{POST}{/api/bluetooth/pair/stop}
|
|
\label{sec:api-bluetooth-pair-stop}
|
|
|
|
Leaves discoverable mode (\texttt{AT+PAIR=0}). Success:
|
|
\texttt{\{"status":"idle"\}}.
|
|
|
|
\apiendpoint{POST}{/api/bluetooth/disconnect}
|
|
\label{sec:api-bluetooth-disconnect}
|
|
|
|
Releases the current A2DP session (\texttt{AT+A2DPDISC}).
|
|
|
|
\apiendpoint{GET}{/api/bluetooth/paired}
|
|
\label{sec:api-bluetooth-paired}
|
|
|
|
Returns paired remotes from \texttt{AT+PLIST}, serialised by
|
|
\texttt{core::serializeBluetoothPairedJson()}.
|
|
|
|
\begin{drnote}[Response schema]
|
|
\begin{drcode}[JSON]
|
|
{"devices":[{"index":1,"mac":"001122334455","name":"Phone"}]}
|
|
\end{drcode}
|
|
\end{drnote}
|
|
|
|
\apiendpoint{POST}{/api/bluetooth/scan}
|
|
\label{sec:api-bluetooth-scan}
|
|
|
|
Scans for nearby Bluetooth devices via \texttt{AT+DISC} on the BT1035
|
|
(\texttt{bluetooth::BluetoothService::scanNearby()}). Blocks for the scan
|
|
window.
|
|
|
|
\begin{drnote}[Request schema]
|
|
\begin{drcode}[JSON]
|
|
{"seconds":45}
|
|
\end{drcode}
|
|
\texttt{seconds} is optional, 1--255 (default 45); malformed or
|
|
out-of-range values fall back to the default rather than failing.
|
|
\end{drnote}
|
|
|
|
\begin{drnote}[Response schema]
|
|
\begin{drcode}[JSON]
|
|
{"devices":[{"index":1,"mac":"001122334455","name":"Bose SoundLink",
|
|
"rssi_dbm":-58}]}
|
|
\end{drcode}
|
|
\end{drnote}
|
|
|
|
HTTP status: \textbf{200 OK}; \textbf{500} on a BT1035 driver error;
|
|
\textbf{503} when the Bluetooth service is unavailable.
|
|
|
|
\apiendpoint{POST}{/api/bluetooth/connect}
|
|
\label{sec:api-bluetooth-connect}
|
|
|
|
Connects A2DP to a specific remote MAC, optionally saving it as the
|
|
default speaker for auto-reconnect on boot
|
|
(\texttt{BluetoothService::connectTo()}).
|
|
|
|
\begin{drnote}[Request schema]
|
|
\begin{drcode}[JSON]
|
|
{"mac":"001122334455","name":"Bose SoundLink","save":true}
|
|
\end{drcode}
|
|
\texttt{mac} is required (12 hex characters, no separators). \texttt{name}
|
|
and \texttt{save} are optional; when \texttt{save} is true, \texttt{mac}
|
|
and \texttt{name} are persisted the same way as
|
|
\texttt{POST /api/bluetooth/speaker}.
|
|
\end{drnote}
|
|
|
|
Success response: \texttt{\{"status":"connected"\}}. HTTP status:
|
|
\textbf{200 OK}; \textbf{400} when \texttt{mac} is missing/invalid;
|
|
\textbf{500} on a BT1035 driver error; \textbf{503} when the Bluetooth
|
|
service is unavailable.
|
|
|
|
\apiendpoint{GET}{/api/bluetooth/speaker}
|
|
\label{sec:api-bluetooth-speaker-get}
|
|
|
|
Returns the saved default-speaker target used for boot-time auto-reconnect,
|
|
if any.
|
|
|
|
\begin{drnote}[Response schema]
|
|
\begin{drcode}[JSON]
|
|
{"configured":true,"mac":"001122334455","name":"Bose SoundLink"}
|
|
\end{drcode}
|
|
\texttt{\{"configured":false\}} when nothing is saved.
|
|
\end{drnote}
|
|
|
|
HTTP status: \textbf{200 OK}; \textbf{500} on an NVS read failure;
|
|
\textbf{503} when the Bluetooth service is unavailable.
|
|
|
|
\apiendpoint{POST}{/api/bluetooth/speaker}
|
|
\label{sec:api-bluetooth-speaker-post}
|
|
|
|
Saves (or replaces) the default-speaker target, without connecting
|
|
immediately. Request schema matches
|
|
\texttt{POST /api/bluetooth/connect} minus \texttt{save} (\texttt{mac}
|
|
required, \texttt{name} optional).
|
|
|
|
Success response: \texttt{\{"status":"saved"\}}. HTTP status:
|
|
\textbf{200 OK}; \textbf{400} for invalid JSON; \textbf{500} on an NVS
|
|
write failure; \textbf{503} when the Bluetooth service is unavailable.
|
|
|
|
\apiendpoint{DELETE}{/api/bluetooth/speaker}
|
|
\label{sec:api-bluetooth-speaker-delete}
|
|
|
|
Clears the saved default-speaker target; boot-time auto-reconnect is
|
|
skipped until a new one is saved. Success response:
|
|
\texttt{\{"status":"cleared"\}}. HTTP status: \textbf{200 OK};
|
|
\textbf{503} when the Bluetooth service is unavailable.
|
|
|
|
\apiendpoint{POST}{/api/bluetooth/reconnect}
|
|
\label{sec:api-bluetooth-reconnect}
|
|
|
|
Manually retries A2DP connect to the saved default speaker
|
|
(\texttt{BluetoothService::reconnectSavedSpeaker()}) --- the same call the
|
|
firmware makes once at boot. Empty request body.
|
|
|
|
Success response: \texttt{\{"status":"connected"\}}. HTTP status:
|
|
\textbf{200 OK}; \textbf{500} on a BT1035 driver error or when no speaker
|
|
is saved; \textbf{503} when the Bluetooth service is unavailable.
|
|
|
|
\apiendpoint{POST}{/api/bluetooth/auto-reconnect}
|
|
\label{sec:api-bluetooth-auto-reconnect}
|
|
|
|
Sets the module auto-reconnect retry count (\texttt{AT+AUTOCONN=0..15}).
|
|
Body parsed by \texttt{core::parseBluetoothAutoReconnectJson()}.
|
|
|
|
\begin{drnote}[Request schema]
|
|
\begin{drcode}[JSON]
|
|
{"times":3}
|
|
\end{drcode}
|
|
Success: \texttt{\{"status":"saved"\}}.
|
|
\end{drnote}
|
|
|
|
% ------------------------------------------------------------------
|
|
% Station presets (Slice 4)
|
|
% ------------------------------------------------------------------
|
|
|
|
\apiendpoint{GET}{/api/stations}
|
|
\label{sec:api-stations-get}
|
|
|
|
Returns presets serialised by \texttt{core::serializeStationListJson()}.
|
|
|
|
\apiendpoint{POST}{/api/stations}
|
|
\label{sec:api-stations-post}
|
|
|
|
Adds one preset (\texttt{core::parseStationJson()}); persists via
|
|
\texttt{ISecureStore::saveStationListJson()}.
|
|
|
|
\apiendpoint{POST}{/api/stations/remove}
|
|
\label{sec:api-stations-remove}
|
|
|
|
Removes a preset by list index (\texttt{\{"index":0\}}).
|
|
|
|
\apiendpoint{POST}{/api/stations/reorder}
|
|
\label{sec:api-stations-reorder}
|
|
|
|
Reorders presets using \texttt{StationList::move()}. Body:
|
|
\texttt{\{"from":1,"to":0\}}. Success: \texttt{\{"status":"reordered"\}}.
|
|
|
|
\apiendpoint{POST}{/api/stations/tune}
|
|
\label{sec:api-stations-tune}
|
|
|
|
Recalls a preset via \texttt{integration::IntegrationService::recallPreset()}
|
|
(tune, re-apply saved audio profile, persist last index).
|
|
|
|
\section{Boot and network state machine}
|
|
\label{sec:api-boot-flow}
|
|
|
|
At boot, \texttt{integration::IntegrationService::startup()} loads presets
|
|
and best-effort recalls the last station. Then
|
|
\texttt{net::NetBootstrap::start(store, tuner, audio, bluetooth, stations,
|
|
integration)} 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.
|
|
|
|
\subsection{BLE provisioning (additive)}
|
|
\label{sec:api-boot-ble-provisioning}
|
|
|
|
Alongside the SoftAP, setup mode also starts ESP-IDF's own
|
|
\texttt{wifi\_provisioning} manager over BLE
|
|
(\texttt{net::ble\_provisioning::start()}), using the ESP32-S3's onboard
|
|
BLE radio --- independent of the BT1035, which stays a separate UART-attached
|
|
classic-Bluetooth A2DP module. This is the standard Espressif provisioning
|
|
protocol (protocomm, Security1), so any generic ``ESP BLE Provisioning''
|
|
app (iOS/Android) can join the device to Wi-Fi without first connecting to
|
|
\texttt{DigiRadio-setup}; the proof-of-possession string is the device's
|
|
own serial number (same source as the SoftAP SSID and
|
|
\texttt{GET /api/health}'s \texttt{serialNumber}). It is not an HTTP
|
|
endpoint --- there is no REST call here --- and it is strictly additive:
|
|
if it fails to start, SoftAP setup keeps working exactly as before. On a
|
|
successful join the received credentials are converted to the same
|
|
\texttt{core::WifiCredentials} type and saved through the same
|
|
\texttt{ISecureStore} that \texttt{POST /api/wifi} uses, then the device
|
|
reboots into STA mode.
|
|
|
|
\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}. Station presets use key
|
|
\texttt{station\_list} (JSON blob). The last recalled preset index is stored
|
|
as \texttt{last\_preset} (u8). Passwords are wrapped in
|
|
\texttt{core::Secret} in RAM and are never logged or returned by the API.
|
|
|
|
\begin{drcaution}[Encryption at rest]
|
|
Firmware~0.8.3+ enables NVS encryption
|
|
(\texttt{CONFIG\_NVS\_ENCRYPTION}) and flash encryption in development mode
|
|
(\texttt{sdkconfig.defaults}). Keys live in the \texttt{nvs\_keys} partition;
|
|
Wi-Fi passwords remain wrapped in \texttt{core::Secret} in RAM and are never
|
|
logged. First upgrade from plain NVS requires \texttt{idf.py erase-flash}.
|
|
HIL checklist: \drpath{Software/docs/security-flash-nvs.md}.
|
|
\end{drcaution}
|