Files
DigiRadio/Software/docs/manual/ch-classes.tex
T
micheleandClaude Sonnet 5 c0eee4a4ad Firmware review pass: BT1035 boot retry, BluetoothJson tests, doc catch-up
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
2026-08-19 09:23:36 +02:00

362 lines
18 KiB
TeX
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
\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\,000108\,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 45)
% ------------------------------------------------------------------
\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}.
% ------------------------------------------------------------------