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

300 lines
12 KiB
Markdown

# 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):
```json
{
"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"`:
```json
"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:
```json
"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:
```json
{
"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.