6.5 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Project Overview
DigiRadio is an ESP32-S3 embedded Hi-Fi DAB+/FM radio firmware. Hardware companion chips:
- Si4684 — DAB+/FM tuner over SPI (AN649 boot sequence)
- ADAU1701 — SigmaDSP audio processor over I2C (safeload for click-free live updates)
- FSC-BT1035 (QCC3056) — Bluetooth A2DP module over UART + AT commands, I2S slave from ADAU1701
- 24AA025E48 — EEPROM with factory EUI-48 for device identity
Read AGENTS.md first — it is the definitive coding rules and Definition of Done.
Build & Development Commands
Device Firmware (ESP-IDF v5.5.x)
idf.py set-target esp32s3 # first time only
idf.py build
idf.py erase-flash flash monitor # first flash
idf.py flash monitor # subsequent flashes
idf.py monitor # monitor only
Serial port: /dev/cu.usbmodem1101
Host Unit Tests (no hardware required)
# Configure (once, or after CMakeLists changes)
cmake -S components/core/test -B build-host \
-DCMAKE_CXX_COMPILER="$(brew --prefix llvm)/bin/clang++"
# Build and run all tests
cmake --build build-host
ctest --test-dir build-host --output-on-failure
# Run a single test
ctest --test-dir build-host -R bt1035_at_test --output-on-failure
Quality Gates (must all pass before commit)
ctest --test-dir build-host --output-on-failure
doxygen Doxyfile # Doxyfile has WARN_AS_ERROR = FAIL_ON_WARNINGS
python3 tools/check-manual-sync.py # public classes must have manual sections
python3 tools/check_si4684_blobs.py # blob checksums
Other Tools
tools/gzip-www.sh # regenerate index.html.gz after editing the web UI
python3 tools/fetch_si4684_firmware.py # download Si4684 firmware blobs (not in git)
Architecture
Layered Design
Imperative Shell → drivers/, services/, net/, main/
↓ dependency injection
Application Services → TunerService, AudioService, BluetoothService, etc.
↓ interface references
Pure Domain Core → components/core/ (zero ESP-IDF headers, host-testable)
The split is strict: never add ESP-IDF includes to components/core/. Domain logic lives there as pure C++23 with std::expected<T,E> for errors, no exceptions (CONFIG_COMPILER_CXX_EXCEPTIONS=n).
Boot Flow
-
app_main()→HardwareBootstrap::boot()- Si4684: POWER_UP → stream ROM patch + image → BOOT
- ADAU1701: RESET# → replay I2C cell writes from SigmaStudio export
- BT1035:
AT+RESET→AT+AUXCFG=3→AT+I2SCFG=67(I2S slave from ADAU) — must appear in boot log - 24AA025E48: read EUI-48 → derive serial number
-
secure_store::initEncryptedStorage()— encrypted NVS (dev mode: encryption off) -
IntegrationService::bootLoadLastPreset()— restore saved frequency + audio profile + mixer -
NetBootstrap::start()— STA (if creds saved) or SoftAP (DigiRadio-<serial>), HTTP server port 80
GPIO Pinout (main/board_pins.hpp — single source of truth)
| Chip | Pins |
|---|---|
| Si4684 | SPI: SCLK=13 MOSI=12 MISO=9 CS=8, RSTB#=38, INTB#=39 |
| ADAU1701 | I2C SDA=4 SCL=5 RSTB#=47 addr=0x34 |
| BT1035 | UART TX=40 RX=41 RTS=14 CTS=21, RESET=17, SYS_CTL=15 |
| I2S | BCLK=6 (slave in), LRCLK=7 (slave in), DATA_OUT=16 |
| 24AA025E48 | I2C addr=0x52 (shared bus with ADAU1701) |
Key Abstractions in components/core/
Interfaces (injected into services):
ITuner— boot, tuneFm, tuneDab, seek, readRsq, readServices, playServiceIDsp— applyProfile, applyMixer, applyEq, setInputVolume, setMasterVolume (all safeload)ISecureStore— hasWifi, loadWifi, saveWifi, stations CRUD, audio profileIFirmwareBlobReader— stream blob chunks without heap allocation
Value types: FrequencyKHz, GainDb, AudioProfile (mixer + 6-band EQ + enhancements), Station, StationList, DeviceIdentity, Secret (no-log, zeroed on destruction)
DSP math: BiquadDesign converts Hz/Q/dB → ADAU1701 fixed-point coefficients (pure math, tested)
Error Handling
All fallible operations return std::expected<T, ErrorType>. Core error types: TunerError, DspError, Bt1035Error, StoreError, NetError, OtaError. Never swallow errors.
HTTP REST API
Served at port 80. Key routes:
GET /api/health— chip status + serial numberPOST /api/tuner/tune—{"band":"fm","frequency":100900}POST /api/tuner/seek—{"direction":"up"/"down"}GET|PUT /api/audio/profile— full mixer + EQ JSONPOST /api/bluetooth/pair,GET /api/bluetooth/paired,POST /api/bluetooth/disconnectGET|POST /api/stations,POST /api/stations/tunePOST /api/wifi— STA provisioning (triggers reboot)POST /api/system/ota— firmware image upload (reboot)POST /api/dsp/program— ADAU1701 DRAD blob upload (reboot)
Dual OTA & Partitions (partitions.csv)
Slots: nvs, otadata, ota_0 (4 MB), ota_1 (4 MB), dsp (40 KB blob), nvs_keys. OtaService streams to inactive slot, validates app descriptor, selects new boot slot.
Important Files
| Path | Purpose |
|---|---|
AGENTS.md |
Coding rules, DoD, subsystem specs — read first |
instructions.md |
Task roadmap |
main/board_pins.hpp |
GPIO pinout (single source of truth) |
main/hardware_bootstrap.cpp |
Driver boot orchestration |
sdkconfig.defaults |
C++23, exceptions off, dev encryption settings |
sdkconfig.defaults.production |
Flash encryption RELEASE mode |
partitions.csv |
Flash layout |
docs/security-flash-nvs.md |
NVS/flash encryption setup + HIL checklist |
.cursor/rules/ |
Per-subsystem Cursor AI rules |
components/net/www/index.html |
Web UI source (gzip with tools/gzip-www.sh after edits) |
Hard Rules
- Never invent register addresses, opcodes, or boot sequences. Cite the datasheet section (AN649 for Si4684; Feasycom guide §5.1 for BT1035).
- No virtual calls in IRAM ISRs (vtables in flash, inaccessible during cache disable).
- No dynamic allocation in audio hot paths.
- No ESP-IDF includes in
components/core/— this breaks host tests. - Complexity ≤ 7 per method; method body fits an 80×24 terminal.
- Every public class needs a Doxygen block with
@dname,@param,@return,@pubstate. - Every file needs an Apache-2.0 license header.