Commit Graph
103 Commits
Author SHA1 Message Date
micheleandClaude Sonnet 5 e4ca925801 Fix igiRadio app discovery: publish mDNS service, scan real subnet
The Bonjour browse for _http._tcp in DigiRadioDiscoveryService never
found the device because the firmware only called mdns_hostname_set()
(answers direct A-record lookups) and never mdns_service_add() (needed
for service browse to see it at all).

The app's LAN-scan fallback also assumed a /24 subnet, so it silently
missed devices on networks with a narrower mask (e.g. /22) when the
phone and the device land in different third-octet blocks of the same
network.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018iiTF1KGJBzY9zzpnz9Zr2
2026-09-03 20:00:52 +02:00
micheleandClaude Sonnet 5 5f46486d46 Fall back to BLE Wi-Fi provisioning on sustained post-boot link loss
Once StaClient::connect() had already succeeded once, a later link
loss (router rebooted, password changed, device moved) just retried
esp_wifi_connect() forever with no bound and no fallback -- the only
recovery path was a power cycle, since BLE provisioning only ever
started from startSetupMode() (no stored credentials, or the initial
boot-time connect attempt exhausting its own bounded retry count).

connect() now optionally takes the secure store and device identity;
when set, a link lost for longer than ~1 minute starts BLE
provisioning (net::ble_provisioning::start(), same GATT flow as first-
time setup) alongside the still-ongoing STA reconnect attempts -- it
doesn't stop trying Wi-Fi on its own, it just also gives the app a way
in over Bluetooth if Wi-Fi doesn't come back.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-28 01:07:36 +02:00
micheleandClaude Sonnet 5 36cd85f4d0 Fix web radio reconnect busy-loop on non-2xx HTTP status
openStream() only established the TCP/TLS connection; it never checked
the HTTP status code. A 4xx/5xx response (wrong stream path, station
temporarily down, etc.) still returned a valid client handle, so
streamWhileEnabled() entered its pump loop, immediately read 0 bytes
(no body), and returned -- skipping the kReconnectDelay branch
entirely, which only fired when openStream() itself returned nullptr.
run()'s outer loop then retried immediately: a full TCP+TLS handshake
in a tight loop bounded only by network RTT, not the intended 5s
backoff -- observed live at roughly 2 attempts/second.

This mattered beyond wasted reconnects: a concurrent full FM band scan
(POST /api/tuner/scan/full, which legitimately takes 70-135s) lost its
HTTP connection outright while this loop was running, before this fix.
After adding the status-code check and routing non-2xx through the
same reconnect delay as a failed connection, the same scan completed
cleanly twice in a row under the same conditions.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-26 01:01:56 +02:00
micheleandClaude Sonnet 5 b46dcea698 Fix web radio streaming: HTTPS URLs were rejected outright
POST /api/streaming's JSON parser only accepted "http://" URLs,
rejecting any "https://" URL with invalid_json before ever attempting
a connection. Nearly every real internet radio stream today is HTTPS-
only, so this made the feature fail for essentially any station a
user would actually try.

Two changes were needed together: the parser now accepts both http://
and https://, and web_radio_stream.cpp's esp_http_client now attaches
ESP-IDF's built-in CA certificate bundle (crt_bundle_attach) so the
TLS handshake actually verifies -- CONFIG_MBEDTLS_CERTIFICATE_BUNDLE
was already enabled in sdkconfig but never wired up here. Requires
adding mbedtls to main's PRIV_REQUIRES (esp_crt_bundle.h lives there).

Confirmed live: an https:// stream URL now gets accepted by the API
and produces a real HTTP response (404, from a guessed-wrong path) --
before this fix it never reached the network at all.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-26 00:27:02 +02:00
micheleandClaude Sonnet 5 012d2a6267 Rename device identity from DigiRadio to igiRadio
SoftAP SSID, Bluetooth name, and mDNS hostname now use the "igiRadio"
prefix (igiRadio-<serial>, igiradio-<serial>.local), matching the iOS
app's name instead of the firmware project's repo name. Confirmed live:
igiradio-CC4DB4.local resolves, the old digiradio-CC4DB4.local no
longer does.

Also adds the app-side briefs/notes accumulated today (active_source,
volume contract, VU-meter polling, RDS availability) as a single
up-to-date file to hand to Cursor.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-26 00:06:21 +02:00
micheleandClaude Sonnet 5 d587032a0a Fix FM RDS: station name/RadioText never decoded, three stacked bugs
GET /api/tuner/status and the full FM band scan always returned an
empty station_name, on every frequency, regardless of signal quality
-- confirmed live on multiple >35 dB SNR channels before this fix.
Three independent problems, found by reading the Si4684 datasheet
(AN649 Rev.2.0) instead of guessing further:

