Extends the existing FM/DAB ANTCAP EEPROM persistence pattern
(Eeprom24aa::writeFmAntCap/writeDabAntCap) to the crystal trim found by
POST /api/tuner/xtal-calibrate, which previously only applied live and
was lost on every reboot.
- Eeprom24aa gains readXtalCalibration()/writeXtalCalibration() at word
addresses 0x02 (ibias), 0x03 (ctun), 0x04-0x07 (xtalFreqHz,
big-endian), right after the existing FM/DAB ANTCAP bytes.
- HardwareBootstrap::boot() now boots ADAU1701 before Si4684 (needed so
the EEPROM read, which borrows ADAU1701's I2C bus, can happen before
Si4684's boot() call, which takes the crystal trim as an argument),
loads the saved trim if present, and falls back to the compiled-in
defaults (ibias=72, ctun=0, xtalFreqHz=19199750) otherwise.
- POST /api/tuner/xtal-calibrate now persists every successful live
recalibration automatically ("persisted":true/false in the response)
via a new saveXtalCalibration()/net::AntennaCalibration::saveXtal
bridge, mirroring the ANTCAP save pattern.
Verified live: boot log confirms "Xtal not calibrated" before the first
save, "Xtal calibration loaded: ibias=72 ctun=0 xtal_freq_hz=19199750"
after, surviving a reboot; DAB/FM tuning unaffected (DAB CNR 17-19dB,
locked).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Root cause of FM/DAB "stonata" audio: xtalCtun=31/nominal XTAL_FREQ
defaults were never measured against this board's actual crystal
(Abracon ABM8-19.200MHZ-10-1-U-T, CL=10pF, two external 15pF load
caps). Fixed via CTUN trim by ear (31->0) plus a new live calibration
loop that reads the Si4684's own FM_RSQ FREQOFF field (broadcast
carriers are GPS-locked, so it's a free precision frequency reference)
to trim XTAL_FREQ with no lab equipment. Converged to CTUN=0,
XTAL_FREQ=19199750 Hz, residual -3/-4 ppm cross-checked on two
stations.
New tools/infrastructure (kept, not one-off):
- Si4684Driver::recalibrateXtal() + POST /api/tuner/xtal-calibrate:
live crystal re-trim without an ESP32 reflash.
- FM_RSQ FREQOFF exposed as "freqoff_ppm" in GET /api/tuner/status.
- tools/si4684_xtal_calibration.py: automates the trim loop.
- tools/si4684_antenna_calibration.py: AN851 Appendix A ANTCAP/VARM/VARB
sweep tool (same session, separate calibration).
- CONFIG_ESP32_I2S_TEST_TONE (off by default): isolates Si4684-specific
audio issues from shared-downstream ones by writing a tone directly
over the I2S bus the Si4684 also uses.
- SIGMA_WRITE_REGISTER_BLOCK/sigma_safeload_block now retry on NACK and
verify via read-back instead of firing I2C writes blind.
- FM de-emphasis set to European 50us (was left at the US 75us default).
- DAB_ACF_ENABLE restored to its previous 0x0000 with a citation
explaining why (tested the datasheet default of 0x0003, made things
audibly worse).
See docs/si4684-rf-investigation-report.md's 2026-08-23 entry for the
full elimination chain and docs/TODO.md for follow-up work (persisting
calibration results to EEPROM instead of requiring a firmware edit).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Extend the FM-only ANTCAP antenna-varactor override to DAB, mirroring the
existing mechanism end to end (driver, tuner, service, EEPROM storage,
HTTP API). Live sweep on real hardware found no ANTCAP value beating
auto-tune on the ensembles tested, so DAB stays on auto-tune by default.
Also fix BT1035 boot: the module's real boot banner doesn't appear until
~18-24s after RESET# releases, not the 3.5s previously waited; add a
2-attempt retry and a baud-rate probe fallback for diagnostics.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Wraps ESP-IDF's official wifi_provisioning manager (BLE transport,
protocomm Security1, NimBLE host) so a phone can join the device to
Wi-Fi over the ESP32-S3's own onboard BLE radio, without first
connecting to the 192.168.4.1 SoftAP. Chosen over a custom GATT
service specifically so the existing generic "ESP BLE Provisioning"
iOS/Android apps work today, before the dedicated DigiRadio app
exists — same standard protocol either app would speak.
net::ble_provisioning::start() is additive, not a replacement: it
runs next to the current SoftAP + POST /api/wifi HTTP route inside
NetBootstrap's startSetupMode(), and failing to start it is
non-fatal (same pattern already used there for the SigmaStudio TCP
bridge) — SoftAP setup keeps working either way. Proof-of-possession
is the device's own serial number (same source as the SoftAP SSID),
so pairing requires reading it off the unit rather than being wide
open. On WIFI_PROV_CRED_SUCCESS the received wifi_sta_config_t is
converted to the same core::WifiCredentials type the HTTP handler
uses and saved through the same ISecureStore, then the device
reboots into STA mode — one persistence path regardless of which
transport provisioned it.
BT1035 is unaffected: it's a separate UART-attached classic
Bluetooth module for A2DP output. This uses the ESP32-S3's
independent internal BLE controller, switched to NimBLE (smaller
footprint than Bluedroid, the only host stack needed for a single
peripheral-role GATT service). App binary still has 33% free flash
after pulling in wifi_provisioning/protocomm/NimBLE.
Verified: idf.py build, doxygen (0 warnings), check-manual-sync,
check_si4684_blobs, ctest (19/19) all green. Not yet tested with a
real BLE provisioning app or on hardware — board is disconnected
this session.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0178rASQ6ZETPMUamvpoR2KR
New endpoint accepts chunked, header-less, 16-bit LE stereo PCM @ 48 kHz
in the request body and writes it straight to the shared I2S sink for as
long as the connection stays open. Chosen deliberately unencoded (no
MP3/AAC/Opus decode) to keep this path as simple and low-risk as
possible — the companion app controls the encoding on its side.
Extracted the I2S TX channel that used to be owned outright by
web_radio_stream.cpp into main/esp32_i2s_sink.cpp, shared by both
producers with a simple tryAcquire()/release() exclusivity guard — web
radio streaming and a phone PCM stream would otherwise fight over the
same physical wire. web_radio_stream.cpp now acquires/releases around
each streamWhileEnabled() cycle instead of owning the channel itself.
net::PhoneStreamSink is a plain function-pointer struct (not a class
hierarchy) threaded through NetBootstrap::start() -> SetupWebServer::start()
-> HttpRouteContext, so components/net stays free of I2S driver headers;
main/phone_stream.cpp supplies the concrete functions (bound to
esp32_i2s_sink) and does the int16->ADAU 32-bit-slot conversion, batched
per chunk rather than per sample for the same reason as the web radio
stutter fix (6974095/7e65394 lineage).
Verified by build only — not confirmed live yet (board disconnected this
session); the actual phone app that will exercise this endpoint doesn't
exist yet either.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0178rASQ6ZETPMUamvpoR2KR
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>
Stream application binaries to the inactive OTA slot via POST /api/system/ota,
validate the esp_app_desc project name in core, and cancel rollback after a
healthy network boot through OtaService::confirmBoot().
Co-authored-by: Cursor <cursoragent@cursor.com>
Read the factory 24AA025E48 serial after ADAU I2C boot, derive SoftAP SSID, BT name, STA hostname, and expose serialNumber on GET /api/health with graceful fallbacks.
Co-authored-by: Cursor <cursoragent@cursor.com>
Replace the services stub with IntegrationService for boot preset recall and tune orchestration, persist last preset in NVS, and remove invalid @return tags that broke CI on T4 metadata headers.
Co-authored-by: Cursor <cursoragent@cursor.com>
Clear doc-block warnings so doxygen exits 0, add GitHub Actions
for host tests, Doxygen, and manual sync, and document CI in the manual.
Co-authored-by: Cursor <cursoragent@cursor.com>
Expose discoverable mode and A2DP control over REST, add persisted
DAB/FM preset list with web UI, and document gaps in docs/TODO.md.
Co-authored-by: Cursor <cursoragent@cursor.com>
Expose Si4684, ADAU1701, and BT1035 boot flags via CompanionChipStatus after
HardwareBootstrap; update API manual schema for Slice 8.
Co-authored-by: Cursor <cursoragent@cursor.com>
Safeload mixer/EQ/master on the ADAU1701, persist AudioProfile in NVS,
expose /api/audio routes and web UI controls, with host tests and manual sync.
Co-authored-by: Cursor <cursoragent@cursor.com>
Implement ESP-IDF walking skeleton with SoftAP, health API, and host
tests, then Slice 2 ISecureStore/NvsSecureStore, STA join, POST
/api/wifi, and the provisioning web UI.
Co-authored-by: Cursor <cursoragent@cursor.com>