Files
DigiRadio/Software/docs/app-cursor-brief-2026-08-25.md
T
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

12 KiB

Brief per Cursor — allineamento app iOS al firmware DigiRadioFinale

Questo documento è vincolante: ogni punto marcato DEVE è un requisito, non un suggerimento. Se qualcosa non è chiaro o sembra in conflitto con codice esistente, chiedi prima di improvvisare una soluzione diversa — non inventare comportamenti non specificati qui.

Dispositivo di test reale: http://192.168.1.62 (mDNS igiradio-CC4DB4.local — vedi §0bis, è cambiato oggi). Verifica ogni funzionalità contro il dispositivo vero prima di considerarla finita, non solo con dati mock.


0bis. Identità dispositivo rinominata "DigiRadio" → "igiRadio" — OBBLIGATORIO

Oggi il firmware è stato rinominato per coerenza con il nome dell'app. Tre stringhe sono cambiate:

Cosa Prima Ora
SSID WiFi di setup DigiRadio-<seriale> igiRadio-<seriale>
Nome Bluetooth DigiRadio igiRadio
Hostname mDNS digiradio-<seriale>.local igiradio-<seriale>.local

Cerca in tutto il progetto app ogni occorrenza letterale di "DigiRadio" o "digiradio" usata per riconoscere/filtrare il dispositivo (discovery WiFi locale, scan Bluetooth, matching SSID durante il provisioning) — non solo testo di UI. Candidati sospetti da controllare: DigiRadioDiscoveryService, BLEProvisioningService, LocalNetworkScanner. Se il filtro cerca ancora "DigiRadio", l'app smette di trovare il dispositivo reale.


0ter. Nuovo: fallback Bluetooth se il WiFi cade durante l'uso normale

Prima, se il WiFi si perdeva DOPO essersi già connesso una volta (router riavviato, password cambiata, spostato di posto), il firmware ritentava all'infinito senza mai avvisare né dare un modo per riconfigurare — bisognava staccare la corrente. Ora, dopo circa un minuto di disconnessione continua, il dispositivo attiva automaticamente il provisioning Bluetooth (stesso protocollo BLE già usato al primo setup) senza smettere di ritentare il WiFi in background.

Non serve un nuovo endpoint HTTP (il dispositivo potrebbe non essere raggiungibile via WiFi in quel momento, è proprio il punto). Azione consigliata: se l'app ha già una schermata "Cambia rete WiFi" che passa da Bluetooth (dovrebbe già esistere per il primo setup), verifica che scatti anche quando l'app perde la connessione HTTP al dispositivo per un po' — è il segnale che potrebbe essere entrato in questa modalità.


0. Problemi riportati oggi dall'utente (da risolvere tutti)

  1. L'interfaccia non è "in stile Apple" — troppo grezza/disorganizzata.
  2. I nuovi controlli DSP (selettore sorgente, Bass Boost, Stereo Spread) non sono stati implementati.
  3. Non è possibile selezionare la sorgente di ingresso (radio/Bluetooth).
  4. Il controllo del volume è sbagliato/inconsistente.
  5. Le stazioni: uno scan completo produce una lista lunga sia FM che DAB, ma aprendo il tab FM o il tab DAB separatamente dice "nessuna stazione" — vanno unificate in un'unica lista.

Le sezioni seguenti danno il contratto dati esatto e i requisiti UI per risolvere ciascuno di questi punti. Non è accettabile implementare solo una parte e considerare il resto "per dopo" senza dirlo esplicitamente.


1. Selettore sorgente (sostituisce il vecchio mixer) — OBBLIGATORIO