1. FM_RDS_CONFIG (property 0x3C02) was enabled (RDSEN=1) but with both
   block-error thresholds at 0 ("no block errors"), so almost any
   real-world RDS group -- routine with ordinary multipath/noise, even
   on a strong signal -- got rejected from the FIFO outright. Raised
   to the datasheet's most tolerant recommended setting (2, "3-5 bit
   errors detected and corrected").

2. FM_RDS_INTERRUPT_FIFO_COUNT (property 0x3C01) was never set at all,
   defaulting to 0 -- which the datasheet states disables RDSFIFOINT
   permanently. Si4684Driver::readFmRds()'s "received" flag reads
   exactly that bit (FM_RDS_STATUS RESP4 bit 0), so it could never be
   true, and Si4684Tuner::refreshStatus()'s RDS poll loop broke out on
   its first iteration every single time, before ever touching the
   accumulator. Set to 1 (fire as soon as one group is queued).

3. The real bug, once groups actually started arriving:
   RdsMetadataAccumulator::applyGroup() read the two Program Service
   name characters from Block C and computed the segment index as
   (blockB >> 1) & 0x3. Per ETSI EN 62106 §3.1.5, PS characters for
   group type 0 (both 0A and 0B) are always in Block D -- Block C
   holds alternate-frequency codes (0A) or a repeated PI code (0B),
   never text -- and the segment address is blockB bits[1:0] with no
   shift. This function had no dedicated test before now, which is
   how a wrong block/bit pair could ship unnoticed: the old test
   fixture encoded the same bug in its "expected" input.

Confirmed live after all three fixes: station_name and radiotext both
populate with stable, plausible content across repeated reads (not
noise) on a real broadcast signal.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-25 23:58:37 +02:00
micheleandClaude Sonnet 5 5d7d4919a0 Add live VU-meter readback via ADAU1701 Data Capture Register
Implements GET /api/audio/levels, reading all six 1×RTA level-detector
cells on demand (no background polling, no caching -- only runs when
called, per explicit request this session).

The read mechanism is the ADAU1701's documented Data Capture Register
(address 2074/0x081A, datasheet Rev.0 pp.30,36: write a (program-step,
register-select) pair to configure what the register mirrors, then
read back a 3-byte 5.19 twos-complement value). The per-meter program-
step indices are taken verbatim from SigmaStudio's own compiler output
(Firmware/ADAU1701-Firmware/IC 1_DigiRadioFinale/net_list_out2/
trap.dat), not invented -- the datasheet explicitly says these indices
must come from that compiler-generated file.

Confirmed live: all six points return distinct, plausible dBFS values,
and the output meters track a master-volume change.

