# 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-` | `igiRadio-` | | Nome Bluetooth | `DigiRadio` | `igiRadio` | | Hostname mDNS | `digiradio-.local` | `igiradio-.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.