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
362 lines
18 KiB
TeX
362 lines
18 KiB
TeX
\chapter{Class Reference}
|
||
\label{ch:classes}
|
||
|
||
This chapter documents the firmware's public classes at the design level:
|
||
what each class is responsible for, which collaborators it depends on, its
|
||
key invariants, and how it fits the system. It complements --- and does
|
||
not duplicate --- the generated API documentation, which carries the exact
|
||
method signatures. The HTTP JSON endpoints are documented separately in
|
||
Chapter~\ref{ch:api}.
|
||
|
||
\begin{drnote}[How this chapter grows]
|
||
Per the development rules, every public, architecturally-significant class
|
||
(the drivers, the application services, and the public domain-core types)
|
||
gets a section here, tagged \texttt{\textbackslash label\{cls:ClassName\}},
|
||
added in the same change that introduces the class. A tooling check keeps
|
||
this chapter in step with the code, so it is always current.
|
||
\end{drnote}
|
||
|
||
The class reference tracks firmware~0.9.0 on \texttt{main}. Public classes
|
||
are grouped by layer: domain core, application services, and hardware drivers.
|
||
|
||
% ------------------------------------------------------------------
|
||
% Domain core (Slice 1)
|
||
% ------------------------------------------------------------------
|
||
|
||
\section{FirmwareVersion}\label{cls:FirmwareVersion}
|
||
Strong type wrapping the firmware release identifier (e.g.\ \texttt{0.8.3}).
|
||
Used by \texttt{HealthStatus} and the \texttt{/api/health} endpoint so
|
||
version strings are never passed as bare \texttt{char*} across module
|
||
boundaries. Invariant: non-empty at construction.
|
||
|
||
\section{FrequencyKHz}\label{cls:FrequencyKHz}
|
||
Validated FM centre frequency in kilohertz (64\,000–108\,000). Parsed once
|
||
from JSON at the HTTP boundary via \texttt{FrequencyKHz::tryFromKhz()};
|
||
trusted downstream by \texttt{ITuner}, \texttt{Si4684Driver}, and status DTOs.
|
||
|
||
\section{HealthStatus}\label{cls:HealthStatus}
|
||
Immutable health-check DTO returned by \texttt{GET /api/health}. Built via
|
||
\texttt{HealthStatus::ok(FirmwareVersion, CompanionChipStatus, serialNumber)}
|
||
in the pure core; JSON serialisation lives in \texttt{serializeHealthStatusJson()}.
|
||
The \texttt{serialNumber} field carries the EEPROM-derived serial or
|
||
\texttt{unknown} when the EUI-48 read fails. No ESP-IDF headers.
|
||
|
||
\section{Eui48}\label{cls:Eui48}
|
||
Strong type for the factory-programmed six-byte EUI-48 read from the
|
||
24AA025E48 at word address \texttt{0xFA} (Microchip DS20001191). Exposes
|
||
\texttt{serialNumber()} (12 uppercase hex digits) and \texttt{shortSuffix()}
|
||
(last three bytes) for SSID and hostname derivation.
|
||
|
||
\section{DeviceIdentity}\label{cls:DeviceIdentity}
|
||
Per-board identity derived from \texttt{Eui48}: SoftAP SSID
|
||
\texttt{DigiRadio-<suffix>}, Bluetooth name, STA hostname
|
||
\texttt{digiradio-<suffix>}, and canonical serial. \texttt{unknown()} yields
|
||
documented fallbacks (\texttt{DigiRadio-setup}, serial \texttt{unknown}).
|
||
|
||
\section{IDeviceIdentitySource}\label{cls:IDeviceIdentitySource}
|
||
Port for reading \texttt{DeviceIdentity} from board storage. Implemented in
|
||
the shell by \texttt{eeprom24aa::Eeprom24aa} on the shared I2C bus.
|
||
|
||
% ------------------------------------------------------------------
|
||
% Network shell (Slice 1)
|
||
% ------------------------------------------------------------------
|
||
|
||
\section{SoftApConfig}\label{cls:SoftApConfig}
|
||
Immutable value type holding SoftAP parameters (SSID, channel, station
|
||
limit). \texttt{forSsid()} builds setup parameters from
|
||
\texttt{DeviceIdentity::softApSsid()}; \texttt{setupDefault()} keeps the
|
||
\texttt{DigiRadio-setup} fallback when the EEPROM read fails.
|
||
|
||
\section{SoftApHost}\label{cls:SoftApHost}
|
||
RAII wrapper around the ESP-IDF Wi-Fi stack that starts and stops the
|
||
configured SoftAP. Imperative shell; no business logic.
|
||
|
||
\section{SetupWebServer}\label{cls:SetupWebServer}
|
||
Minimal HTTP server: gzipped setup UI and typed JSON REST handlers:
|
||
\begin{itemize}
|
||
\item \texttt{GET /api/health}, \texttt{POST /api/wifi}
|
||
\item Tuner: \texttt{/api/tuner/*}
|
||
\item Audio: \texttt{/api/audio/*}
|
||
\item Bluetooth: \texttt{/api/bluetooth/*}
|
||
\item Presets: \texttt{/api/stations/*}
|
||
\item Streaming: \texttt{/api/streaming}
|
||
\end{itemize}
|
||
JSON parsing and serialisation delegate to the pure core; credentials persist via \texttt{ISecureStore};
|
||
tuner via \texttt{tuner::TunerService}; audio via
|
||
\texttt{audio::AudioService}; Bluetooth via \texttt{bluetooth::BluetoothService};
|
||
presets via \texttt{station::StationService}; streaming via
|
||
\texttt{webradio::WebRadioService}.
|
||
|
||
\section{WifiScanner}\label{cls:WifiScanner}
|
||
Stateless shell helper wrapping \texttt{esp\_wifi} scan for the setup UI's
|
||
Wi-Fi tab. \texttt{scanNearby()} temporarily switches an AP-only radio to
|
||
APSTA mode when needed, runs a blocking scan, dedupes access points by
|
||
SSID (keeping the strongest RSSI), and maps results to
|
||
\texttt{core::WifiScannedNetwork}. Called from \texttt{POST /api/wifi/scan}.
|
||
|
||
\section{NetBootstrap}\label{cls:NetBootstrap}
|
||
Owns network resources for setup or STA mode. Initialises encrypted NVS via
|
||
\texttt{secure\_store::initEncryptedStorage()}, then Wi-Fi and the HTTP server.
|
||
Wires tuner, audio, Bluetooth, station, and integration services into REST
|
||
handlers. Must outlive \texttt{app\_main} for the process lifetime.
|
||
|
||
% ------------------------------------------------------------------
|
||
% Domain core + secure store (Slice 2)
|
||
% ------------------------------------------------------------------
|
||
|
||
\section{Secret}\label{cls:Secret}
|
||
Opaque wrapper for sensitive strings. Buffer is zeroised on destruction;
|
||
no \texttt{operator<<} and no plaintext serialisation. Shell code uses
|
||
\texttt{usePlaintext()} transiently for driver APIs only.
|
||
|
||
\section{WifiSsid}\label{cls:WifiSsid}
|
||
Validated Wi-Fi SSID (1--32 bytes). Parsed once from untrusted JSON at
|
||
the HTTP boundary before persistence or driver calls.
|
||
|
||
\section{WifiCredentials}\label{cls:WifiCredentials}
|
||
Domain value pairing \texttt{WifiSsid} with a \texttt{Secret} password.
|
||
Open networks use an empty password; WPA-PSK requires 8--63 characters.
|
||
|
||
\section{ISecureStore}\label{cls:ISecureStore}
|
||
Abstract persistence boundary for credentials and preset data at rest.
|
||
Wi-Fi credentials, station list JSON, and last-preset index use
|
||
\texttt{NvsSecureStore}; audio profiles use \texttt{NvsAudioProfileStore}.
|
||
Host tests use fakes; the shell uses NVS backends after
|
||
\texttt{initEncryptedStorage()}.
|
||
|
||
\section{StaClient}\label{cls:StaClient}
|
||
RAII STA join helper with an explicit connect timeout. Assumes
|
||
\texttt{esp\_wifi\_init()} was already called by \texttt{NetBootstrap}.
|
||
|
||
\section{NvsSecureStore}\label{cls:NvsSecureStore}
|
||
\texttt{ISecureStore} implementation backed by an NVS namespace. Passwords
|
||
are stored as NVS strings and never logged. NVS encryption and flash
|
||
encryption are enabled in \texttt{sdkconfig.defaults}; initialisation runs in
|
||
\texttt{secure\_store::initEncryptedStorage()} before network bring-up.
|
||
|
||
% ------------------------------------------------------------------
|
||
% Hardware drivers (Slice 3)
|
||
% ------------------------------------------------------------------
|
||
|
||
\section{IFirmwareBlobReader}\label{cls:IFirmwareBlobReader}
|
||
Abstract streaming reader for firmware blobs embedded in flash. The Si4684
|
||
driver uses it to HOST\_LOAD patch and application images in bounded SPI
|
||
chunks without allocating the full image on the heap. Host tests use the
|
||
same interface over in-memory buffers.
|
||
|
||
\section{EmbeddedBlobReader}\label{cls:EmbeddedBlobReader}
|
||
Non-owning \texttt{IFirmwareBlobReader} over a contiguous byte range (linker
|
||
symbols from \texttt{EMBED\_FILES} or test data). Implements offset-based
|
||
\texttt{read()} with truncation at end-of-blob.
|
||
|
||
\section{Si4684EmbeddedImages}\label{cls:Si4684EmbeddedImages}
|
||
Owns \texttt{EmbeddedBlobReader} views over \texttt{rom\_patch\_016.bin},
|
||
\texttt{dab\_firmware.bin}, and \texttt{fm\_firmware.bin}. Constructed once;
|
||
passed to \texttt{Si4684Driver}. \texttt{applicationImage(Si4684Band)} selects
|
||
DAB or FM.
|
||
|
||
\section{Si4684Driver}\label{cls:Si4684Driver}
|
||
Full SPI driver for the Si4684 DAB+/FM tuner (Chapter~\ref{ch:si4684}).
|
||
|
||
\texttt{boot(Si4684Band)} runs AN649 HOST\_LOAD, configures I\textsuperscript{2}S
|
||
and band-specific properties, and records \texttt{loadedBand()}. After boot the
|
||
driver exposes FM tuning/seek/RSQ/RDS, DAB ensemble tuning, digital service list
|
||
fetch, and \texttt{startDabService}. Property access (\texttt{setProperty},
|
||
\texttt{setVolume}) and diagnostics (\texttt{getPartInfo}, \texttt{getSysState})
|
||
are included. Wrong-band calls return \texttt{Si4684Error::WrongBand}.
|
||
Register-level opcodes remain private; see \texttt{Si4684Types.hpp} for status
|
||
DTOs. Full DAB/FM tuning guide: Chapter~\ref{ch:si4684}.
|
||
|
||
\section{Bt1035PairedDevice}\label{cls:Bt1035PairedDevice}
|
||
Plain DTO for one \texttt{+PLIST=} line from the FSC-BT1035: paired-record
|
||
index (1--8), 12-digit MAC, and optional friendly name. Parsed by
|
||
\texttt{core::parseBt1035PairedListResponse()} and serialised on
|
||
\texttt{GET /api/bluetooth/paired}.
|
||
|
||
\section{Bt1035Driver}\label{cls:Bt1035Driver}
|
||
UART driver for the FSC-BT1035 (Chapter~\ref{ch:bt1035}). \texttt{boot()}
|
||
pulses RESET\#, opens UART2 with RTS/CTS, and runs
|
||
\texttt{core::bootInitSequence()} (Ping + \texttt{AT+AUXCFG=3} +
|
||
\texttt{AT+I2SCFG=35}). Returns
|
||
\texttt{Bt1035Error} on timeout, ERROR response, or UART failure.
|
||
|
||
\section{Adau1701Driver}\label{cls:Adau1701Driver}
|
||
RAII I\textsuperscript{2}C driver for the ADAU1701 SigmaDSP
|
||
(Chapter~\ref{ch:adau1701}). \texttt{boot()} asserts RESET\#,
|
||
initialises the shared I2C bus, and replays the SigmaStudio export from
|
||
\texttt{Firmware/ADAU1701-Firmware/} on every power-up (no EEPROM self-boot
|
||
on DigiRadio). Exposes \texttt{i2cBusHandle()} after boot for the shared
|
||
24AA025E48 EEPROM. Runtime mixer, EQ, and master volume updates use the
|
||
ADAU1701 safeload mechanism via \texttt{applyProfile()} and related methods
|
||
(Section~\ref{sec:adau1701-driver}).
|
||
|
||
\section{Eeprom24aa}\label{cls:Eeprom24aa}
|
||
I\textsuperscript{2}C shell driver for the 24AA025E48 identity EEPROM
|
||
(Chapter~\ref{ch:hardware}, address \texttt{0x52}). Implements
|
||
\texttt{IDeviceIdentitySource}: reads six bytes at word address \texttt{0xFA}
|
||
per Microchip DS20001191 and builds \texttt{DeviceIdentity}. Called from
|
||
\texttt{HardwareBootstrap} after \texttt{Adau1701Driver::boot()}.
|
||
|
||
\section{Adau1701Dsp}\label{cls:Adau1701Dsp}
|
||
\texttt{core::IDsp} adapter over \texttt{Adau1701Driver}. Maps domain-level
|
||
audio control to safeload I\textsuperscript{2}C transactions without exposing
|
||
parameter RAM addresses to services (Chapter~\ref{ch:adau1701},
|
||
Section~\ref{sec:adau1701-stack}).
|
||
|
||
\section{RegisterWrite}\label{cls:RegisterWrite}
|
||
One SigmaStudio \texttt{SIGMA\_WRITE\_REGISTER\_BLOCK} step: 16-bit address
|
||
plus payload bytes. Aggregated by \texttt{DspProgram}.
|
||
|
||
\section{DspProgram}\label{cls:DspProgram}
|
||
Ordered ADAU1701 RAM download script (pure domain). Parsed from a framed
|
||
\texttt{DRAD} flash blob or built by \texttt{EmbeddedDspProgramSource}.
|
||
|
||
\section{IDspProgramSource}\label{cls:IDspProgramSource}
|
||
Port supplying \texttt{DspProgram} to \texttt{Adau1701Driver::boot()}.
|
||
Implementations: flash partition, embedded export, and fallback composite.
|
||
|
||
\section{EmbeddedDspProgramSource}\label{cls:EmbeddedDspProgramSource}
|
||
Builds the factory \texttt{DspProgram} from \texttt{DigiRadio\_IC\_1.h}
|
||
(five writes matching \texttt{default\_download\_IC\_1()}).
|
||
|
||
\section{FlashDspProgramSource}\label{cls:FlashDspProgramSource}
|
||
Reads and parses the \texttt{dsp} flash partition; \texttt{storeBlob()} erases
|
||
and writes a validated blob from \texttt{POST /api/dsp/program}.
|
||
|
||
\section{FallbackDspProgramSource}\label{cls:FallbackDspProgramSource}
|
||
Tries \texttt{FlashDspProgramSource} first, then
|
||
\texttt{EmbeddedDspProgramSource} when the partition is empty or invalid.
|
||
|
||
% ------------------------------------------------------------------
|
||
% Domain core — audio (Slice 5)
|
||
% ------------------------------------------------------------------
|
||
|
||
\section{GainDb}\label{cls:GainDb}
|
||
Validated decibel gain/attenuation ($-96$\,dB to $+12$\,dB). Used for
|
||
input volumes, master level, and EQ band gain. Converted to ADAU 8.23
|
||
fixpoint via \texttt{gainDbToLinearFixpoint()} before safeload.
|
||
|
||
\section{FrequencyHz}\label{cls:FrequencyHz}
|
||
Validated PEQ centre frequency (20--20\,000\,Hz) for the 48\,kHz
|
||
SigmaStudio project. Parsed at the HTTP boundary before coefficient design.
|
||
|
||
\section{EqBandIndex}\label{cls:EqBandIndex}
|
||
Strong index into the six-band \texttt{Param EQ1} module (0--5). Maps to
|
||
parameter RAM via \texttt{adau1701::paramAddrEqBandBase()}.
|
||
|
||
\section{EqProfile}\label{cls:EqProfile}
|
||
Six-band parametric EQ snapshot aligned with the SigmaStudio map
|
||
(Chapter~\ref{ch:sigmastudio}): band index~0 is the fixed 20\,Hz
|
||
high-pass; bands 1--5 are peaking at 100\,Hz--8\,kHz. Designed in the
|
||
pure core; peaking bands are safeloaded by \texttt{Adau1701Driver::applyEq()}
|
||
(band~0 is skipped).
|
||
|
||
\section{IDsp}\label{cls:IDsp}
|
||
Abstract ADAU1701 control boundary. Defines \texttt{applyProfile()},
|
||
\texttt{setInputVolume()}, \texttt{setEqBand()}, and related intent-level
|
||
operations without ESP-IDF types.
|
||
|
||
\section{IAudioProfileStore}\label{cls:IAudioProfileStore}
|
||
Persistence boundary for \texttt{AudioProfile} (mixer, EQ, master). Device
|
||
implementation: \texttt{NvsAudioProfileStore}; host tests use fakes.
|
||
|
||
% ------------------------------------------------------------------
|
||
% Application services (Slice 4–5)
|
||
% ------------------------------------------------------------------
|
||
|
||
\section{ITuner}\label{cls:ITuner}
|
||
Abstract tuner boundary in the pure core. Defines boot, band selection,
|
||
status readout, FM/DAB tuning, service list, playback, and volume without
|
||
ESP-IDF types. \texttt{si4684::Si4684Tuner} implements it on device; host
|
||
tests can use fakes.
|
||
|
||
\section{TunerService}\label{cls:TunerService}
|
||
Application service exposing intent-level tuner operations to HTTP and
|
||
future UI. Holds a reference to \texttt{core::ITuner}, tracks last tune
|
||
target and volume, and maps driver failures to \texttt{core::TunerError}.
|
||
|
||
\section{Si4684Tuner}\label{cls:Si4684Tuner}
|
||
\texttt{ITuner} adapter over \texttt{Si4684Driver}. Translates domain calls
|
||
into SPI commands and maps \texttt{Si4684Error} to \texttt{TunerError}.
|
||
Constructed once in \texttt{HardwareBootstrap} alongside the driver.
|
||
|
||
\section{AudioService}\label{cls:AudioService}
|
||
Application service for ADAU1701 mixer, EQ, and master volume
|
||
(Chapter~\ref{ch:adau1701}, Section~\ref{sec:adau1701-service}). Holds a
|
||
reference to \texttt{core::IDsp}, tracks the in-memory \texttt{AudioProfile},
|
||
loads from \texttt{IAudioProfileStore} after boot, and applies changes via
|
||
safeload. Stereo depth and bass enhance levels are merged into the effective
|
||
EQ via \texttt{core::applyEnhancementsToEq()}. Exposed on
|
||
\texttt{/api/audio/*} and the web UI Audio section.
|
||
|
||
\section{EnhanceLevel}\label{cls:EnhanceLevel}
|
||
Strong type for 0--100 enhancement intensity (stereo depth and bass boost).
|
||
Validated at the HTTP boundary by \texttt{core::parseEnhanceLevelJson()}.
|
||
|
||
\section{NvsAudioProfileStore}\label{cls:NvsAudioProfileStore}
|
||
\texttt{IAudioProfileStore} implementation storing serialised
|
||
\texttt{AudioProfile} JSON in NVS namespace \texttt{digiradio}.
|
||
|
||
\section{StationName}\label{cls:StationName}
|
||
Strong type for a preset label (1--32 bytes). Parsed at the HTTP boundary
|
||
via \texttt{StationName::tryFrom()}.
|
||
|
||
\section{BroadcastLabel}\label{cls:BroadcastLabel}
|
||
Trimmed on-air label from RDS or DAB chip byte buffers (up to 128 bytes).
|
||
Built by \texttt{BroadcastLabel::tryFromChipBytes()} in the pure core.
|
||
|
||
\section{RdsMetadataAccumulator}\label{cls:RdsMetadataAccumulator}
|
||
Stateful RDS decoder assembling program service name and radiotext from
|
||
raw block~A--D snapshots returned by the Si4684 driver.
|
||
|
||
\section{DabDynamicLabelAccumulator}\label{cls:DabDynamicLabelAccumulator}
|
||
Reassembles segmented DAB DLS payloads into one dynamic label for
|
||
\texttt{TunerStatus}.
|
||
|
||
\section{PresetSlot}\label{cls:PresetSlot}
|
||
Optional hardware preset button index (1--20). Validated by
|
||
\texttt{PresetSlot::tryFrom()}.
|
||
|
||
\section{Station}\label{cls:Station}
|
||
Immutable preset value: name, band (DAB/FM), tune coordinates, optional
|
||
preset slot. FM presets carry \texttt{FrequencyKHz}; DAB presets carry
|
||
ensemble index and optional service/component ids for playback.
|
||
|
||
\section{StationList}\label{cls:StationList}
|
||
Pure-domain ordered collection (max 20 entries) with add/remove/move and
|
||
duplicate tune-target rejection. Persisted as JSON through
|
||
\texttt{ISecureStore}.
|
||
|
||
\section{StationService}\label{cls:StationService}
|
||
Application service loading/saving presets from NVS and recalling them via
|
||
\texttt{TunerService}. Exposed on \texttt{/api/stations/*} and the Presets
|
||
web UI section.
|
||
|
||
\section{IntegrationService}\label{cls:IntegrationService}
|
||
End-to-end orchestration at boot and on preset recall: loads the station
|
||
list, reapplies the saved \texttt{AudioProfile} after tune, and persists
|
||
the last preset index (NVS key \texttt{last\_preset}). Used by
|
||
\texttt{app\_main} and \texttt{POST /api/stations/tune}.
|
||
|
||
\section{BluetoothService}\label{cls:BluetoothService}
|
||
Application service for BT1035 pairing and A2DP status
|
||
(Chapter~\ref{ch:bt1035}). Delegates to \texttt{Bt1035Driver}; tracks
|
||
whether discoverable mode was requested. Exposed on
|
||
\texttt{/api/bluetooth/*}.
|
||
|
||
\section{OtaService}\label{cls:OtaService}
|
||
Application service wrapping \texttt{esp\_ota\_ops}: streams firmware into
|
||
the inactive OTA slot, validates the app descriptor via
|
||
\texttt{core::validateOtaAppDescriptor()}, and exposes
|
||
\texttt{confirmBoot()} for rollback cancellation after a healthy network boot.
|
||
|
||
\section{WebRadioService}\label{cls:WebRadioService}
|
||
Thread-safe holder for the internet radio streaming config (enabled flag +
|
||
HTTP MP3 URL), backed by \texttt{core::ISecureStore}. A FreeRTOS mutex
|
||
guards an in-RAM copy so \texttt{setConfig()} from an HTTP handler and
|
||
\texttt{config()} polled by the streaming task in
|
||
\texttt{main/web\_radio\_stream.cpp} never race. Exposed on
|
||
\texttt{/api/streaming}.
|
||
|
||
% ------------------------------------------------------------------
|