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>
This commit is contained in:
2026-08-26 00:06:21 +02:00
co-authored by Claude Sonnet 5
parent d587032a0a
commit 012d2a6267
6 changed files with 148 additions and 14 deletions
@@ -18,9 +18,9 @@ namespace core {
namespace { namespace {
constexpr std::string_view kSerialUnknown = "unknown"; constexpr std::string_view kSerialUnknown = "unknown";
constexpr std::string_view kSoftApFallback = "DigiRadio-setup"; constexpr std::string_view kSoftApFallback = "igiRadio-setup";
constexpr std::string_view kBluetoothFallback = "DigiRadio"; constexpr std::string_view kBluetoothFallback = "igiRadio";
constexpr std::string_view kHostnameFallback = "digiradio"; constexpr std::string_view kHostnameFallback = "igiradio";
[[nodiscard]] std::string prefixed(std::string_view prefix, [[nodiscard]] std::string prefixed(std::string_view prefix,
std::string_view suffix) std::string_view suffix)
@@ -44,9 +44,9 @@ DeviceIdentity DeviceIdentity::fromEui48(Eui48 eui)
const std::string suffix = eui.shortSuffix(); const std::string suffix = eui.shortSuffix();
return DeviceIdentity(eui, return DeviceIdentity(eui,
eui.serialNumber(), eui.serialNumber(),
prefixed("DigiRadio-", suffix), prefixed("igiRadio-", suffix),
prefixed("DigiRadio-", suffix), prefixed("igiRadio-", suffix),
prefixed("digiradio-", suffix)); prefixed("igiradio-", suffix));
} }
bool DeviceIdentity::isKnown() const noexcept bool DeviceIdentity::isKnown() const noexcept
@@ -52,13 +52,13 @@ namespace {
if (!expectEqual(identity.serialNumber(), "0004A3123456")) { if (!expectEqual(identity.serialNumber(), "0004A3123456")) {
return EXIT_FAILURE; return EXIT_FAILURE;
} }
if (!expectEqual(identity.softApSsid(), "DigiRadio-123456")) { if (!expectEqual(identity.softApSsid(), "igiRadio-123456")) {
return EXIT_FAILURE; return EXIT_FAILURE;
} }
if (!expectEqual(identity.bluetoothName(), "DigiRadio-123456")) { if (!expectEqual(identity.bluetoothName(), "igiRadio-123456")) {
return EXIT_FAILURE; return EXIT_FAILURE;
} }
if (!expectEqual(identity.hostname(), "digiradio-123456")) { if (!expectEqual(identity.hostname(), "igiradio-123456")) {
return EXIT_FAILURE; return EXIT_FAILURE;
} }
return EXIT_SUCCESS; return EXIT_SUCCESS;
@@ -74,13 +74,13 @@ namespace {
if (!expectEqual(identity.serialNumber(), "unknown")) { if (!expectEqual(identity.serialNumber(), "unknown")) {
return EXIT_FAILURE; return EXIT_FAILURE;
} }
if (!expectEqual(identity.softApSsid(), "DigiRadio-setup")) { if (!expectEqual(identity.softApSsid(), "igiRadio-setup")) {
return EXIT_FAILURE; return EXIT_FAILURE;
} }
if (!expectEqual(identity.bluetoothName(), "DigiRadio")) { if (!expectEqual(identity.bluetoothName(), "igiRadio")) {
return EXIT_FAILURE; return EXIT_FAILURE;
} }
if (!expectEqual(identity.hostname(), "digiradio")) { if (!expectEqual(identity.hostname(), "igiradio")) {
return EXIT_FAILURE; return EXIT_FAILURE;
} }
return EXIT_SUCCESS; return EXIT_SUCCESS;
@@ -41,7 +41,7 @@ public:
* @brief setupDefault — factory for the Slice 1 setup SoftAP. * @brief setupDefault — factory for the Slice 1 setup SoftAP.
* *
* @dname setupDefault * @dname setupDefault
* @return SoftApConfig with SSID DigiRadio-setup. * @return SoftApConfig with SSID igiRadio-setup.
* @pubstate none * @pubstate none
* *
* @author Michele Bigi * @author Michele Bigi
+1 -1
View File
@@ -21,7 +21,7 @@
namespace net { namespace net {
namespace { namespace {
constexpr std::string_view kSetupSsid = "DigiRadio-setup"; constexpr std::string_view kSetupSsid = "igiRadio-setup";
constexpr std::uint8_t kSetupChannel = 1; constexpr std::uint8_t kSetupChannel = 1;
constexpr std::uint8_t kSetupMaxConnections = 4; constexpr std::uint8_t kSetupMaxConnections = 4;
} // namespace } // namespace
@@ -147,6 +147,35 @@ 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.
---
## 5. Stile UI — Apple, minimalista ma con una sezione grafica curata ## 5. Stile UI — Apple, minimalista ma con una sezione grafica curata
- Componenti nativi SwiftUI: `Picker` segmented per la sorgente, `Slider` con - Componenti nativi SwiftUI: `Picker` segmented per la sorgente, `Slider` con
@@ -0,0 +1,105 @@
# Nota per Cursor — Volume master e VU-meter
Due punti precisi da correggere/implementare nell'app. Segui esattamente i
contratti sotto, non improvvisare formati diversi.
Dispositivo di test: `http://192.168.1.62`.
---
## 1. Volume master
**Endpoint**: fa parte del profilo audio completo, non ha una rotta a sé.
```
GET /api/audio/profile -> legge lo stato attuale (incluso "master")
PUT /api/audio/profile -> scrive il profilo COMPLETO
```
Campo:
```json
"master": {"left_db": 0, "right_db": 0}
```
Regole obbligatorie:
1. **Range reale: da -96.0 a +12.0 dB.** Lo slider volume in UI non deve
fermarsi a 0 dB — quello è solo "unity gain", non il massimo. Il massimo
vero è **+12 dB**. Se oggi lo slider arriva solo a 0, è un limite messo
nell'app, va tolto.
2. **`PUT /api/audio/profile` sostituisce l'intero oggetto**, non solo il
volume. Ogni volta che l'utente muove lo slider del volume, il body della
PUT deve contenere ANCHE `active_source`, `eq` (tutte e 6 le bande) ed
`enhancements`, con i valori correnti — non solo `{"master": {...}}`.
Il modo corretto:
- tieni sempre in memoria (o rileggi con GET) lo stato completo del
profilo;
- quando l'utente cambia il volume, aggiorna SOLO il campo `master` in
quello stato locale;
- invia l'intero oggetto aggiornato con PUT.
3. Normalmente `left_db` e `right_db` vanno impostati **uguali** con un unico
slider "Volume" (non serve un secondo controllo per il bilanciamento L/R,
a meno che non venga chiesto esplicitamente).
4. Valori fuori range (-96/+12) vengono rifiutati dal firmware con errore —
clampa lato client prima di inviare.
Esempio completo di richiesta corretta (cambio solo il volume a -6 dB,
tutto il resto invariato):
```json
PUT /api/audio/profile
{
"active_source": "radio",
"master": {"left_db": -6, "right_db": -6},
"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}
}
```
---
## 2. VU-meter
**Endpoint nuovo, disponibile da oggi**:
```
GET /api/audio/levels
```
Risposta (tutti i valori in dBFS, tipicamente negativi, 0 = fondo scala):
```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
}
```
Regole obbligatorie:
1. **Il firmware non fa polling né cache** — ogni chiamata GET rilegge live
dal DSP in quel preciso istante. Se vuoi un meter che si aggiorna nel
tempo, il polling periodico lo devi fare tu lato app.
2. **Frequenza consigliata: ogni 200-500 ms**, non più veloce — ogni
chiamata impegna il bus I2C del dispositivo per 6 letture sequenziali
(una per meter, il chip ha solo 2 registri hardware di cattura).
3. **Ferma il polling quando la schermata con i meter non è visibile**
(es. `onDisappear` / quando l'utente cambia tab) — non lasciarlo attivo
in background, non serve e spreca risorse sul dispositivo.
4. `radio_in_*` sono il livello Si4684 (post-compressore), `bluetooth_in_*`
il livello ESP32, `output_*` il livello dopo Bass Boost (prima del
limiter finale) — utile per capire dove mostrare quale barra.
5. Se la risposta HTTP non è 200 (es. 500), mostra i meter come "non
disponibili" invece di un valore congelato/stantio.