Il firmware non ha più un mixer che combina sorgenti. DEVE esserci un controllo a scelta singola (es. Picker segmented, non due slider separati) con tre opzioni: Radio, Bluetooth, e opzionalmente Test tone (quest'ultimo solo in una sezione diagnostica, non nella UI principale).

Contratto API — PUT /api/audio/profile, corpo completo (tutti i campi sono obbligatori nella richiesta, il firmware non li fa opzionali):

{
  "active_source": "radio",
  "master": {"left_db": 0, "right_db": 0},
  "eq": [
    {"gain_db": 0, "center_hz": 20, "q": 1.414},
    {"gain_db": 0, "center_hz": 100, "q": 1},
    {"gain_db": 0, "center_hz": 400, "q": 1},
    {"gain_db": 0, "center_hz": 1000, "q": 1},
    {"gain_db": 0, "center_hz": 3000, "q": 1},
    {"gain_db": 0, "center_hz": 8000, "q": 1}
  ],
  "enhancements": {"stereo_level": 0, "bass_level": 0}
}

active_source accetta esattamente le stringhe "radio", "bluetooth", "beep" (minuscolo, invariato). Nessun altro valore.

GET /api/audio/profile restituisce lo stesso oggetto — usalo per inizializzare il Picker all'avvio schermata, non assumere sempre "radio".

Errore comune da evitare: NON costruire il body facendo merge parziale di un vecchio oggetto "mixer" — quel campo non esiste più, se lo mandi il parser lo ignora e basta (non causa errore, ma non fa nulla).


2. Volume — contratto esatto

Il volume master è nello stesso oggetto profilo, campo "master":

"master": {"left_db": -6, "right_db": -6}
  • Range valido: -96.0 .. +12.0 (dB). Valori fuori range vengono rifiutati dal firmware (risposta di errore, non clamping silenzioso) — clampa lato client PRIMA di inviare.
  • Non fermare lo slider a 0 dB — il firmware supporta fino a +12 dB di guadagno reale sul master. Se lo slider attuale si ferma a 0 e "sembra basso", è un limite arbitrario della UI, non del firmware: estendi il range fino a +12 dB.
  • Normalmente L e R vanno impostati uguali con un unico slider "Volume" (non serve un controllo stereo separato per il volume master, a meno che non venga esplicitamente richiesto un bilanciamento L/R).
  • Ogni PUT /api/audio/profile sostituisce l'intero profilo — se lo slider del volume invia solo {"master": {...}} senza gli altri campi (active_source, eq, enhancements), il firmware risponde con errore di campo mancante. DEVI sempre inviare l'oggetto completo, leggendo prima lo stato corrente con GET /api/audio/profile (o tenendolo in uno store locale sempre sincronizzato) e poi cambiando solo il campo che l'utente ha toccato.

Questo "invia sempre tutto l'oggetto" è quasi certamente la causa del "volume sbagliato": se il codice attuale manda solo il volume da solo, il firmware lo rifiuta o (se altri campi arrivano con default sbagliati) resetta sorgente/EQ/enhancement senza che l'utente l'abbia chiesto.


3. Bass Boost e Stereo Spread — nuovi controlli, OBBLIGATORI

Due slider 0-100, indipendenti da sorgente/volume/EQ:

POST /api/audio/bass-enhance     {"level": 0-100}
POST /api/audio/stereo-enhance   {"level": 0-100}
  • Questi sono endpoint a sé stanti, non fanno parte del body di /api/audio/profile per la scrittura (ma il loro stato corrente torna dentro GET /api/audio/profile"enhancements": {"bass_level":.., "stereo_level":..}).
  • Bass Boost è un algoritmo dinamico: su un tono fisso di test non si sente quasi nulla, è normale — non è un bug se durante un test con la radio spenta sembra "non fare niente".
  • Non esiste più il vecchio comportamento per cui alzare questi livelli "bloccava" delle bande dell'equalizzatore manuale — l'EQ è ora sempre indipendente. Il campo "locked" per banda nell'array eq[] (in GET /api/audio/profile) è ora sempre false tranne la banda 0 (che è sempre true, invariato da prima — è un passa-alto fisso, non toccarlo).

4. Lista stazioni unificata — OBBLIGATORIO

Il backend ha un solo elenco stazioni, non due:

GET /api/stations

restituisce un array dove ogni stazione ha un campo "band" che vale "fm" oppure "dab", più i campi specifici di banda (fm_frequency_khz per FM; dab_freq_index, dab_service_id, dab_component_id per DAB).

Il bug riportato oggi (lo scan produce una lista lunga, ma il tab FM o il tab DAB dicono "nessuna stazione") indica che l'app sta usando due sorgenti dati diverse — una per la lista generale/scan e una diversa (vuota o rotta) per i tab FM/DAB filtrati. DEVI unificare: un solo StationListStore (o equivalente) alimentato da GET /api/stations, con i tab FM e DAB che sono semplicemente filtri client-side (station.band == .fm / .dab) sulla STESSA lista, non due chiamate/store separati.

UI richiesta: un'unica lista in stile Apple (List con sezioni, o una vista tipo Impostazioni), non due implementazioni diverse per FM e DAB — stesso componente di riga, stesso stile, filtrato per banda.


4bis. RDS (nome stazione FM) — ora funziona davvero, da oggi

Fino a oggi il firmware non decodificava mai l'RDS (bug a tre livelli, risolto). Ora GET /api/tuner/status e lo scan FM completo possono restituire:

"fm": {
  "frequency_khz": 92100,
  "rssi_dbuv": 57,
  "snr_db": 40,
  "station_name": "M DUE O",
  "radiotext": "...testo libero..."
}

station_name e radiotext sono opzionali — compaiono solo dopo qualche secondo di ricezione stabile (l'RDS impiega tempo ad accumularsi), quindi possono mancare subito dopo una sintonizzazione. Mostrali quando presenti (es. nella card "Now Playing" e nella riga della lista stazioni FM), altrimenti mostra la sola frequenza come già fai.

Se durante l'ascolto normale (non solo durante lo scan) il nome stazione o il radiotext cambiano o compaiono per la prima volta, aggiorna la UI di conseguenza — l'utente ha chiesto esplicitamente un piccolo banner/notifica quando arriva un nuovo nome/messaggio RDS durante la riproduzione.


7. VU-meter — nuovo, disponibile da oggi

GET /api/audio/levels

Risposta:

{
  "radio_in_left_db": -1.5,
  "radio_in_right_db": -1.5,
  "bluetooth_in_left_db": -0.9,
  "bluetooth_in_right_db": -1.0,
  "output_left_db": -1.9,
  "output_right_db": -2.0
}

Valori reali in dBFS, letti dal vivo dal DSP ad ogni chiamata — il firmware non fa polling in background né cache. Se vuoi un meter che si aggiorna in tempo reale, il polling lo fai tu lato app (es. ogni 200-500ms mentre la schermata è visibile) — fermalo quando la schermata non è a video, per non generare traffico I2C continuo inutile sul dispositivo. Questa è la sezione "grafica" giusta per barre VU vere (radio in / BT in / uscita), non un'animazione finta.


8. Scan FM completo — NON è bloccato, è solo lento (86s misurati)

POST /api/tuner/scan/full

Fa una scansione dell'intera banda FM (fino a 60 canali candidati, con pausa+lettura RDS per ciascuno) e risponde una sola volta alla fine, misurato: ~86 secondi per uno scan completo. Non è un bug, è il tempo reale che serve per farlo bene (RDS incluso).

Se l'app usa un timeout HTTP standard (30-60s), questa richiesta scade prima che il firmware finisca — la request fallisce lato client, ma il firmware nel frattempo continua e completa comunque (il risultato però va perso perché il client non lo aspetta più). Sembra "bloccato", ma non lo è.

Azione richiesta: per questa chiamata specifica, imposta un timeout di almeno 120 secondi sulla request HTTP, e mostra un indicatore "scansione in corso..." per tutta la durata (non un caricamento breve). POST /api/tuner/scan (senza /full, per una singola stazione con filtro nome) è invece rapido, timeout normale va bene.


8bis. Streaming web radio — bug corretto, ora accetta HTTPS

POST /api/streaming {"enabled":true,"url":"..."} prima rifiutava categoricamente qualsiasi URL https:// (errore invalid_json), accettando solo http:// — dato che quasi tutte le radio via internet reali sono HTTPS-only, questo probabilmente era il motivo per cui "qualsiasi cosa si faccia" dava errore. Corretto oggi: ora accetta sia http:// che https://, e il firmware verifica il certificato TLS con la CA bundle integrata di ESP-IDF. Nessun cambio di contratto per l'app — stessa forma JSON di prima, semplicemente ora funziona anche con URL HTTPS.


9. Checklist di autoverifica prima di considerare il lavoro finito

  • Cambiare sorgente da Radio a Bluetooth nell'app cambia davvero l'audio sul dispositivo reale (non solo lo stato locale dell'app).
  • Cambiare il volume aggiorna il volume reale e non resetta accidentalmente sorgente/EQ/enhancement.
  • Bass Boost e Stereo Spread hanno uno slider visibile, funzionante, e il valore torna corretto dopo un refresh/riavvio app (persistente lato firmware).
  • Il tab FM mostra le stazioni FM dello scan; il tab DAB mostra quelle DAB; entrambe vengono dalla stessa fonte dati.
  • Nessuna schermata mostra contemporaneamente controlli tecnici (indirizzi DSP, tono di test) mescolati a quelli quotidiani.
  • Lo slider del volume arriva fino a +12 dB, non si ferma a 0 dB.
  • I VU-meter (se implementati) mostrano numeri che cambiano nel tempo con l'audio reale, e il polling si ferma quando la schermata non è visibile.