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>
writeCommand() had no way to send a non-zero ARG1, since it always
hardcoded ARG1=0x00 before the caller's payload. Every command whose real
ARG1 needed to carry a flag (STCACK, INTACK, SERTYPE, DIGRAD/EVENT ack) or
whose payload needed to start at ARG1 instead of ARG2 was silently broken:
- seekFm(): SEEKUP/WRAP never reached the chip (always ARG2=0x00), so
hardware seek always searched down/no-wrap; masked by the existing
100 kHz software-step fallback in Si4684Tuner.
- startDabService()/stopDabService(): SERVICE_ID/COMPONENT_ID shifted one
byte right of their real ARG4-11 positions, with SERTYPE landing where
the spec requires a fixed 0x00.
- readDabServiceData(): same shift, plus STATUS_ONLY was bit3 (0x08)
instead of the correct bit4 (0x10).
- clearFmStc(), readFmRsq(), readFmRds(), fetchDabServiceList(),
readDabDigRadStatus(), readDabEventStatus(): these AN649 commands have
only ARG1 and no ARG2 at all, so the old two-argument writeCommand()
could never carry their ack/status flags — clearFmStc()'s STCACK never
fired in this driver's history (masked by FM_TUNE_FREQ/FM_SEEK_START
auto-clearing STC per their own spec).
writeCommand() gains a fourth parameter, arg1 (default 0x00, preserving
every already-correct call site); each caller above now passes its flag
through arg1 instead of the payload array.
Confirmed live: first locked:true and first genuine hardware seek (not
software-fallback) in this driver's history — 87.5 -> 98.3 MHz, RSSI +12
dBuV, SNR +14 dB.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0178rASQ6ZETPMUamvpoR2KR
Si4684Driver::getPartInfo()/getSysState() existed but were never called, so
their byte-offset bugs never surfaced. Wired them into a boot-time diagnostic
log (part number, firmware revision, active image, streamed blob byte counts)
to check whether the loaded DAB/FM firmware images are genuine and intact, as
an alternative explanation to the QFN exposed-pad hardware hypothesis.
Fixed two rounds of off-by-one bugs found while doing this: the fields were
initially read one byte too far right (e.g. firmwareBuild was reading a flag
byte, not a version number); the first fix undershot in the other direction
by not accounting for readRaw()'s one-byte SPI lead-in before STATUS0 (already
documented and confirmed elsewhere in this file, in pollStc()) -- caught
because the "fixed" GET_SYS_STATE reported image=192 (0xC0), the exact
signature of STATUS3 with PUP_STATE=3 seen throughout this investigation.
All three response buffers were already sized for the lead-in byte, which
confirmed the correct fix.
Verdict, captured live: blob streamed bytes match local file sizes exactly (no
truncation), GET_SYS_STATE reports image=2 (DAB active, correct), GET_PART_INFO
reports part=4684 (matches expected Si4684 part number) with a plausible
firmware revision -- the loaded DAB firmware is genuine and intact. This closes
the last plausible firmware-side explanation for the FM/DAB no-lock symptom;
docs/si4684-rf-investigation-report.md and docs/TODO.md (P4/H5) updated with
the full record and verdict.
Also noted, not yet fixed: BT1035 AT-init now fails deterministically on every
boot (was a one-off earlier this session) -- see report's Open Items.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Add docs/si4684-rf-investigation-report.md: full record of the FM/DAB no-lock
investigation (crystal, front-end matching, ANTCAP sweep, continuity, leading
EP solder-defect hypothesis, PCBWay report sent) plus the separate audio
profile NVS bug found and partially fixed this session.
- Fix sigma_i2c_write() (SigmaStudioFW.c): no retry on I2C failure meant a single
transient NACK anywhere in a long safeload burst (EQ apply = ~55 sequential
transactions) aborted the whole sequence. Added a 3-attempt retry.
- Add granular failure logging (AudioService::applyProfileToDsp/persistProfile,
Adau1701Driver::applyMixer/applyEq, NvsAudioProfileStore::saveProfile error
codes) to isolate the remaining NVS-side audio profile save failure.
- Si4684Driver::tuneFm gains an optional ANTCAP argument (default 0 = unchanged
auto-tune behavior) used during this session's front-end matching sweep.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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>
Align README, manual, and backlog to 0.8.4; add Web UI System tab for OTA/DSP uploads, serial in health header, FM seek down, and BT1035 paired list plus auto-reconnect API.
Co-authored-by: Cursor <cursoragent@cursor.com>
Enable GPIO_PULLDOWN_ENABLE during gpio_config so RSTB cannot glitch high before gpio_set_level(0), and add manual validation notes explaining why the internal pull-down complements the external one.
Co-authored-by: Cursor <cursoragent@cursor.com>
Ship station reorder, DAB playing ids in presets, RDS PS/RT and DAB DLS in tuner status, Si4684 data-service read, host tests, and UI now-playing lines.
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>