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>
300 lines
12 KiB
Markdown
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.
|