Also folds two pieces of live user feedback into the app brief: the
volume slider should reach +12 dB (the firmware's real ceiling), not
stop at 0 dB, and the new levels endpoint is what a "VU meter" UI
section should poll instead of faking one.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-25 22:33:09 +02:00
micheleandClaude Sonnet 5 3e5cfe814d Integrate DigiRadioFinale DSP program: exclusive source mux, real
Bass Boost1/SPhat1 enhancement blocks

Replaces the 74-parameter ADAU1701 program with the new 224-parameter
SigmaStudio export. The new netlist has no mixer -- MX1 is an
exclusive selector (DC1) passing through exactly one of three stereo
pairs (radio/bluetooth/beep) instead of blending them with independent
gains. core::MixSource/MixerState are gone; core::ActiveSource plus
IDsp::selectSource() replace applyMixer()/setInputVolume() throughout
the driver/service/API stack. AudioProfile.mixer -> activeSource;
the HTTP "mixer" JSON object -> a single "active_source" string.

DC1 turned out to need a raw 32-bit integer (0/1/2), not the 5.23
fixpoint its compiled TYPE_DC1 macro implies -- confirmed live by
writing both encodings and listening for which one actually switched
sources. Documented in Adau1701Driver::selectSource() and
adau1701::paramSourceIndex().

Bass Boost1 and SPhat1 are real ADI algorithm blocks in this revision,
so core::applyEnhancementsToEq() (the old EQ-band-overwrite hack for
bass/stereo enhancement) is deleted; IDsp::setBassBoostLevel()/
setStereoSpreadLevel() scale each block's compiled coefficients toward
identity/unity instead. Confirmed live: Bass Boost audible on real
program content (not a static test tone -- the algorithm is dynamics-
based), Stereo Spread audible but subtle. The EQ "locked" flag from
2026-08-24 (enhancements silently overwriting manually-set bands) no
longer applies to bands 1-5.

A full sweep of all 223 non-DC1 named parameters (writing each back to
its own compiled default) completed with zero failures, confirming the
whole new address space is reachable.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-25 21:53:00 +02:00
micheleandClaude Sonnet 5 b55f2dac9d Document DigiRadioFinale DSP revision, archive SigmaStudio source
Adds the new SigmaStudio project export (Sigmastudio.zip, source
screenshot) alongside the existing project archives in Sigmastudio/,
rewrites docs/adau1701-sigmastudio-analysis.md for the 224-parameter
program (exclusive source mux replacing the old mixer, Bass Boost1/
SPhat1 dedicated blocks, unimplemented VU-meter readback), adds the
DigiRadioFinale figure/table to the manual, and adds a brief for the
iOS app side (active_source API shape, enhancement semantics change)
to hand to Cursor.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-25 21:52:17 +02:00
micheleandClaude Sonnet 5 75cb147f09 Fix SigmaStudio TCP bridge accept() spinning on EBADF forever
Every boot path constructs SigmaStudioTcpServer as a named local,
start()s it, then moves it into NetBootstrap. The moved-from local's
own destructor still runs stop() right after, which used to do an
unconditional activeListenFd().store(-1) -- clobbering the singleton
the moved-to (real, running) instance had just inherited. From then on
acceptLoopTask() called accept(-1, ...) == EBADF forever, on every
single boot, breaking every SigmaStudio Remote Connection attempt.

stop() now only clears the singleton via compare-exchange against its
own listenFd_, so a moved-from husk with no fd of its own leaves the
real instance's registration alone. Keeps a recreateListenSocket()
self-heal in acceptLoopTask() as a safety net for EBADF from any other
future cause, though it's no longer expected to fire in normal
operation.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-25 18:34:28 +02:00
micheleandClaude Sonnet 5 736db623b3 Fix total-audio-silence-on-any-EQ-change: negate ADAU1701 feedback coeffs
designPeakingEq() mapped the RBJ cookbook's a1/a2 straight into the
ADAU1701 Param EQ cell's A0/A1 registers. The cookbook's difference
equation subtracts the feedback terms; the ADAU1701 cell adds them. Any
nonzero band gain therefore applied positive instead of negative
feedback at the target frequency, so the biquad's state diverged and
railed to a constant (inaudible DC) value -- the "any EQ/enhancement
change goes completely silent, even the beep test tone" bug.

Confirmed live: SigmaStudio's own direct safeload writes to the same
registers (correctly signed by its own tool) only distorted, never
silenced, which pointed at this driver's own coefficient math rather
than the DSP chain or the safeload mechanism itself.

Adds a host test asserting Jury stability for the ADD-convention
denominator across the actual bass/stereo-enhance gain values and the
GainDb range extremes, so a regression trips ctest instead of requiring
a live listening test to notice.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-25 18:33:47 +02:00
micheleandClaude Sonnet 5 af51948912 Fix root cause of audio profile never persisting; document ADAU1701 registers
Root cause (found via SigmaStudio firmware analysis requested this
session): NVS was initialized AFTER HardwareBootstrap::boot(), which
internally calls AudioService::loadAndApply() to restore the saved
mixer/EQ/master-volume profile. Every nvs_open() inside
NvsAudioProfileStore::hasProfile()/loadProfile() failed with
ESP_ERR_NVS_NOT_INITIALIZED (0x1101), silently swallowed as "no saved
profile" -- the audio profile was never actually restored on any boot,
regardless of how many times it was saved via PUT /api/audio/profile.
Fixed by moving secure_store::initEncryptedStorage() to the top of
app_main(), before HardwareBootstrap::boot() (nvs_flash_init() has no
hardware dependency, so this is safe).

Second, related bug: HardwareBootstrap::boot() called
AudioService::applyRadioFirstMix() unconditionally right after
loadAndApply(), discarding any just-restored mixer/master values on
every boot. loadAndApply() now returns whether it actually restored a
profile from NVS; the radio-first fallback only applies when nothing
was saved.

Verified live: a distinct mixer+master+5-EQ-band test pattern now
survives a full reboot exactly as saved (previously always reset to
factory default). DAB/FM/BT unaffected.

Also: added a "locked" flag per EQ band in the audio profile JSON --
band 0 is always locked (fixed high-pass, Adau1701Driver::applyEq()
never safeloads it) and bands 1-2/3-5 are locked whenever
bass_level/stereo_level is active (core::applyEnhancementsToEq()
overwrites them with formula-derived values). This was previously
undiscoverable from the API -- GET echoed back the stored, inert value
with no indication it wasn't what was actually playing.

Full register-by-register analysis of the compiled SigmaStudio program
(signal chain, all 74 Parameter RAM addresses grouped by function, HTTP
API mapping, every endpoint tested live) in
docs/adau1701-sigmastudio-analysis.md.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-25 15:52:38 +02:00
micheleandClaude Sonnet 5 a7a5311c2c Persist Si4684 crystal calibration (ibias/ctun/xtalFreqHz) to EEPROM
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>
2026-08-25 15:22:03 +02:00
micheleandClaude Sonnet 5 d17902f19c Add DigiRadio PCB photo to README
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-24 23:21:00 +02:00
micheleandClaude Sonnet 5 7cb25be3f3 Fix audio-profile NVS persistence and whitespace-intolerant JSON parsers
- NvsAudioProfileStore used the key "audio_profile_json" (18 chars),
  exceeding NVS's 15-char key-name limit. Every nvs_set_str call failed
  silently with ESP_ERR_NVS_KEY_TOO_LONG (0x1109): applyProfile() always
  updated live DSP audio correctly, so the bug was invisible except as
  "store_failed" in the HTTP response and settings never surviving a
  reboot. Renamed to "audio_profile" (13 chars); confirmed live, EQ/mixer
  changes now persist across a reset.

- The hand-rolled JSON extractJsonString()/extractJsonBool() helpers
  (duplicated per-file: TunerJson, DspParamJson, StationListJson,
  WebRadioJson, WifiProvisionJson, AudioProfileJson, plus inline mac/name/
  save parsing in BluetoothJson) matched only the exact literal
  `"key":"value"` / `"key":true`, with no tolerance for a space after the
  colon. Standard JSON encoders (e.g. Swift's JSONEncoder in its default,
  non-compact mode) emit `"key": "value"`, which silently failed to parse
  as invalid_json/missing_field. Numeric fields were already fine
  (strtoul/strtof skip leading whitespace per the C standard); fixed only
  the string/bool extractors to skip whitespace after the colon before
  matching the value.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-24 14:55:11 +02:00
micheleandClaude Sonnet 5 38e768edbb Fix ADAU1701 SerialInputRegister IBP override causing persistent hiss
The 2026-08-16 IBP=1 override (commit 6974095) was applied alongside a
separate, simultaneous fix to Si4684's PIN_CONFIG_ENABLE and credited
with turning static into audible music. SerialInputRegister (0x081F) is
a single register shared by every SDATA_INx pin on the ADAU1701, so the
override also applied to the ESP32 streaming input, not just Si4684's.
Live A/B testing today (DAB, FM, and ESP32 web-radio, all isolated via
mixer gain, with and without ADAU1701 DSP bypass, with and without BT
A2DP codec changes) narrowed the hiss to this one shared register.
Removing the override and leaving IBP at its compiled default (0x00)
resolved the hiss on both the Si4684 and ESP32 paths, confirmed by ear.

Also:
- Add a runtime HTTP API (GET/POST /api/bluetooth/a2dp-codec) to change
  the BT1035 A2DP codec bitmask without reflashing, used to rule out
  AAC/SBC codec choice as a contributing cause.
- Extend the ADAU1701 boot-time EQ diagnostic to read back all 6 bands
  (previously band 0 only) using the correct 5.23 fixed-point format.
- Note EEPROM persistence for ANTCAP/crystal calibration as TODO.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-24 14:37:57 +02:00
micheleandClaude Sonnet 5 fae2305164 Fix Si4684 pitch distortion: uncalibrated crystal (CTUN/XTAL_FREQ)
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>
2026-08-23 20:30:42 +02:00
micheleandClaude Sonnet 5 a91c63fc31 Redesign BT1035 RESET#/SYS_CTRL handling, add CTS/RTS diagnostics
RESET# (pin 8) is no longer driven at all: reconfigured as a floating
input relying entirely on the module's own internal pull-up per
datasheet §4.8, as a diagnostic test to rule out host-side RESET# drive
as a contributor to intermittent boot failures.

SYS_CTRL (pin 34) now performs a genuine LOW(2.5s)->HIGH power-cycle on
every resetAndInitOnce() call rather than being asserted once ever:
previously every later retry from bt1035RetryTask silently reused an
already-HIGH SYS_CTRL line without ever actually power-cycling the
module.

Added read-only diagnostics on the CTS/RTS pins (physically wired,
named in board_pins.hpp since their original definition, never
configured by any driver code, host flow control disabled) to observe
their level around the boot-banner wait, after reviewing a sibling
project's PinScope report and re-reading the datasheet's UART flow
control and PA_MUTE default-function documentation.

Across ~45 minutes of live testing after these changes, zero successful
boots were observed - inconclusive on whether this improves anything,
but each change is independently correct per the datasheet. Escalated
to Feasycom support with the full findings. Documented in the RF
investigation report and TODO.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-22 13:58:27 +02:00
micheleandClaude Sonnet 5 3ffc9930cd Remove intra-boot() BT1035 retry loop; restore validated single-attempt design
Git archaeology traced the boot sequence back to fd9d4ae (2026-08-15,
documented 5/5 clean boots), which removed a redundant AT+RESET and added
the boot-banner listen window in a single commit. Comparing that
validated design to today's working tree found one real structural
deviation: an intra-boot() retry loop (2 attempts, only 300ms between
hardware reset pulses) added earlier today, which never existed in the
validated baseline. The BT1035 datasheet's own Reset Protection timeout
(typically >1.8s) means a second pulse fired only 300ms later may not
reach a clean power-off state before repowering.

Removed the intra-boot() retry loop entirely (kBootAttempts,
kBootRetryDelayMs deleted) — boot() now makes exactly one attempt per
call, matching fd9d4ae. Retries remain exclusively at the
bt1035RetryTask level (whole clean boot() calls, confirmed live at
~31.8s apart). Banner wait (25s) and GPIO readback left untouched.

Documented the full commit-by-commit analysis and live test result
(structurally correct, hit-rate inconclusive on this sample) in the RF
investigation report and TODO for future sessions.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-21 17:11:36 +02:00
micheleandClaude Sonnet 5 f6dc9916a4 Add BT1035 background boot retry; document intermittent silence findings
BT1035 boot can fail with total UART silence even after the banner-timing
fix, on otherwise-identical, correctly-powered hardware. Confirmed via
multimeter (VBAT_IN, 1.8V_OUT, SYS_CTRL/RESET, TX all normal) and by
observing the same physical module both succeed and fail across different
boot attempts in one session, that this is intermittent, not a dead
module — the crystal is sealed inside the module and not inspectable or
fixable from our side.

Since the fault self-clears on a later attempt, mitigate with an
indefinite background retry task: if the initial boot() fails, keep
retrying with no artificial delay (each attempt already takes ~25-60s) so
a temporary failure becomes a bounded, self-recovering delay instead of
requiring a manual power cycle. Documented the full diagnostic session,
including the ruled-out theories and a possible future ESP32-S31 (native
Bluetooth Classic) migration path, in the RF investigation report.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-21 09:32:07 +02:00
micheleandClaude Sonnet 5 3a58d33aad Add DAB ANTCAP calibration and fix BT1035 boot banner timing
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>
2026-08-21 08:12:08 +02:00
micheleandCursor 9ad2e42fe2 Add audio profile presets tab with user-saved profiles and EQ editor.
Replace separate Audio/EQ tabs with a unified profile picker (built-in + local presets), keeping mixer access and applying full profiles to the device.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-18 17:06:59 +02:00
micheleandCursor a1aaefd47a Add multi-source streaming to igiRadio (web radio, URL, local files).
Supports POST /api/streaming for HTTP MP3 stations and phone PCM push for on-device audio files, with a dedicated Stream tab and preset for Radio Monte Carlo.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-18 16:15:21 +02:00
micheleandCursor 3afcba0d36 Polish igiRadio UI with premium audio, home, and FM screens.
Adds graphic EQ, vertical mixer faders, redesigned home and FM layouts, and an Audio tab for quicker access to DSP controls.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-18 15:56:51 +02:00
micheleandCursor 6087bb5297 Add igiRadio iOS/iPadOS app for DigiRadio control.
Introduces a new SwiftUI app with HTTP REST device control, mock mode for UI development, BLE-based discovery, and documented architecture aligned with the firmware API.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-18 15:45:55 +02:00
micheleandClaude Sonnet 5 3244b9f084 Document previously-missing HTTP API endpoints and BLE provisioning
ch-api.tex only covered a subset of the routes SetupWebServer.cpp
actually registers. Backfilled the ones that had grown undocumented
over the last several sessions, plus this session's three new ones:

- POST /api/wifi/scan (existed, undocumented)
- POST /api/tuner/scan, POST /api/tuner/scan/full (full FM band scan,
  this session's item 1)
- POST /api/audio/beep, GET /api/dsp/params, PUT /api/dsp/param
  (generic ADAU1701 parameter access, this session's item 4)
- PUT /api/stream/phone (phone PCM streaming, item 2, commit e8f79c4)
- GET/POST /api/streaming (web radio config, existed, undocumented)
- POST /api/bluetooth/scan, POST /api/bluetooth/connect,
  GET/POST/DELETE /api/bluetooth/speaker, POST /api/bluetooth/reconnect
  (existed, undocumented)

Also added a subsection under "Boot and network state machine"
covering BLE provisioning (commit 74c40ee) — it isn't an HTTP
endpoint so it doesn't fit the \apiendpoint table, but belongs next
to the SoftAP/STA state description it's additive to.

Verified: tools/check-manual-sync.py passes, and a full two-pass
xelatex build of the manual compiles clean (no undefined references,
no errors) with the new sections in place.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0178rASQ6ZETPMUamvpoR2KR
2026-08-18 12:24:05 +02:00
micheleandClaude Sonnet 5 74c40ee986 Add BLE Wi-Fi provisioning alongside the setup SoftAP
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
2026-08-18 08:19:02 +02:00
micheleandClaude Sonnet 5 e8f79c4b79 Add PUT /api/stream/phone: raw PCM audio push from a phone app
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
2026-08-18 08:03:39 +02:00
micheleandClaude Sonnet 5 39984a97d1 Add generic ADAU1701 parameter access (GET/PUT /api/dsp/param)
New core::IDsp::writeRawParam(address, value) safeloads any named
Parameter RAM cell in the compiled SigmaStudio program (74 cells: mixer/
Beep1/PEQ coefficients/limiter thresholds/etc — the same set SigmaStudio's
own Remote Connection, components/net/SigmaStudioTcpServer, already has
full access to). Value is a plain SigmaStudio floating coefficient,
converted to ADAU 8.23 fixpoint via the existing core::floatToFixpoint823.

Adau1701ParamTable.hpp holds the name->address table extracted from
Firmware/ADAU1701-Firmware/DigiRadio_IC_1_PARAM.h; core::DspParamJson
handles the wire format without core/ depending on the adau1701 driver
(net/SetupWebServer.cpp bridges the two, consistent with how other
JSON DTOs are supplied their data by the net layer).

GET /api/dsp/params lists every cell (name + address) for discovery.
PUT /api/dsp/param {"name":...,"value":...} writes one. Deliberately no
domain validation, matching the trust level already implied by the
existing SigmaStudio TCP bridge being reachable on the same network.
HTTP routes were registered in the SetupWebServer.cpp change committed
alongside the FM band scan feature (3a10ed7).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0178rASQ6ZETPMUamvpoR2KR
2026-08-18 07:51:09 +02:00
micheleandClaude Sonnet 5 7e65394100 Fix web radio streaming stutter: batch I2S writes, widen DMA buffer
writeFrame() called i2s_channel_write() once per decoded PCM sample (up to
1152 separate driver calls per MP3 frame), each with its own locking/DMA
bookkeeping overhead. Now accumulates a whole frame into one buffer and
writes it in a single call.

The I2S TX channel also used the ESP-IDF default DMA config (6 descriptors
x 240 frames = ~30 ms of buffering at 48 kHz), leaving almost no headroom
against network jitter in this single-task fetch+decode+play pipeline.
Widened to 12 x 480 (~120 ms) so a brief HTTP stall doesn't immediately
starve the DMA and audibly crackle.

Both are static/architectural fixes verified by build only — not confirmed
live yet (board disconnected this session).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0178rASQ6ZETPMUamvpoR2KR
2026-08-18 07:50:48 +02:00
micheleandClaude Sonnet 5 3a10ed75aa Add full FM band scan for building a station/channel list
New TunerService::scanFullFmBand(): tunes to the band bottom then seeks up
repeatedly (reusing the existing hardware-seek + RDS-name-poll machinery
from scanForStation()) until the sweep wraps back around, collecting every
station that clears the existing scan RSSI/SNR thresholds. Returns the
list without touching saved presets or leaving the tuner in any particular
place — callers decide what to do with the results.

New POST /api/tuner/scan/full endpoint (core::TunerFmScannedStation DTO,
serializeTunerFmBandScanJson). Blocks for the whole sweep like the existing
/api/tuner/scan.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0178rASQ6ZETPMUamvpoR2KR
2026-08-18 07:50:28 +02:00
micheleandClaude Sonnet 5 8a69cbed08 Fix Si4684 DAB response-parsing bugs found in firmware review
readDabServiceData(): 6 of 7 response fields (dataSrc, serviceId,
componentId, byteCount, segmentIndex, segmentCount) read one byte too
early, using the old raw[4]=RESP4 convention instead of the correct
raw[5]=RESP4 (established elsewhere in this driver by getPartInfo() and
readDabDigRadStatus()'s own ficQuality/cnrDb fields). dataSrc landing on
the wrong byte meant the DAB dynamic label (PAD/now-playing text) check
(dataSrc == 2) could essentially never match — it has likely never worked.
Header buffer grown 24->25 bytes to fit the correctly-positioned last field.

fetchDabServiceList(): didn't match AN649 Table 14's "DAB/DMB Digital
Service List" layout at all — serviceCount read from the wrong byte, every
per-service field misaligned, componentId assumed 4 bytes wide (actually
2 per the spec), and only the first of a service's possibly-several
components was ever skipped past (desyncing every later entry). Rewrote
against the actual Table 14 field layout. Confirmed live yesterday this
was producing garbled service_id/component_id/label output
(component_id values decoding as literal ASCII spaces).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0178rASQ6ZETPMUamvpoR2KR
2026-08-18 07:50:09 +02:00
micheleandClaude Sonnet 5 9b337e7bca Fix Si4684 response-parsing offset bug: RDS, DAB acquired/event status
readFmRds(), readDabDigRadStatus()'s acquired field, and
readDabEventStatus() all read raw[4] expecting AN649's RESP4 field, but
this driver's own established convention (getPartInfo(), and the
already-correct ficQuality/cnrDb fields in readDabDigRadStatus() itself)
is raw[5]=RESP4 (raw[0]=SPI lead-in, raw[1..4]=STATUS0-3). This is why
DAB_GET_EVENT_STATUS's serviceListReady never set — it was reading
STATUS3's ERRNR bit instead of RESP4's SVRLISTINT bit, so
/api/tuner/services returned service_list_empty forever regardless of lock
quality. Fixed all four sites; readFmRds()'s fifoUsed/blockA-D were
consequently also off by one and fixed together with it.

Confirmed live: first DAB ensemble locks in this project's history (3 found
sweeping freq_index 0-35, fic_quality=100, best CNR 20 dB on index 23), and
/api/tuner/services now returns real entries instead of service_list_empty.
The service-list entry contents themselves are still garbled (a third,
separate bug in fetchDabServiceList()'s body parsing, documented but not
fixed this session — see docs/si4684-rf-investigation-report.md).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0178rASQ6ZETPMUamvpoR2KR
2026-08-16 01:24:54 +02:00
micheleandClaude Sonnet 5 6974095f35 Fix Si4684 I2S audio: PIN_CONFIG_ENABLE conflict + ADAU1701 BCLK polarity
Two independent bugs silenced the digital audio path from Si4684 into the
ADAU1701 even after the FM/DAB tune fix produced a real RF lock:

1. Si4684Driver::configureAfterBoot() wrote PIN_CONFIG_ENABLE (0x0800) as
   0x0003, enabling both I2SOUTEN and DACOUTEN. AN649: "only I2SOUTEN or
   DACOUTEN can be enabled at a time. If both enabled, only analog audio
   output is enabled" — the chip silently fell back to its unused analog
   DAC output on every boot. Fixed to 0x8002 (I2SOUTEN + INTBOUTEN),
   matching the value hitech95/si468x_dab_receiver's working ALSA codec
   driver uses (SI468X_PROP_I2S_ENABLED); I2SOUTEN alone (0x0002) was not
   sufficient on this hardware. Also fixed AUDIO_OUTPUT_CONFIG (0x0302),
   which was being written with a stray I2S-enable bit that property does
   not have (its only real field is bit0 MONO).

2. Even with the chip correctly outputting I2S, the ADAU1701 received only
   static. Traced with SIGMA_WRITE_REGISTER_BLOCK live overrides of
   SerialInputRegister (0x081F, baked into the compiled SigmaStudio export
   at ILP=0/IBP=0): IBP=1 (input data clocked on the opposite BCLK edge)
   produced real, recognizable music instead of static on a locked, strong
   FM signal — first confirmed end-to-end audio in this project's history.
   ILP=1 made it worse and was reverted; IBP=1 kept as a runtime override
   in Adau1701Driver::boot().

Remaining noise on top of the music is attributed to antenna quality
(RSSI/SNR fluctuated significantly between retunes of the same station on
the current improvised antenna) — not yet confirmed with a proper antenna.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0178rASQ6ZETPMUamvpoR2KR
2026-08-16 01:12:57 +02:00
micheleandClaude Sonnet 5 f5fe92f93e Fix remaining Si4684 ARG-offset bugs: seek, DAB services, RSQ/RDS/DIGRAD acks
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
2026-08-16 00:33:46 +02:00
micheleandClaude Sonnet 5 ef15e9839c Fix Si4684 FM/DAB tune commands: ARG offset bug caused total RF blackout
writeCommand() always prepends a fixed ARG1=0x00 byte before the payload;
tuneFm()/tuneDab() built their argument arrays starting at what the author
believed was ARG1, so every byte landed one slot right of its real AN649
position and an extra unused byte was appended. The chip never received the
requested frequency. This is the root cause of the months-long total RF
blackout (RSQ frozen at all-zero on every frequency/ANTCAP value) previously
attributed to a QFN exposed-pad hardware defect — that hypothesis is now
overturned, confirmed live: RSSI/SNR now read real, frequency-dependent
values with zero STC timeouts after the fix.

Also make BT1035 boot failure non-fatal in HardwareBootstrap::boot() so a
companion-chip fault no longer halts the whole device (Si4684 tuning, web
UI, Wi-Fi already isolate BT1035 readiness via CompanionChipStatus).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0178rASQ6ZETPMUamvpoR2KR
2026-08-16 00:19:43 +02:00
micheleandClaude Sonnet 5 bc1b1b1472 Si4684 RF report: FM blob verified genuine, PCBWay dispute closed, BT1035 finding logged
FM blob confirmed byte-perfect and reproduces the identical no-lock symptom
seen on DAB, strengthening the hardware-side hypothesis over firmware.
PCBWay closed the dispute without X-ray verification. Records the BT1035
AT-init root cause (software, not hardware) as a calibration note against
over-attributing intermittent symptoms to physical damage.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-15 23:02:48 +02:00
micheleandClaude Sonnet 5 fd9d4aed5c Fix BT1035 boot regression: drop redundant software AT+RESET, widen boot-banner listen window
Redundant AT+RESET sent right after the hardware RESET# pulse could interrupt
the module mid bring-up; boot-banner probe window (1500ms) was too short for
the real +VER banner (~5s), causing spurious "AT init failed". 5/5 clean
boots after fix vs ~1/13 before.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-15 23:02:48 +02:00
micheleandClaude Sonnet 5 42dadea369 Fix GET_PART_INFO/GET_SYS_STATE/GET_FUNC_INFO byte offsets; confirm Si4684 blobs genuine
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>
2026-08-13 22:53:27 +02:00
micheleandClaude Sonnet 5 bee8cfa86f Si4684 RF investigation report; fix ADAU1701 I2C reliability under long safeload bursts
- 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>
2026-08-13 22:21:04 +02:00
micheleandClaude Sonnet 5 6f7b6dd12c Add internet radio streaming with runtime API, modernize web UI, remove auto-tune/beep at boot
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>
2026-08-08 21:15:22 +02:00
micheleandClaude Sonnet 4.6 8a8523515d Add CLAUDE.md with build commands and architecture overview
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-05 20:26:35 +02:00
micheleandCursor 7d3369b098 Fix Wi-Fi credential save with plain NVS (encryption off).
Initialise NVS before integration startup, recover from encrypted-partition mismatches without re-enabling encryption, and improve provisioning logs and POST body handling.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-05 14:10:00 +02:00
micheleandCursor 8609d038be Add BOM reference datasheets and August fab outputs.
Bundle crystal, LDO, ESD, and module PDFs under Hardware/DATASHEET plus ODB++ gerbers and pick-and-place for the 2026-08-05 board release.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-05 14:01:54 +02:00
michele 5204f750ef aggiornamento SetupWebServer 2026-08-05 14:00:35 +02:00
michele 6ca40f1b1d Full boot working: ADAU1701 DSP fallback (probe empty partition), BT1035 flow control disabled + reset timing — all companion chips ready 2026-08-05 13:25:24 +02:00
michele 34f247019a Si4684 boot fix: flash encryption off, 16MB flash, NVS enc off, main task stack 8KB, CTS timeout 10s (fixes BootCmd error 6), DMA-safe HOST_LOAD buffer 2026-08-05 08:57:43 +02:00
micheleandCursor a28120b44e Sync hardware deliverables and manual after pre-fab design review.
Refresh BOM (2026-07-09 CSV), Gerbers, EasyEDA project, pick-and-place, and schematic export; document corrected ADAU crystal/PLL strapping and Si4684 reference clock in the manual; add PCBWay acknowledgement on the back cover only.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-09 15:53:27 +02:00
micheleandCursor fcda4ef151 Fix manual typographic overflow in tables and API chapter.
Add wrapping table columns, breakable paths, and ragged callout boxes in digiradio-manual.sty, then reflow long endpoint headings, JSON examples, and hardware/driver tables to stay within page margins.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-08 15:02:30 +02:00
micheleandCursor ad5e4827a8 Stop tracking LaTeX build artifacts under Software/docs/manual.
Add gitignore rules for aux, log, toc, fls, fdb_latexmk, listing, out, and pdf so only source .tex/.sty stay in version control.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-08 14:43:15 +02:00