From 6087bb52973d6ac1f0b63091e4ea9a7ded4d899f Mon Sep 17 00:00:00 2001 From: Michele Bigi Date: Tue, 18 Aug 2026 15:45:55 +0200 Subject: [PATCH 01/12] Add igiRadio iOS/iPadOS app for DigiRadio control. Introduces a new SwiftUI app with HTTP REST device control, mock mode for UI development, BLE-based discovery, and documented architecture aligned with the firmware API. Co-authored-by: Cursor --- .../Documentation/APP_ARCHITECTURE.md | 35 ++ .../igiRadio/Documentation/BLE_PROTOCOL.md | 45 ++ .../APP/igiRadio/Documentation/DEVELOPMENT.md | 58 +++ .../igiRadio/Documentation/DEVICE_STATE.md | 17 + .../Documentation/PROJECT_ANALYSIS.md | 151 +++++++ .../igiRadio/Documentation/UI_ARCHITECTURE.md | 31 ++ .../igiRadio/Scripts/generate_xcodeproj.py | 421 ++++++++++++++++++ .../Tests/igiRadioTests/igiRadioTests.swift | 65 +++ .../igiRadio.xcodeproj/project.pbxproj | 363 +++++++++++++++ .../xcshareddata/xcschemes/igiRadio.xcscheme | 22 + .../igiRadio/App/AppEnvironment.swift | 30 ++ .../igiRadio/igiRadio/App/igiRadioApp.swift | 20 + .../igiRadio/Components/DesignSystem.swift | 150 +++++++ .../igiRadio/Mock/MockDigiRadioService.swift | 259 +++++++++++ .../igiRadio/Models/DigiRadioState.swift | 245 ++++++++++ .../AccentColor.colorset/Contents.json | 38 ++ .../Resources/Assets.xcassets/Contents.json | 6 + .../igiRadio/igiRadio/Resources/Info.plist | 50 +++ .../Bluetooth/BLEProvisioningService.swift | 73 +++ .../Device/DigiRadioDiscoveryService.swift | 97 ++++ .../Services/Device/DigiRadioService.swift | 75 ++++ .../Device/RealDigiRadioService.swift | 192 ++++++++ .../HTTPDigiRadioClient.swift | 411 +++++++++++++++++ .../ViewModels/ConnectionViewModel.swift | 67 +++ .../igiRadio/ViewModels/HomeViewModel.swift | 52 +++ .../igiRadio/igiRadio/Views/AudioView.swift | 86 ++++ .../igiRadio/Views/BLEProvisioningView.swift | 62 +++ .../igiRadio/Views/BluetoothView.swift | 73 +++ .../igiRadio/Views/ConnectionView.swift | 144 ++++++ .../igiRadio/Views/DABRadioView.swift | 77 ++++ .../igiRadio/igiRadio/Views/FMRadioView.swift | 150 +++++++ .../igiRadio/igiRadio/Views/HomeView.swift | 221 +++++++++ .../igiRadio/igiRadio/Views/PresetsView.swift | 86 ++++ .../igiRadio/igiRadio/Views/RootView.swift | 95 ++++ .../igiRadio/Views/SettingsViews.swift | 100 +++++ 35 files changed, 4067 insertions(+) create mode 100644 Software/APP/igiRadio/Documentation/APP_ARCHITECTURE.md create mode 100644 Software/APP/igiRadio/Documentation/BLE_PROTOCOL.md create mode 100644 Software/APP/igiRadio/Documentation/DEVELOPMENT.md create mode 100644 Software/APP/igiRadio/Documentation/DEVICE_STATE.md create mode 100644 Software/APP/igiRadio/Documentation/PROJECT_ANALYSIS.md create mode 100644 Software/APP/igiRadio/Documentation/UI_ARCHITECTURE.md create mode 100644 Software/APP/igiRadio/Scripts/generate_xcodeproj.py create mode 100644 Software/APP/igiRadio/Tests/igiRadioTests/igiRadioTests.swift create mode 100644 Software/APP/igiRadio/igiRadio.xcodeproj/project.pbxproj create mode 100644 Software/APP/igiRadio/igiRadio.xcodeproj/xcshareddata/xcschemes/igiRadio.xcscheme create mode 100644 Software/APP/igiRadio/igiRadio/App/AppEnvironment.swift create mode 100644 Software/APP/igiRadio/igiRadio/App/igiRadioApp.swift create mode 100644 Software/APP/igiRadio/igiRadio/Components/DesignSystem.swift create mode 100644 Software/APP/igiRadio/igiRadio/Mock/MockDigiRadioService.swift create mode 100644 Software/APP/igiRadio/igiRadio/Models/DigiRadioState.swift create mode 100644 Software/APP/igiRadio/igiRadio/Resources/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 Software/APP/igiRadio/igiRadio/Resources/Assets.xcassets/Contents.json create mode 100644 Software/APP/igiRadio/igiRadio/Resources/Info.plist create mode 100644 Software/APP/igiRadio/igiRadio/Services/Bluetooth/BLEProvisioningService.swift create mode 100644 Software/APP/igiRadio/igiRadio/Services/Device/DigiRadioDiscoveryService.swift create mode 100644 Software/APP/igiRadio/igiRadio/Services/Device/DigiRadioService.swift create mode 100644 Software/APP/igiRadio/igiRadio/Services/Device/RealDigiRadioService.swift create mode 100644 Software/APP/igiRadio/igiRadio/Services/DigiRadioProtocol/HTTPDigiRadioClient.swift create mode 100644 Software/APP/igiRadio/igiRadio/ViewModels/ConnectionViewModel.swift create mode 100644 Software/APP/igiRadio/igiRadio/ViewModels/HomeViewModel.swift create mode 100644 Software/APP/igiRadio/igiRadio/Views/AudioView.swift create mode 100644 Software/APP/igiRadio/igiRadio/Views/BLEProvisioningView.swift create mode 100644 Software/APP/igiRadio/igiRadio/Views/BluetoothView.swift create mode 100644 Software/APP/igiRadio/igiRadio/Views/ConnectionView.swift create mode 100644 Software/APP/igiRadio/igiRadio/Views/DABRadioView.swift create mode 100644 Software/APP/igiRadio/igiRadio/Views/FMRadioView.swift create mode 100644 Software/APP/igiRadio/igiRadio/Views/HomeView.swift create mode 100644 Software/APP/igiRadio/igiRadio/Views/PresetsView.swift create mode 100644 Software/APP/igiRadio/igiRadio/Views/RootView.swift create mode 100644 Software/APP/igiRadio/igiRadio/Views/SettingsViews.swift diff --git a/Software/APP/igiRadio/Documentation/APP_ARCHITECTURE.md b/Software/APP/igiRadio/Documentation/APP_ARCHITECTURE.md new file mode 100644 index 0000000..2cec32b --- /dev/null +++ b/Software/APP/igiRadio/Documentation/APP_ARCHITECTURE.md @@ -0,0 +1,35 @@ +# APP_ARCHITECTURE — igiRadio + +## Stack + +``` +SwiftUI Views + ↓ @Observable ViewModels +DigiRadioService (protocol) + ↓ +RealDigiRadioService ──→ HTTPDigiRadioClient (URLSession) +MockDigiRadioService ──→ dati in-memory + +BLEProvisioningService (solo setup Wi‑Fi) + ↓ CoreBluetooth + protocomm (ESP Security1) +``` + +## Principi + +1. Le View **non** chiamano URLSession o CoreBluetooth direttamente. +2. Un solo `DigiRadioState` osservabile aggiornato dal service. +3. Errori tipizzati (`DigiRadioError`). +4. Dependency injection via `AppEnvironment` in `igiRadioApp`. + +## Connessione + +| Fase | Meccanismo | +|------|------------| +| Prima configurazione | BLE provisioning **oppure** join SoftAP + POST /api/wifi | +| Uso normale | mDNS `digiradio-xxxxxx.local` o IP manuale | +| Base URL | `http://{host}/api/...` | + +## Polling vs push + +- Nessuna notifica BLE documentata per stato tuner. +- `TunerViewModel` può pollare `GET /api/tuner/status` a intervallo configurabile quando la schermata radio è attiva. diff --git a/Software/APP/igiRadio/Documentation/BLE_PROTOCOL.md b/Software/APP/igiRadio/Documentation/BLE_PROTOCOL.md new file mode 100644 index 0000000..49eeb6e --- /dev/null +++ b/Software/APP/igiRadio/Documentation/BLE_PROTOCOL.md @@ -0,0 +1,45 @@ +# BLE_PROTOCOL — igiRadio + +## Riepilogo + +**DigiRadio non espone un protocollo BLE GATT documentato per il controllo radio/audio.** + +Il BLE dell'ESP32-S3 è usato **solo** per il provisioning Wi‑Fi (setup iniziale). + +--- + +## BLE Wi‑Fi Provisioning (documentato) + +| Parametro | Valore | +|-----------|--------| +| Stack | ESP-IDF `wifi_provisioning` + `scheme_ble` | +| Sicurezza | protocomm **Security1** | +| Proof of possession | **Serial number** dispositivo (EUI-48 da EEPROM) | +| Nome advertising | `DigiRadio-` (es. `DigiRadio-CC4DB4`) | +| Riferimento firmware | `components/net/src/BleProvisioning.cpp` | +| Riferimento manuale | `ch-api.tex` §BLE provisioning | + +### Flusso + +1. Dispositivo in setup mode (no credenziali Wi‑Fi salvate) +2. Advertise BLE con nome = SoftAP SSID +3. App iOS invia credenziali Wi‑Fi via protocomm (Security1 + PoP) +4. Firmware valida, salva in NVS, reboot in STA +5. App passa a controllo via **HTTP** su LAN + +### Implementazione igiRadio + +- Usare **CoreBluetooth** solo per questa fase +- Compatibile con app generiche "ESP BLE Provisioning" (stesso protocollo Espressif) +- PoP richiesto dall'utente: serial da etichetta o da schermata setup SoftAP + +### UNKNOWN + +- UUID servizio/caratteristica custom DigiRadio per controllo: **non documentati** +- Notifiche BLE stato tuner: **non documentate** + +--- + +## Controllo dispositivo (post-provisioning) + +Vedi `APP_ARCHITECTURE.md` — trasporto **HTTP REST** porta 80. diff --git a/Software/APP/igiRadio/Documentation/DEVELOPMENT.md b/Software/APP/igiRadio/Documentation/DEVELOPMENT.md new file mode 100644 index 0000000..84b3e79 --- /dev/null +++ b/Software/APP/igiRadio/Documentation/DEVELOPMENT.md @@ -0,0 +1,58 @@ +# DEVELOPMENT — igiRadio + +## Requisiti + +- macOS con **Xcode 15+** (testato Xcode 26.6) +- iOS 17+ deployment target +- Dispositivo DigiRadio su stessa rete Wi‑Fi (STA) o in setup SoftAP + +## Aprire il progetto + +```bash +open /Users/michelebigi/Documents/Develop/DigiRadio/Software/APP/igiRadio/igiRadio.xcodeproj +``` + +Rigenerare il progetto (se necessario): + +```bash +cd Software/APP/igiRadio +python3 Scripts/generate_xcodeproj.py +``` + +## Build e test da terminale + +```bash +cd Software/APP/igiRadio +xcodebuild -scheme igiRadio -destination 'platform=iOS Simulator,name=iPhone 17' build test +``` + +## Scheme + +- **igiRadio** — app principale +- **igiRadioTests** — unit test protocollo e state + +## Mock vs reale + +In **DEBUG**, `AppEnvironment` avvia in modalità mock (`MockDigiRadioService`). +Disattivare in **Connessione → Modalità demo (Mock)** e inserire l'host HTTP reale. + +## Test su hardware + +1. Provisioning Wi‑Fi (BLE discovery in-app, o app ESP BLE Provisioning; oppure SoftAP `http://192.168.4.1`) +2. In app: Impostazioni → Connessione → host `http://digiradio-.local` +3. Verificare `GET /api/health` + +## Documentazione firmware + +`/Users/michelebigi/Documents/Develop/DigiRadio/Software/docs/manual/ch-api.tex` + +## Architettura runtime + +``` +SwiftUI → ViewModel → DigiRadioService + ├── RealDigiRadioService → HTTPDigiRadioClient (REST JSON) + └── MockDigiRadioService +BLEProvisioningService (CoreBluetooth, solo discovery setup) +``` + +Il controllo tuner/audio **non** usa GATT proprietario — vedi `BLE_PROTOCOL.md`. diff --git a/Software/APP/igiRadio/Documentation/DEVICE_STATE.md b/Software/APP/igiRadio/Documentation/DEVICE_STATE.md new file mode 100644 index 0000000..9977b93 --- /dev/null +++ b/Software/APP/igiRadio/Documentation/DEVICE_STATE.md @@ -0,0 +1,17 @@ +# DEVICE_STATE — DigiRadioState + +Modello centrale in `Models/DigiRadioState.swift`. + +## Sezioni + +| Sezione | Campi documentati | +|---------|-------------------| +| `connection` | host, isConnected, lastError | +| `health` | status, firmware, serialNumber, chips | +| `tuner` | band, locked, volume, fm?, dab? | +| `audio` | mixer, master, eq, enhancements | +| `bluetooth` | booted, pairing, a2dpState, deviceName, speaker | +| `stations` | [Station] | +| `streaming` | enabled, url | + +Campi non documentati nell'API **non** sono presenti nel modello. diff --git a/Software/APP/igiRadio/Documentation/PROJECT_ANALYSIS.md b/Software/APP/igiRadio/Documentation/PROJECT_ANALYSIS.md new file mode 100644 index 0000000..98ae23d --- /dev/null +++ b/Software/APP/igiRadio/Documentation/PROJECT_ANALYSIS.md @@ -0,0 +1,151 @@ +# PROJECT_ANALYSIS — DigiRadio (fonte: documentazione firmware) + +**Data analisi:** 2026-08-18 +**Fonte primaria:** `Software/docs/manual/` (non modificata) +**Firmware di riferimento:** 0.8.5+ + +--- + +## 1. Documenti analizzati + +| File | Contenuto | +|------|-----------| +| `ch-intro.tex` | Introduzione prodotto | +| `ch-hardware.tex` | PCB, pinout, catena audio | +| `ch-firmware.tex` | Architettura firmware ESP32-S3 | +| `ch-si4684.tex` | Tuner FM/DAB, tune, seek, RDS, DLS | +| `ch-adau1701.tex` | DSP SigmaStudio, mixer, EQ, safeload | +| `ch-bt1035.tex` | Modulo BT classic A2DP (UART AT), I2S da ADAU | +| `ch-sigmastudio.tex` | Design SigmaStudio | +| `ch-api.tex` | **HTTP REST API** (protocollo app) | +| `ch-classes.tex` | Classi firmware | +| `ch-build.tex` | Build/flash | +| `ch-licensing.tex` | Licenze | + +Documentazione aggiuntiva letta: `Software/CLAUDE.md`, `Software/docs/security-flash-nvs.md`, `BleProvisioning.cpp`. + +--- + +## 2. Distinzione critica: BLE vs controllo + +### Controllo dispositivo (tuner, audio, preset, BT config) + +**Trasporto documentato:** HTTP JSON REST su **TCP porta 80** (Wi‑Fi). + +- Setup mode: SoftAP `DigiRadio-` → `http://192.168.4.1/` +- STA mode: stessa API su IP LAN o **mDNS** `digiradio-.local` +- **NON esiste** un protocollo GATT proprietario documentato per tuner/audio/preset. + +### BLE (ESP32-S3 onboard) + +**Solo provisioning Wi‑Fi** in setup mode (`ch-api.tex` §BLE provisioning): + +- Stack: ESP-IDF `wifi_provisioning` + `scheme_ble` +- Sicurezza: **Security1** (protocomm) +- **Proof of possession (PoP):** serial number dispositivo (stesso valore di `GET /api/health` → `serialNumber`) +- **Nome servizio BLE:** SSID SoftAP = `DigiRadio-` (es. `DigiRadio-CC4DB4`) +- Su successo: credenziali salvate in NVS, reboot in STA +- **Non è un endpoint HTTP** — nessuna REST call + +### Bluetooth Audio (streaming verso speaker esterno) + +**Modulo separato FSC-BT1035** (classic BR/EDR A2DP), controllato dal firmware via **UART AT**, esposto all'app solo tramite **HTTP** `/api/bluetooth/*`. + +L'iPhone **non** si collega in A2DP al DigiRadio per ascoltare: il DigiRadio invia audio al Bose/Speaker via BT1035. + +--- + +## 3. Architettura hardware (sintesi) + +``` +Si4684 (SPI) ──I2S──► ADAU1701 (I2S master 48 kHz) ──I2S──► BT1035 ──A2DP──► Speaker +ESP32-S3 (Wi‑Fi/BLE, HTTP server, opz. I2S test) +``` + +Sorgenti audio nel DSP: **Si4684** (radio), **ESP32 I2S** (stream locale / phone push). + +--- + +## 4. Endpoint HTTP documentati (controllo app) + +| Metodo | Path | Funzione | +|--------|------|----------| +| GET | `/api/health` | Stato, fw, serial, chip boot | +| POST | `/api/wifi` | Provisioning STA (reboot) | +| POST | `/api/wifi/scan` | Scan reti vicine | +| GET | `/api/tuner/status` | Stato tuner FM/DAB | +| GET | `/api/tuner/services` | Lista servizi DAB | +| POST | `/api/tuner/tune` | Sintonia FM/DAB | +| POST | `/api/tuner/play` | Play servizio DAB | +| POST | `/api/tuner/seek` | Seek FM up/down | +| POST | `/api/tuner/scan` | Scan singola stazione | +| POST | `/api/tuner/scan/full` | Scan completo banda FM | +| GET/PUT | `/api/audio/profile` | Profilo mixer/EQ/master | +| POST | `/api/audio/reset` | Reset profilo factory | +| POST | `/api/audio/stereo-enhance` | Enhancement stereo 0–100 | +| POST | `/api/audio/bass-enhance` | Enhancement bass 0–100 | +| POST | `/api/audio/beep` | Beep diagnostico ADAU | +| GET | `/api/dsp/params` | Tabella parametri SigmaStudio | +| PUT | `/api/dsp/param` | Scrittura cella raw (live) | +| POST | `/api/dsp/program` | Upload blob DSP (reboot) | +| POST | `/api/system/ota` | OTA firmware ESP32 (reboot) | +| GET/POST | `/api/streaming` | Web radio MP3 URL | +| PUT | `/api/stream/phone` | Push PCM stereo 48 kHz da phone | +| GET | `/api/bluetooth/status` | Stato BT1035 | +| POST | `/api/bluetooth/pair` | Modalità discoverable | +| POST | `/api/bluetooth/pair/stop` | Esci pairing | +| POST | `/api/bluetooth/disconnect` | Disconnect A2DP | +| GET | `/api/bluetooth/paired` | Lista paired | +| POST | `/api/bluetooth/scan` | Scan speaker vicini | +| POST | `/api/bluetooth/connect` | Connect A2DP + opz. save | +| GET/POST/DELETE | `/api/bluetooth/speaker` | Speaker default | +| POST | `/api/bluetooth/reconnect` | Reconnect manuale | +| POST | `/api/bluetooth/auto-reconnect` | Retry count 0–15 | +| GET | `/api/stations` | Lista preset | +| POST | `/api/stations` | Aggiungi preset | +| POST | `/api/stations/remove` | Rimuovi preset | +| POST | `/api/stations/reorder` | Riordina preset | +| POST | `/api/stations/tune` | Richiama preset | + +--- + +## 5. Funzionalità per schermata app + +| Area | Supportato (documentato) | Note | +|------|--------------------------|------| +| Connessione Wi‑Fi | ✅ | BLE prov. + SoftAP + POST /api/wifi | +| FM tune/seek/scan | ✅ | RSSI, SNR, RDS PS/RT | +| DAB tune/play/services | ✅ | DLS, ensemble, service list | +| Preset | ✅ | CRUD + reorder + recall | +| Volume/mixer/EQ | ✅ | Via audio profile | +| DSP raw params | ✅ | Escape hatch tecnico | +| BT speaker pairing | ✅ | Via HTTP → BT1035 AT | +| OTA firmware | ✅ | POST /api/system/ota | +| OTA DSP | ✅ | POST /api/dsp/program | +| Phone audio stream | ✅ | PUT /api/stream/phone | +| Web radio stream | ✅ | POST /api/streaming | +| Artwork album | ❌ | **UNKNOWN** — non in API | +| Batteria dispositivo | ❌ | Alimentato da rete | +| AUX/USB come sorgente UI | ❌ | Non esposto in API | +| Notifiche push BLE stato | ❌ | Nessun GATT documentato | + +--- + +## 6. Implicazioni per igiRadio + +1. **DigiRadioService** → implementazione **HTTP REST** (URLSession), non GATT custom. +2. **BLE layer** → solo **Wi‑Fi provisioning** (ESP protocomm Security1 + PoP = serial). +3. **MockDigiRadioService** → stessa interfaccia, dati fittizi. +4. **Discovery** → mDNS `.local`, IP manuale, o join SoftAP in setup. +5. **Real-time** → polling selettivo su `/api/tuner/status` quando in radio; nessuna notifica BLE documentata. + +--- + +## 7. UNKNOWN — specification required + +| Voce | Motivo | +|------|--------| +| UUID GATT controllo tuner/audio | Non documentati — controllo è HTTP | +| Artwork/metadata streaming | Non in API tuner | +| Protocol version field | Solo `fw` in health | +| Hardware revision API | Solo `serialNumber` + chip flags | diff --git a/Software/APP/igiRadio/Documentation/UI_ARCHITECTURE.md b/Software/APP/igiRadio/Documentation/UI_ARCHITECTURE.md new file mode 100644 index 0000000..0ab521f --- /dev/null +++ b/Software/APP/igiRadio/Documentation/UI_ARCHITECTURE.md @@ -0,0 +1,31 @@ +# UI_ARCHITECTURE — igiRadio + +## iPhone + +`TabView` con tab: Home, Radio, Presets, Audio, Settings. + +## iPad + +`NavigationSplitView`: +- Sidebar: Home, FM, DAB, Presets, Bluetooth, Audio, Settings +- Detail: contenuto selezionato + +## Schermate + +| Schermata | ViewModel | +|-----------|-----------| +| Connection | `ConnectionViewModel` | +| Home | `HomeViewModel` | +| FM | `FMViewModel` | +| DAB | `DABViewModel` | +| Presets | `PresetsViewModel` | +| Audio | `AudioViewModel` | +| Bluetooth | `BluetoothViewModel` | +| Settings | `SettingsViewModel` | +| Device Info | `DeviceViewModel` | +| Diagnostics | `DiagnosticsViewModel` | +| Firmware | `FirmwareViewModel` | + +## Design System + +`Components/DesignSystem/` — colori semantici, tipografia, card, slider, status dot. diff --git a/Software/APP/igiRadio/Scripts/generate_xcodeproj.py b/Software/APP/igiRadio/Scripts/generate_xcodeproj.py new file mode 100644 index 0000000..875b0a3 --- /dev/null +++ b/Software/APP/igiRadio/Scripts/generate_xcodeproj.py @@ -0,0 +1,421 @@ +#!/usr/bin/env python3 +"""Generate igiRadio.xcodeproj using PBXFileSystemSynchronizedRootGroup.""" + +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent +PROJECT = "igiRadio" +BUNDLE = "com.digiradio.igiRadio" + +def uid(n): + # 24-char hex IDs (stable) + import hashlib + return hashlib.md5(f"igiRadio-{n}".encode()).hexdigest()[:24].upper() + +IDS = {k: uid(k) for k in [ + "project", "main", "products", "app", "test", "app_product", "test_product", + "sources", "resources", "fw_app", "fw_test", "test_sources", "test_fw", + "sync_app", "sync_tests", "sync_exception", "proj_cfg", "app_cfg", "test_cfg", + "dbg_proj", "rel_proj", "dbg_app", "rel_app", "dbg_test", "rel_test", + "proxy", "dep", +]} + +pbx = f'''// !$*UTF8*$! +{{ +\tarchiveVersion = 1; +\tclasses = {{ +\t}}; +\tobjectVersion = 77; +\tobjects = {{ + +/* Begin PBXBuildFile section */ +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ +\t\t{IDS["proxy"]} /* PBXContainerItemProxy */ = {{ +\t\t\tisa = PBXContainerItemProxy; +\t\t\tcontainerPortal = {IDS["project"]} /* Project object */; +\t\t\tproxyType = 1; +\t\t\tremoteGlobalIDString = {IDS["app"]}; +\t\t\tremoteInfo = {PROJECT}; +\t\t}}; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ +\t\t{IDS["app_product"]} /* {PROJECT}.app */ = {{isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = {PROJECT}.app; sourceTree = BUILT_PRODUCTS_DIR; }}; +\t\t{IDS["test_product"]} /* {PROJECT}Tests.xctest */ = {{isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = {PROJECT}Tests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }}; +/* End PBXFileReference section */ + +/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ +\t\t{IDS["sync_exception"]} /* Exceptions for "{PROJECT}" folder in "{PROJECT}" target */ = {{ +\t\t\tisa = PBXFileSystemSynchronizedBuildFileExceptionSet; +\t\t\tmembershipExceptions = ( +\t\t\t\tResources/Info.plist, +\t\t\t); +\t\t\ttarget = {IDS["app"]} /* {PROJECT} */; +\t\t}}; +/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ + +/* Begin PBXFileSystemSynchronizedRootGroup section */ +\t\t{IDS["sync_app"]} /* {PROJECT} */ = {{ +\t\t\tisa = PBXFileSystemSynchronizedRootGroup; +\t\t\texceptions = ( +\t\t\t\t{IDS["sync_exception"]} /* Exceptions for "{PROJECT}" folder in "{PROJECT}" target */, +\t\t\t); +\t\t\tpath = {PROJECT}; +\t\t\tsourceTree = ""; +\t\t}}; +\t\t{IDS["sync_tests"]} /* {PROJECT}Tests */ = {{ +\t\t\tisa = PBXFileSystemSynchronizedRootGroup; +\t\t\tpath = Tests/{PROJECT}Tests; +\t\t\tsourceTree = ""; +\t\t}}; +/* End PBXFileSystemSynchronizedRootGroup section */ + +/* Begin PBXFrameworksBuildPhase section */ +\t\t{IDS["fw_app"]} /* Frameworks */ = {{ +\t\t\tisa = PBXFrameworksBuildPhase; +\t\t\tbuildActionMask = 2147483647; +\t\t\tfiles = ( +\t\t\t); +\t\t\trunOnlyForDeploymentPostprocessing = 0; +\t\t}}; +\t\t{IDS["test_fw"]} /* Frameworks */ = {{ +\t\t\tisa = PBXFrameworksBuildPhase; +\t\t\tbuildActionMask = 2147483647; +\t\t\tfiles = ( +\t\t\t); +\t\t\trunOnlyForDeploymentPostprocessing = 0; +\t\t}}; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ +\t\t{IDS["products"]} /* Products */ = {{ +\t\t\tisa = PBXGroup; +\t\t\tchildren = ( +\t\t\t\t{IDS["app_product"]} /* {PROJECT}.app */, +\t\t\t\t{IDS["test_product"]} /* {PROJECT}Tests.xctest */, +\t\t\t); +\t\t\tname = Products; +\t\t\tsourceTree = ""; +\t\t}}; +\t\t{IDS["main"]} = {{ +\t\t\tisa = PBXGroup; +\t\t\tchildren = ( +\t\t\t\t{IDS["sync_app"]} /* {PROJECT} */, +\t\t\t\t{IDS["sync_tests"]} /* {PROJECT}Tests */, +\t\t\t\t{IDS["products"]} /* Products */, +\t\t\t); +\t\t\tsourceTree = ""; +\t\t}}; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ +\t\t{IDS["app"]} /* {PROJECT} */ = {{ +\t\t\tisa = PBXNativeTarget; +\t\t\tbuildConfigurationList = {IDS["app_cfg"]} /* Build configuration list for PBXNativeTarget "{PROJECT}" */; +\t\t\tbuildPhases = ( +\t\t\t\t{IDS["sources"]} /* Sources */, +\t\t\t\t{IDS["fw_app"]} /* Frameworks */, +\t\t\t\t{IDS["resources"]} /* Resources */, +\t\t\t); +\t\t\tbuildRules = ( +\t\t\t); +\t\t\tdependencies = ( +\t\t\t); +\t\t\tfileSystemSynchronizedGroups = ( +\t\t\t\t{IDS["sync_app"]} /* {PROJECT} */, +\t\t\t); +\t\t\tname = {PROJECT}; +\t\t\tproductName = {PROJECT}; +\t\t\tproductReference = {IDS["app_product"]} /* {PROJECT}.app */; +\t\t\tproductType = "com.apple.product-type.application"; +\t\t}}; +\t\t{IDS["test"]} /* {PROJECT}Tests */ = {{ +\t\t\tisa = PBXNativeTarget; +\t\t\tbuildConfigurationList = {IDS["test_cfg"]} /* Build configuration list for PBXNativeTarget "{PROJECT}Tests" */; +\t\t\tbuildPhases = ( +\t\t\t\t{IDS["test_sources"]} /* Sources */, +\t\t\t\t{IDS["test_fw"]} /* Frameworks */, +\t\t\t); +\t\t\tbuildRules = ( +\t\t\t); +\t\t\tdependencies = ( +\t\t\t\t{IDS["dep"]} /* PBXTargetDependency */, +\t\t\t); +\t\t\tfileSystemSynchronizedGroups = ( +\t\t\t\t{IDS["sync_tests"]} /* {PROJECT}Tests */, +\t\t\t); +\t\t\tname = {PROJECT}Tests; +\t\t\tproductName = {PROJECT}Tests; +\t\t\tproductReference = {IDS["test_product"]} /* {PROJECT}Tests.xctest */; +\t\t\tproductType = "com.apple.product-type.bundle.unit-test"; +\t\t}}; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ +\t\t{IDS["project"]} /* Project object */ = {{ +\t\t\tisa = PBXProject; +\t\t\tattributes = {{ +\t\t\t\tBuildIndependentTargetsInParallel = 1; +\t\t\t\tLastSwiftUpdateCheck = 2600; +\t\t\t\tLastUpgradeCheck = 2600; +\t\t\t\tTargetAttributes = {{ +\t\t\t\t\t{IDS["app"]} = {{ +\t\t\t\t\t\tCreatedOnToolsVersion = 26.0; +\t\t\t\t\t}}; +\t\t\t\t\t{IDS["test"]} = {{ +\t\t\t\t\t\tCreatedOnToolsVersion = 26.0; +\t\t\t\t\t\tTestTargetID = {IDS["app"]}; +\t\t\t\t\t}}; +\t\t\t\t}}; +\t\t\t}}; +\t\t\tbuildConfigurationList = {IDS["proj_cfg"]} /* Build configuration list for PBXProject "{PROJECT}" */; +\t\t\tcompatibilityVersion = "Xcode 16.0"; +\t\t\tdevelopmentRegion = en; +\t\t\thasScannedForEncodings = 0; +\t\t\tknownRegions = ( +\t\t\t\ten, +\t\t\t\tBase, +\t\t\t); +\t\t\tmainGroup = {IDS["main"]}; +\t\t\tproductRefGroup = {IDS["products"]} /* Products */; +\t\t\tprojectDirPath = ""; +\t\t\tprojectRoot = ""; +\t\t\ttargets = ( +\t\t\t\t{IDS["app"]} /* {PROJECT} */, +\t\t\t\t{IDS["test"]} /* {PROJECT}Tests */, +\t\t\t); +\t\t}}; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ +\t\t{IDS["resources"]} /* Resources */ = {{ +\t\t\tisa = PBXResourcesBuildPhase; +\t\t\tbuildActionMask = 2147483647; +\t\t\tfiles = ( +\t\t\t); +\t\t\trunOnlyForDeploymentPostprocessing = 0; +\t\t}}; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ +\t\t{IDS["sources"]} /* Sources */ = {{ +\t\t\tisa = PBXSourcesBuildPhase; +\t\t\tbuildActionMask = 2147483647; +\t\t\tfiles = ( +\t\t\t); +\t\t\trunOnlyForDeploymentPostprocessing = 0; +\t\t}}; +\t\t{IDS["test_sources"]} /* Sources */ = {{ +\t\t\tisa = PBXSourcesBuildPhase; +\t\t\tbuildActionMask = 2147483647; +\t\t\tfiles = ( +\t\t\t); +\t\t\trunOnlyForDeploymentPostprocessing = 0; +\t\t}}; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ +\t\t{IDS["dep"]} /* PBXTargetDependency */ = {{ +\t\t\tisa = PBXTargetDependency; +\t\t\ttarget = {IDS["app"]} /* {PROJECT} */; +\t\t\ttargetProxy = {IDS["proxy"]} /* PBXContainerItemProxy */; +\t\t}}; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ +\t\t{IDS["dbg_proj"]} /* Debug */ = {{ +\t\t\tisa = XCBuildConfiguration; +\t\t\tbuildSettings = {{ +\t\t\t\tALWAYS_SEARCH_USER_PATHS = NO; +\t\t\t\tCLANG_ENABLE_MODULES = YES; +\t\t\t\tCOPY_PHASE_STRIP = NO; +\t\t\t\tDEBUG_INFORMATION_FORMAT = dwarf; +\t\t\t\tENABLE_TESTABILITY = YES; +\t\t\t\tGCC_OPTIMIZATION_LEVEL = 0; +\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 17.0; +\t\t\t\tMTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; +\t\t\t\tONLY_ACTIVE_ARCH = YES; +\t\t\t\tSDKROOT = iphoneos; +\t\t\t\tSWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; +\t\t\t\tSWIFT_OPTIMIZATION_LEVEL = "-Onone"; +\t\t\t}}; +\t\t\tname = Debug; +\t\t}}; +\t\t{IDS["rel_proj"]} /* Release */ = {{ +\t\t\tisa = XCBuildConfiguration; +\t\t\tbuildSettings = {{ +\t\t\t\tALWAYS_SEARCH_USER_PATHS = NO; +\t\t\t\tCLANG_ENABLE_MODULES = YES; +\t\t\t\tCOPY_PHASE_STRIP = NO; +\t\t\t\tDEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; +\t\t\t\tENABLE_NS_ASSERTIONS = NO; +\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 17.0; +\t\t\t\tMTL_ENABLE_DEBUG_INFO = NO; +\t\t\t\tSDKROOT = iphoneos; +\t\t\t\tSWIFT_COMPILATION_MODE = wholemodule; +\t\t\t\tVALIDATE_PRODUCT = YES; +\t\t\t}}; +\t\t\tname = Release; +\t\t}}; +\t\t{IDS["dbg_app"]} /* Debug */ = {{ +\t\t\tisa = XCBuildConfiguration; +\t\t\tbuildSettings = {{ +\t\t\t\tASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; +\t\t\t\tCODE_SIGN_STYLE = Automatic; +\t\t\t\tCURRENT_PROJECT_VERSION = 1; +\t\t\t\tENABLE_PREVIEWS = YES; +\t\t\t\tGENERATE_INFOPLIST_FILE = NO; +\t\t\t\tINFOPLIST_FILE = {PROJECT}/Resources/Info.plist; +\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 17.0; +\t\t\t\tLD_RUNPATH_SEARCH_PATHS = ( +\t\t\t\t\t"$(inherited)", +\t\t\t\t\t"@executable_path/Frameworks", +\t\t\t\t); +\t\t\t\tMARKETING_VERSION = 1.0; +\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = {BUNDLE}; +\t\t\t\tPRODUCT_NAME = "$(TARGET_NAME)"; +\t\t\t\tSDKROOT = iphoneos; +\t\t\t\tSUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; +\t\t\t\tSWIFT_EMIT_LOC_STRINGS = YES; +\t\t\t\tSWIFT_VERSION = 5.0; +\t\t\t\tTARGETED_DEVICE_FAMILY = "1,2"; +\t\t\t}}; +\t\t\tname = Debug; +\t\t}}; +\t\t{IDS["rel_app"]} /* Release */ = {{ +\t\t\tisa = XCBuildConfiguration; +\t\t\tbuildSettings = {{ +\t\t\t\tASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; +\t\t\t\tCODE_SIGN_STYLE = Automatic; +\t\t\t\tCURRENT_PROJECT_VERSION = 1; +\t\t\t\tENABLE_PREVIEWS = YES; +\t\t\t\tGENERATE_INFOPLIST_FILE = NO; +\t\t\t\tINFOPLIST_FILE = {PROJECT}/Resources/Info.plist; +\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 17.0; +\t\t\t\tLD_RUNPATH_SEARCH_PATHS = ( +\t\t\t\t\t"$(inherited)", +\t\t\t\t\t"@executable_path/Frameworks", +\t\t\t\t); +\t\t\t\tMARKETING_VERSION = 1.0; +\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = {BUNDLE}; +\t\t\t\tPRODUCT_NAME = "$(TARGET_NAME)"; +\t\t\t\tSDKROOT = iphoneos; +\t\t\t\tSUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; +\t\t\t\tSWIFT_EMIT_LOC_STRINGS = YES; +\t\t\t\tSWIFT_VERSION = 5.0; +\t\t\t\tTARGETED_DEVICE_FAMILY = "1,2"; +\t\t\t}}; +\t\t\tname = Release; +\t\t}}; +\t\t{IDS["dbg_test"]} /* Debug */ = {{ +\t\t\tisa = XCBuildConfiguration; +\t\t\tbuildSettings = {{ +\t\t\t\tBUNDLE_LOADER = "$(TEST_HOST)"; +\t\t\t\tCODE_SIGN_STYLE = Automatic; +\t\t\t\tCURRENT_PROJECT_VERSION = 1; +\t\t\t\tGENERATE_INFOPLIST_FILE = YES; +\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 17.0; +\t\t\t\tMARKETING_VERSION = 1.0; +\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = {BUNDLE}Tests; +\t\t\t\tPRODUCT_NAME = "$(TARGET_NAME)"; +\t\t\t\tSDKROOT = iphoneos; +\t\t\t\tSUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; +\t\t\t\tSWIFT_EMIT_LOC_STRINGS = NO; +\t\t\t\tSWIFT_VERSION = 5.0; +\t\t\t\tTARGETED_DEVICE_FAMILY = "1,2"; +\t\t\t\tTEST_HOST = "$(BUILT_PRODUCTS_DIR)/{PROJECT}.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/{PROJECT}"; +\t\t\t}}; +\t\t\tname = Debug; +\t\t}}; +\t\t{IDS["rel_test"]} /* Release */ = {{ +\t\t\tisa = XCBuildConfiguration; +\t\t\tbuildSettings = {{ +\t\t\t\tBUNDLE_LOADER = "$(TEST_HOST)"; +\t\t\t\tCODE_SIGN_STYLE = Automatic; +\t\t\t\tCURRENT_PROJECT_VERSION = 1; +\t\t\t\tGENERATE_INFOPLIST_FILE = YES; +\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 17.0; +\t\t\t\tMARKETING_VERSION = 1.0; +\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = {BUNDLE}Tests; +\t\t\t\tPRODUCT_NAME = "$(TARGET_NAME)"; +\t\t\t\tSDKROOT = iphoneos; +\t\t\t\tSUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; +\t\t\t\tSWIFT_EMIT_LOC_STRINGS = NO; +\t\t\t\tSWIFT_VERSION = 5.0; +\t\t\t\tTARGETED_DEVICE_FAMILY = "1,2"; +\t\t\t\tTEST_HOST = "$(BUILT_PRODUCTS_DIR)/{PROJECT}.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/{PROJECT}"; +\t\t\t}}; +\t\t\tname = Release; +\t\t}}; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ +\t\t{IDS["proj_cfg"]} /* Build configuration list for PBXProject "{PROJECT}" */ = {{ +\t\t\tisa = XCConfigurationList; +\t\t\tbuildConfigurations = ( +\t\t\t\t{IDS["dbg_proj"]} /* Debug */, +\t\t\t\t{IDS["rel_proj"]} /* Release */, +\t\t\t); +\t\t\tdefaultConfigurationIsVisible = 0; +\t\t\tdefaultConfigurationName = Release; +\t\t}}; +\t\t{IDS["app_cfg"]} /* Build configuration list for PBXNativeTarget "{PROJECT}" */ = {{ +\t\t\tisa = XCConfigurationList; +\t\t\tbuildConfigurations = ( +\t\t\t\t{IDS["dbg_app"]} /* Debug */, +\t\t\t\t{IDS["rel_app"]} /* Release */, +\t\t\t); +\t\t\tdefaultConfigurationIsVisible = 0; +\t\t\tdefaultConfigurationName = Release; +\t\t}}; +\t\t{IDS["test_cfg"]} /* Build configuration list for PBXNativeTarget "{PROJECT}Tests" */ = {{ +\t\t\tisa = XCConfigurationList; +\t\t\tbuildConfigurations = ( +\t\t\t\t{IDS["dbg_test"]} /* Debug */, +\t\t\t\t{IDS["rel_test"]} /* Release */, +\t\t\t); +\t\t\tdefaultConfigurationIsVisible = 0; +\t\t\tdefaultConfigurationName = Release; +\t\t}}; +/* End XCConfigurationList section */ +\t}}; +\trootObject = {IDS["project"]} /* Project object */; +}} +''' + +out = ROOT / f"{PROJECT}.xcodeproj" / "project.pbxproj" +out.parent.mkdir(parents=True, exist_ok=True) +out.write_text(pbx) + +scheme_dir = ROOT / f"{PROJECT}.xcodeproj" / "xcshareddata" / "xcschemes" +scheme_dir.mkdir(parents=True, exist_ok=True) +scheme = f''' + + + + + + + + + + + + + + + + + + + + + +''' +(scheme_dir / f"{PROJECT}.xcscheme").write_text(scheme) +print(f"Wrote {out}") + +if __name__ == "__main__": + pass diff --git a/Software/APP/igiRadio/Tests/igiRadioTests/igiRadioTests.swift b/Software/APP/igiRadio/Tests/igiRadioTests/igiRadioTests.swift new file mode 100644 index 0000000..9b86fe1 --- /dev/null +++ b/Software/APP/igiRadio/Tests/igiRadioTests/igiRadioTests.swift @@ -0,0 +1,65 @@ +import XCTest +@testable import igiRadio + +final class HTTPDecoderTests: XCTestCase { + func testDecodeHealth() throws { + let json = """ + {"status":"ok","fw":"0.8.5","serialNumber":"CC4DB4MOCK01", + "chips":{"si4684":true,"adau1701":true,"bt1035":true}} + """.data(using: .utf8)! + let health = try JSONDecoder.api.decode(HealthResponse.self, from: json) + XCTAssertEqual(health.status, "ok") + XCTAssertEqual(health.fw, "0.8.5") + XCTAssertEqual(health.serialNumber, "CC4DB4MOCK01") + XCTAssertTrue(health.chips.si4684) + } + + func testDecodeTunerFM() throws { + let json = """ + {"booted":true,"band":"fm","locked":true,"volume":50, + "fm":{"frequency_khz":102300,"rssi_dbuv":48,"snr_db":20,"stereo":true, + "station_name":"Radio Test","radiotext":"Hello"}, + "dab":null} + """.data(using: .utf8)! + let tuner = try JSONDecoder.api.decode(TunerStatusResponse.self, from: json) + XCTAssertEqual(tuner.band, "fm") + XCTAssertEqual(tuner.fm?.frequencyKhz, 102300) + XCTAssertEqual(tuner.fm?.stationName, "Radio Test") + } + + func testDecodeAudioProfile() throws { + let json = """ + {"mixer":{"si4684_left_db":0,"si4684_right_db":0,"esp32_left_db":0,"esp32_right_db":0, + "mix_left_db":0,"mix_right_db":0}, + "master":{"left_db":10,"right_db":10}, + "eq":[{"gain_db":0,"center_hz":40,"q":1.414}], + "enhancements":{"stereo_level":5,"bass_level":7}} + """.data(using: .utf8)! + let profile = try JSONDecoder.api.decode(AudioProfileDTO.self, from: json) + XCTAssertEqual(profile.master.leftDb, 10) + XCTAssertEqual(profile.enhancements.bassLevel, 7) + } + + func testA2DPStateMapping() { + XCTAssertEqual(A2DPState(apiValue: "streaming"), .streaming) + XCTAssertEqual(A2DPState(apiValue: "unknown-value"), .unknown) + } + + func testBLENameToHost() { + XCTAssertEqual( + DigiRadioDiscoveryService.httpHost(fromBLEName: "DigiRadio-CC4DB4"), + "http://digiradio-cc4db4.local" + ) + XCTAssertNil(DigiRadioDiscoveryService.httpHost(fromBLEName: "OtherDevice")) + } +} + +final class MockDigiRadioServiceTests: XCTestCase { + func testMockConnectAndTune() async throws { + let mock = MockDigiRadioService() + try await mock.connect(host: "mock.local") + XCTAssertTrue(mock.state.connection.isConnected) + try await mock.tuneFM(frequencyKhz: 101_500) + XCTAssertEqual(mock.state.tuner.fm?.frequencyKhz, 101_500) + } +} diff --git a/Software/APP/igiRadio/igiRadio.xcodeproj/project.pbxproj b/Software/APP/igiRadio/igiRadio.xcodeproj/project.pbxproj new file mode 100644 index 0000000..cd105b9 --- /dev/null +++ b/Software/APP/igiRadio/igiRadio.xcodeproj/project.pbxproj @@ -0,0 +1,363 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 77; + objects = { + +/* Begin PBXBuildFile section */ +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + C050F319BDFADBF7CE6700F8 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = D7ACBDA7D90DDE7BBA02A41B /* Project object */; + proxyType = 1; + remoteGlobalIDString = 0938FB30D2E421C09D31973A; + remoteInfo = igiRadio; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 366935DDA7048CE6717FCB93 /* igiRadio.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = igiRadio.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97BD5E0DAD822A58093D141B /* igiRadioTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = igiRadioTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ + 7D085337BF0EA6EB04337F45 /* Exceptions for "igiRadio" folder in "igiRadio" target */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + Resources/Info.plist, + ); + target = 0938FB30D2E421C09D31973A /* igiRadio */; + }; +/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ + +/* Begin PBXFileSystemSynchronizedRootGroup section */ + F7E3CB0B027CB68EA63A4AB8 /* igiRadio */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + 7D085337BF0EA6EB04337F45 /* Exceptions for "igiRadio" folder in "igiRadio" target */, + ); + path = igiRadio; + sourceTree = ""; + }; + 693436FA1780106DA05D06E3 /* igiRadioTests */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = Tests/igiRadioTests; + sourceTree = ""; + }; +/* End PBXFileSystemSynchronizedRootGroup section */ + +/* Begin PBXFrameworksBuildPhase section */ + 71F357EA3EEC95DAEFCF7345 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 0A8032441559F0A3B8F103F8 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 2761999F42D5E1910DD2B679 /* Products */ = { + isa = PBXGroup; + children = ( + 366935DDA7048CE6717FCB93 /* igiRadio.app */, + 97BD5E0DAD822A58093D141B /* igiRadioTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + C5D6659C8B3150288BA78279 = { + isa = PBXGroup; + children = ( + F7E3CB0B027CB68EA63A4AB8 /* igiRadio */, + 693436FA1780106DA05D06E3 /* igiRadioTests */, + 2761999F42D5E1910DD2B679 /* Products */, + ); + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 0938FB30D2E421C09D31973A /* igiRadio */ = { + isa = PBXNativeTarget; + buildConfigurationList = ECA92BA517002122145704D1 /* Build configuration list for PBXNativeTarget "igiRadio" */; + buildPhases = ( + DF1C5CA20CABFD7B8DA40E94 /* Sources */, + 71F357EA3EEC95DAEFCF7345 /* Frameworks */, + 3C8EE57839841131D4276B21 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + F7E3CB0B027CB68EA63A4AB8 /* igiRadio */, + ); + name = igiRadio; + productName = igiRadio; + productReference = 366935DDA7048CE6717FCB93 /* igiRadio.app */; + productType = "com.apple.product-type.application"; + }; + 48A9EB97D44FF615ADCA031A /* igiRadioTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 61D281E9594B71DA372FEE34 /* Build configuration list for PBXNativeTarget "igiRadioTests" */; + buildPhases = ( + F994E2BFA3D135FCD316A95C /* Sources */, + 0A8032441559F0A3B8F103F8 /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 0E0716BAD2A3DFD42CA27715 /* PBXTargetDependency */, + ); + fileSystemSynchronizedGroups = ( + 693436FA1780106DA05D06E3 /* igiRadioTests */, + ); + name = igiRadioTests; + productName = igiRadioTests; + productReference = 97BD5E0DAD822A58093D141B /* igiRadioTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + D7ACBDA7D90DDE7BBA02A41B /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 2600; + LastUpgradeCheck = 2600; + TargetAttributes = { + 0938FB30D2E421C09D31973A = { + CreatedOnToolsVersion = 26.0; + }; + 48A9EB97D44FF615ADCA031A = { + CreatedOnToolsVersion = 26.0; + TestTargetID = 0938FB30D2E421C09D31973A; + }; + }; + }; + buildConfigurationList = C8A2C2BD01CBC6B9DFC6F199 /* Build configuration list for PBXProject "igiRadio" */; + compatibilityVersion = "Xcode 16.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = C5D6659C8B3150288BA78279; + productRefGroup = 2761999F42D5E1910DD2B679 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 0938FB30D2E421C09D31973A /* igiRadio */, + 48A9EB97D44FF615ADCA031A /* igiRadioTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 3C8EE57839841131D4276B21 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + DF1C5CA20CABFD7B8DA40E94 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F994E2BFA3D135FCD316A95C /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 0E0716BAD2A3DFD42CA27715 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 0938FB30D2E421C09D31973A /* igiRadio */; + targetProxy = C050F319BDFADBF7CE6700F8 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + FDBD275B398C4976FD6BC80D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ENABLE_MODULES = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_TESTABILITY = YES; + GCC_OPTIMIZATION_LEVEL = 0; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 54274B7C1572A50D5BE0DEA8 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ENABLE_MODULES = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 73C8BF7223EFFA7108969F77 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = NO; + INFOPLIST_FILE = igiRadio/Resources/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.digiradio.igiRadio; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + FA1BD134133059F9163AC34F /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = NO; + INFOPLIST_FILE = igiRadio/Resources/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.digiradio.igiRadio; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + 90735310944459417946066C /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.digiradio.igiRadioTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/igiRadio.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/igiRadio"; + }; + name = Debug; + }; + E6AD029DFF2D9078F77BDCBA /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.digiradio.igiRadioTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/igiRadio.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/igiRadio"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + C8A2C2BD01CBC6B9DFC6F199 /* Build configuration list for PBXProject "igiRadio" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + FDBD275B398C4976FD6BC80D /* Debug */, + 54274B7C1572A50D5BE0DEA8 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + ECA92BA517002122145704D1 /* Build configuration list for PBXNativeTarget "igiRadio" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 73C8BF7223EFFA7108969F77 /* Debug */, + FA1BD134133059F9163AC34F /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 61D281E9594B71DA372FEE34 /* Build configuration list for PBXNativeTarget "igiRadioTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 90735310944459417946066C /* Debug */, + E6AD029DFF2D9078F77BDCBA /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = D7ACBDA7D90DDE7BBA02A41B /* Project object */; +} diff --git a/Software/APP/igiRadio/igiRadio.xcodeproj/xcshareddata/xcschemes/igiRadio.xcscheme b/Software/APP/igiRadio/igiRadio.xcodeproj/xcshareddata/xcschemes/igiRadio.xcscheme new file mode 100644 index 0000000..e5d884e --- /dev/null +++ b/Software/APP/igiRadio/igiRadio.xcodeproj/xcshareddata/xcschemes/igiRadio.xcscheme @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/Software/APP/igiRadio/igiRadio/App/AppEnvironment.swift b/Software/APP/igiRadio/igiRadio/App/AppEnvironment.swift new file mode 100644 index 0000000..412089d --- /dev/null +++ b/Software/APP/igiRadio/igiRadio/App/AppEnvironment.swift @@ -0,0 +1,30 @@ +import Foundation +import Observation + +/// Dependency container for igiRadio. +@Observable +final class AppEnvironment { + var useMockDevice: Bool + private(set) var digiRadio: any DigiRadioService + + var state: DigiRadioState { + digiRadio.state + } + + init(useMockDevice: Bool = { + #if DEBUG + true + #else + false + #endif + }()) { + self.useMockDevice = useMockDevice + self.digiRadio = useMockDevice ? MockDigiRadioService() : RealDigiRadioService() + } + + func setUseMock(_ enabled: Bool) { + guard useMockDevice != enabled else { return } + useMockDevice = enabled + digiRadio = enabled ? MockDigiRadioService() : RealDigiRadioService() + } +} diff --git a/Software/APP/igiRadio/igiRadio/App/igiRadioApp.swift b/Software/APP/igiRadio/igiRadio/App/igiRadioApp.swift new file mode 100644 index 0000000..8a34c86 --- /dev/null +++ b/Software/APP/igiRadio/igiRadio/App/igiRadioApp.swift @@ -0,0 +1,20 @@ +import SwiftUI + +@main +struct igiRadioApp: App { + @State private var environment = AppEnvironment() + + var body: some Scene { + WindowGroup { + RootView() + .environment(environment) + .task { + #if DEBUG + if environment.useMockDevice, !environment.state.connection.isConnected { + try? await environment.digiRadio.connect(host: "mock.local") + } + #endif + } + } + } +} diff --git a/Software/APP/igiRadio/igiRadio/Components/DesignSystem.swift b/Software/APP/igiRadio/igiRadio/Components/DesignSystem.swift new file mode 100644 index 0000000..872b132 --- /dev/null +++ b/Software/APP/igiRadio/igiRadio/Components/DesignSystem.swift @@ -0,0 +1,150 @@ +import SwiftUI + +enum IGITheme { + static let cornerRadius: CGFloat = 16 + static let cardRadius: CGFloat = 20 + static let spacingXS: CGFloat = 4 + static let spacingS: CGFloat = 8 + static let spacingM: CGFloat = 16 + static let spacingL: CGFloat = 24 + static let spacingXL: CGFloat = 32 + + static let accent = Color.accentColor + static let cardBackground = Color(.secondarySystemGroupedBackground) + static let screenBackground = Color(.systemGroupedBackground) +} + +struct IGICard: View { + @ViewBuilder var content: () -> Content + + var body: some View { + content() + .padding(IGITheme.spacingM) + .background(IGITheme.cardBackground, in: RoundedRectangle(cornerRadius: IGITheme.cardRadius, style: .continuous)) + } +} + +struct IGIStatusDot: View { + var isConnected: Bool + + var body: some View { + Circle() + .fill(isConnected ? Color.green : Color.orange) + .frame(width: 10, height: 10) + .accessibilityLabel(isConnected ? "Connesso" : "Non connesso") + } +} + +struct IGIPrimaryButton: View { + var title: String + var systemImage: String? + var action: () -> Void + + var body: some View { + Button(action: action) { + Label { + Text(title) + .fontWeight(.semibold) + } icon: { + if let systemImage { + Image(systemName: systemImage) + } + } + .frame(maxWidth: .infinity) + .padding(.vertical, 14) + } + .buttonStyle(.borderedProminent) + .controlSize(.large) + } +} + +struct IGISignalBar: View { + var level: Double + + var body: some View { + GeometryReader { proxy in + ZStack(alignment: .leading) { + Capsule().fill(Color.secondary.opacity(0.2)) + Capsule() + .fill(Color.accentColor.gradient) + .frame(width: proxy.size.width * max(0, min(1, level))) + } + } + .frame(height: 8) + .accessibilityLabel("Segnale") + .accessibilityValue("\(Int(level * 100)) percento") + } +} + +struct IGISectionHeader: View { + var title: String + + var body: some View { + Text(title) + .font(.title3.weight(.semibold)) + .frame(maxWidth: .infinity, alignment: .leading) + } +} + +struct IGIEmptyState: View { + var title: String + var message: String + var systemImage: String + + var body: some View { + VStack(spacing: IGITheme.spacingM) { + Image(systemName: systemImage) + .font(.system(size: 44)) + .foregroundStyle(.secondary) + Text(title).font(.title3.weight(.semibold)) + Text(message) + .multilineTextAlignment(.center) + .foregroundStyle(.secondary) + } + .padding(IGITheme.spacingXL) + } +} + +struct IGIVolumeSlider: View { + @Binding var value: Double + var onEditingEnded: ((Double) -> Void)? + + var body: some View { + VStack(alignment: .leading, spacing: IGITheme.spacingS) { + HStack { + Image(systemName: "speaker.fill") + Slider(value: $value, in: 0 ... 100, step: 1) { editing in + if !editing { onEditingEnded?(value) } + } + Image(systemName: "speaker.wave.3.fill") + } + Text("Volume \(Int(value))") + .font(.caption) + .foregroundStyle(.secondary) + } + } +} + +struct IGIScanningIndicator: View { + @State private var rotation: Double = 0 + @Environment(\.accessibilityReduceMotion) private var reduceMotion + + var body: some View { + ZStack { + Circle() + .stroke(Color.accentColor.opacity(0.2), lineWidth: 4) + Circle() + .trim(from: 0, to: 0.28) + .stroke(Color.accentColor, style: StrokeStyle(lineWidth: 4, lineCap: .round)) + .rotationEffect(.degrees(rotation)) + } + .frame(width: 56, height: 56) + .onAppear { + guard !reduceMotion else { return } + withAnimation(.linear(duration: 1).repeatForever(autoreverses: false)) { + rotation = 360 + } + } + .accessibilityLabel("Ricerca in corso") + } +} diff --git a/Software/APP/igiRadio/igiRadio/Mock/MockDigiRadioService.swift b/Software/APP/igiRadio/igiRadio/Mock/MockDigiRadioService.swift new file mode 100644 index 0000000..e3ae4be --- /dev/null +++ b/Software/APP/igiRadio/igiRadio/Mock/MockDigiRadioService.swift @@ -0,0 +1,259 @@ +import Foundation +import Observation + +@Observable +final class MockDigiRadioService: DigiRadioService { + private(set) var state = DigiRadioState() + + init() { + seed() + } + + func connect(host: String) async throws { + try await delay() + state.connection.host = host + state.connection.isConnected = true + state.connection.lastError = nil + } + + func disconnect() { + state.connection = ConnectionState() + seed() + } + + func refreshHealth() async throws { + try await delay() + } + + func refreshTunerStatus() async throws { + try await delay() + } + + func refreshAudioProfile() async throws { + try await delay() + } + + func refreshBluetoothStatus() async throws { + try await delay() + } + + func refreshStations() async throws { + try await delay() + } + + func refreshStreaming() async throws { + try await delay() + } + + func tuneFM(frequencyKhz: Int) async throws { + try await delay() + state.tuner.band = .fm + state.tuner.locked = true + state.tuner.fm = FMTunerState( + frequencyKhz: frequencyKhz, + rssiDbuv: 48, + snrDb: 22, + stereo: true, + stationName: "Radio Mock", + radiotext: "igiRadio demo mode" + ) + } + + func tuneDAB(freqIndex: Int) async throws { + try await delay() + state.tuner.band = .dab + state.tuner.locked = true + state.tuner.dab = DABTunerState( + freqIndex: freqIndex, + ficQuality: 78, + cnrDb: 24, + playingServiceId: 101, + playingComponentId: 1, + dynamicLabel: "DAB Mock Ensemble" + ) + } + + func playDAB(serviceId: Int, componentId: Int) async throws { + try await delay() + state.tuner.dab?.playingServiceId = serviceId + state.tuner.dab?.playingComponentId = componentId + state.tuner.dab?.dynamicLabel = "Now playing mock service" + } + + func seekFM(direction: String) async throws { + try await delay() + guard var fm = state.tuner.fm else { return } + let step = direction == "down" ? -100 : 100 + fm.frequencyKhz = max(64_000, min(108_000, fm.frequencyKhz + step)) + state.tuner.fm = fm + } + + func scanFM(maxSteps: Int, name: String?) async throws -> TunerScanResponse { + try await delay(1.2) + return TunerScanResponse( + status: "found", + band: "fm", + steps: 8, + frequencyKhz: 101_500, + stationName: name ?? "Mock FM", + freqIndex: nil, + serviceId: nil, + componentId: nil + ) + } + + func scanFullFM() async throws -> [FMScanHit] { + try await delay(1.5) + return [ + FMScanHit(frequencyKhz: 97_500, rssiDbuv: 55, snrDb: 20, stationName: "Mock R1"), + FMScanHit(frequencyKhz: 101_500, rssiDbuv: 48, snrDb: 18, stationName: "Mock R2"), + FMScanHit(frequencyKhz: 102_300, rssiDbuv: 51, snrDb: 19, stationName: "Radio Italia") + ] + } + + func setVolume(_ volume: Int) async throws { + try await delay() + let clamped = max(0, min(100, volume)) + state.tuner.volume = clamped + state.audio.master.leftDb = Double(clamped) + state.audio.master.rightDb = Double(clamped) + } + + func listDABServices() async throws -> [DABService] { + try await delay() + return [ + DABService(serviceId: 101, componentId: 1, label: "Mock Radio 1"), + DABService(serviceId: 102, componentId: 1, label: "Mock Radio 2"), + DABService(serviceId: 103, componentId: 1, label: "Mock Jazz") + ] + } + + func saveStation(_ station: Station) async throws { + try await delay() + state.stations.append(station) + } + + func removeStation(at index: Int) async throws { + try await delay() + guard state.stations.indices.contains(index) else { return } + state.stations.remove(at: index) + } + + func reorderStation(from: Int, to: Int) async throws { + try await delay() + guard state.stations.indices.contains(from), state.stations.indices.contains(to) else { return } + let item = state.stations.remove(at: from) + state.stations.insert(item, at: to) + } + + func tuneStation(at index: Int) async throws { + try await delay() + guard state.stations.indices.contains(index) else { return } + let station = state.stations[index] + switch station.band { + case .fm: + if let freq = station.fmFrequencyKhz { try await tuneFM(frequencyKhz: freq) } + case .dab: + if let idx = station.dabFreqIndex { try await tuneDAB(freqIndex: idx) } + if let sid = station.dabServiceId, let cid = station.dabComponentId { + try await playDAB(serviceId: sid, componentId: cid) + } + } + } + + func applyAudioProfile(_ profile: AudioProfileDTO) async throws { + try await delay() + state.audio.mixer = profile.mixer + state.audio.master = profile.master + state.audio.eq = profile.eq + state.audio.enhancements = profile.enhancements + } + + func setStereoEnhance(level: Int) async throws { + try await delay() + state.audio.enhancements.stereoLevel = level + } + + func setBassEnhance(level: Int) async throws { + try await delay() + state.audio.enhancements.bassLevel = level + } + + func resetAudio() async throws { + try await delay() + state.audio = AudioState() + } + + func scanBluetooth(seconds: Int) async throws { + try await delay(0.8) + state.bluetooth.nearbyDevices = [ + BluetoothDevice(index: 0, mac: "AA:BB:CC:DD:EE:FF", name: "Bose Solo II", rssiDbm: -52), + BluetoothDevice(index: 1, mac: "11:22:33:44:55:66", name: "Living Room", rssiDbm: -68) + ] + } + + func connectBluetooth(mac: String, name: String?, save: Bool) async throws { + try await delay() + state.bluetooth.a2dpState = .streaming + state.bluetooth.deviceName = name ?? "Speaker" + if save { + state.bluetooth.savedSpeaker = SavedSpeaker(mac: mac, name: name ?? "Speaker") + } + } + + func reconnectBluetooth() async throws { + try await delay() + state.bluetooth.a2dpState = .streaming + } + + private func delay(_ seconds: Double = 0.35) async throws { + try await Task.sleep(for: .seconds(seconds)) + } + + private func seed() { + state.health = HealthState( + status: "ok", + firmware: "0.8.5-mock", + serialNumber: "CC4DB4MOCK01", + chips: ChipHealth(si4684: true, adau1701: true, bt1035: true) + ) + state.tuner = TunerState( + booted: true, + band: .fm, + locked: true, + volume: 42, + fm: FMTunerState( + frequencyKhz: 102_300, + rssiDbuv: 51, + snrDb: 19, + stereo: true, + stationName: "Radio Italia", + radiotext: "Mock mode — nessun hardware collegato" + ), + dab: nil + ) + state.audio = AudioState( + mixer: MixerState(), + master: MasterVolumeState(leftDb: 42, rightDb: 42), + eq: (0 ..< 6).map { i in + EQBandState(index: i, gainDb: 0, centerHz: [40, 120, 400, 1000, 3000, 10000][i], q: 1.414) + }, + enhancements: EnhancementsState(stereoLevel: 20, bassLevel: 15) + ) + state.bluetooth = BluetoothState( + booted: true, + pairing: false, + a2dpState: .streaming, + deviceName: "Bose Solo II", + autoReconnect: 5, + savedSpeaker: SavedSpeaker(mac: "BC:87:FA:E6:9D:6E", name: "Bose Solo II") + ) + state.stations = [ + Station(name: "Radio Italia", band: .fm, fmFrequencyKhz: 102_300), + Station(name: "RDS", band: .fm, fmFrequencyKhz: 97_500) + ] + state.streaming = StreamingState(enabled: false, url: "") + state.connection.isConnected = true + state.connection.host = "digiradio-mock.local" + } +} diff --git a/Software/APP/igiRadio/igiRadio/Models/DigiRadioState.swift b/Software/APP/igiRadio/igiRadio/Models/DigiRadioState.swift new file mode 100644 index 0000000..06a9ea0 --- /dev/null +++ b/Software/APP/igiRadio/igiRadio/Models/DigiRadioState.swift @@ -0,0 +1,245 @@ +import Foundation + +/// Central device snapshot — only documented API fields. +struct DigiRadioState: Equatable, Sendable { + var connection = ConnectionState() + var health = HealthState() + var tuner = TunerState() + var audio = AudioState() + var bluetooth = BluetoothState() + var stations: [Station] = [] + var streaming = StreamingState() +} + +struct ConnectionState: Equatable, Sendable { + var host: String = "" + var isConnected = false + var isConnecting = false + var lastError: String? +} + +struct HealthState: Equatable, Sendable { + var status: String = "" + var firmware: String = "" + var serialNumber: String = "" + var chips = ChipHealth() +} + +struct ChipHealth: Equatable, Sendable { + var si4684 = false + var adau1701 = false + var bt1035 = false +} + +struct TunerState: Equatable, Sendable { + var booted = false + var band: TunerBand = .fm + var locked = false + var volume: Int = 0 + var fm: FMTunerState? + var dab: DABTunerState? +} + +enum TunerBand: String, Codable, Sendable, CaseIterable { + case fm + case dab +} + +struct FMTunerState: Equatable, Sendable, Codable { + var frequencyKhz: Int + var rssiDbuv: Int? + var snrDb: Int? + var stereo: Bool? + var stationName: String? + var radiotext: String? + + enum CodingKeys: String, CodingKey { + case frequencyKhz = "frequency_khz" + case rssiDbuv = "rssi_dbuv" + case snrDb = "snr_db" + case stereo + case stationName = "station_name" + case radiotext + } +} + +struct DABTunerState: Equatable, Sendable, Codable { + var freqIndex: Int + var ficQuality: Int? + var cnrDb: Int? + var playingServiceId: Int? + var playingComponentId: Int? + var dynamicLabel: String? + + enum CodingKeys: String, CodingKey { + case freqIndex = "freq_index" + case ficQuality = "fic_quality" + case cnrDb = "cnr_db" + case playingServiceId = "playing_service_id" + case playingComponentId = "playing_component_id" + case dynamicLabel = "dynamic_label" + } +} + +struct AudioState: Equatable, Sendable { + var mixer = MixerState() + var master = MasterVolumeState() + var eq: [EQBandState] = [] + var enhancements = EnhancementsState() +} + +struct MixerState: Equatable, Sendable, Codable { + var si4684LeftDb: Double = 0 + var si4684RightDb: Double = 0 + var esp32LeftDb: Double = 0 + var esp32RightDb: Double = 0 + var mixLeftDb: Double = 0 + var mixRightDb: Double = 0 + + enum CodingKeys: String, CodingKey { + case si4684LeftDb = "si4684_left_db" + case si4684RightDb = "si4684_right_db" + case esp32LeftDb = "esp32_left_db" + case esp32RightDb = "esp32_right_db" + case mixLeftDb = "mix_left_db" + case mixRightDb = "mix_right_db" + } +} + +struct MasterVolumeState: Equatable, Sendable, Codable { + var leftDb: Double = 0 + var rightDb: Double = 0 + + enum CodingKeys: String, CodingKey { + case leftDb = "left_db" + case rightDb = "right_db" + } +} + +struct EQBandState: Equatable, Sendable, Codable, Identifiable { + var id: Int { index } + var index: Int + var gainDb: Double + var centerHz: Double + var q: Double + + enum CodingKeys: String, CodingKey { + case gainDb = "gain_db" + case centerHz = "center_hz" + case q + } + + init(index: Int, gainDb: Double, centerHz: Double, q: Double) { + self.index = index + self.gainDb = gainDb + self.centerHz = centerHz + self.q = q + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + index = 0 + gainDb = try container.decode(Double.self, forKey: .gainDb) + centerHz = try container.decode(Double.self, forKey: .centerHz) + q = try container.decode(Double.self, forKey: .q) + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(gainDb, forKey: .gainDb) + try container.encode(centerHz, forKey: .centerHz) + try container.encode(q, forKey: .q) + } +} + +struct EnhancementsState: Equatable, Sendable, Codable { + var stereoLevel: Int = 0 + var bassLevel: Int = 0 + + enum CodingKeys: String, CodingKey { + case stereoLevel = "stereo_level" + case bassLevel = "bass_level" + } +} + +struct BluetoothState: Equatable, Sendable { + var booted = false + var pairing = false + var a2dpState: A2DPState = .unknown + var deviceName: String = "" + var autoReconnect: Int = 0 + var savedSpeaker: SavedSpeaker? + var nearbyDevices: [BluetoothDevice] = [] + var pairedDevices: [BluetoothPairedDevice] = [] +} + +enum A2DPState: String, Sendable { + case standby, connecting, connected, streaming, paused, unknown + + init(apiValue: String) { + switch apiValue.lowercased() { + case "standby": self = .standby + case "connecting": self = .connecting + case "connected": self = .connected + case "streaming": self = .streaming + case "paused": self = .paused + default: self = .unknown + } + } +} + +struct SavedSpeaker: Equatable, Sendable { + var mac: String + var name: String +} + +struct BluetoothDevice: Equatable, Sendable, Identifiable { + var id: String { mac } + var index: Int + var mac: String + var name: String + var rssiDbm: Int +} + +struct BluetoothPairedDevice: Equatable, Sendable, Identifiable { + var id: String { mac } + var index: Int + var mac: String + var name: String +} + +struct Station: Equatable, Sendable, Codable, Identifiable { + var id: String { "\(band.rawValue)-\(name)-\(fmFrequencyKhz ?? dabFreqIndex ?? 0)" } + var name: String + var band: TunerBand + var dabFreqIndex: Int? + var dabServiceId: Int? + var dabComponentId: Int? + var fmFrequencyKhz: Int? + + enum CodingKeys: String, CodingKey { + case name, band + case dabFreqIndex = "dab_freq_index" + case dabServiceId = "dab_service_id" + case dabComponentId = "dab_component_id" + case fmFrequencyKhz = "fm_frequency_khz" + } +} + +struct StreamingState: Equatable, Sendable, Codable { + var enabled = false + var url: String = "" +} + +struct DABService: Equatable, Sendable, Identifiable, Codable { + var id: String { "\(serviceId)-\(componentId)" } + var serviceId: Int + var componentId: Int + var label: String + + enum CodingKeys: String, CodingKey { + case serviceId = "service_id" + case componentId = "component_id" + case label + } +} diff --git a/Software/APP/igiRadio/igiRadio/Resources/Assets.xcassets/AccentColor.colorset/Contents.json b/Software/APP/igiRadio/igiRadio/Resources/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..6da0343 --- /dev/null +++ b/Software/APP/igiRadio/igiRadio/Resources/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.984", + "green" : "0.447", + "red" : "0.227" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "0.584", + "red" : "0.376" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Software/APP/igiRadio/igiRadio/Resources/Assets.xcassets/Contents.json b/Software/APP/igiRadio/igiRadio/Resources/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/Software/APP/igiRadio/igiRadio/Resources/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Software/APP/igiRadio/igiRadio/Resources/Info.plist b/Software/APP/igiRadio/igiRadio/Resources/Info.plist new file mode 100644 index 0000000..af75307 --- /dev/null +++ b/Software/APP/igiRadio/igiRadio/Resources/Info.plist @@ -0,0 +1,50 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + igiRadio + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + LSRequiresIPhoneOS + + NSBluetoothAlwaysUsageDescription + igiRadio usa Bluetooth per trovare DigiRadio in setup mode e configurare il Wi‑Fi. + NSLocalNetworkUsageDescription + igiRadio si connette a DigiRadio sulla rete locale per controllare radio e audio. + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + + UILaunchScreen + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/Software/APP/igiRadio/igiRadio/Services/Bluetooth/BLEProvisioningService.swift b/Software/APP/igiRadio/igiRadio/Services/Bluetooth/BLEProvisioningService.swift new file mode 100644 index 0000000..f9d4bd3 --- /dev/null +++ b/Software/APP/igiRadio/igiRadio/Services/Bluetooth/BLEProvisioningService.swift @@ -0,0 +1,73 @@ +import CoreBluetooth +import Foundation +import Observation +import OSLog + +/// Discovers DigiRadio BLE provisioning endpoints (ESP-IDF wifi_provisioning / scheme_ble). +/// Full credential exchange uses Espressif protocomm Security1 (PoP = device serial). +/// This service performs discovery only; credential provisioning UI is documented in BLE_PROTOCOL.md. +@Observable +final class BLEProvisioningService: NSObject { + enum Phase: Equatable { + case idle + case scanning + case found(name: String, identifier: UUID) + case unsupported + case error(String) + } + + private(set) var phase: Phase = .idle + private(set) var discoveredDevices: [(name: String, id: UUID, rssi: Int)] = [] + + private var central: CBCentralManager? + private let logger = Logger(subsystem: "com.digiradio.igiRadio", category: "BLE") + + override init() { + super.init() + central = CBCentralManager(delegate: self, queue: .main) + } + + func startScan() { + guard let central else { return } + discoveredDevices = [] + phase = .scanning + guard central.state == .poweredOn else { + phase = .error("Bluetooth non disponibile") + return + } + central.scanForPeripherals(withServices: nil, options: [CBCentralManagerScanOptionAllowDuplicatesKey: false]) + logger.info("BLE scan started") + } + + func stopScan() { + central?.stopScan() + if case .scanning = phase { + phase = discoveredDevices.isEmpty ? .idle : .found(name: discoveredDevices[0].name, identifier: discoveredDevices[0].id) + } + } +} + +extension BLEProvisioningService: CBCentralManagerDelegate { + func centralManagerDidUpdateState(_ central: CBCentralManager) { + if central.state != .poweredOn, case .scanning = phase { + phase = .error("Bluetooth spento o non autorizzato") + central.stopScan() + } + } + + func centralManager( + _ central: CBCentralManager, + didDiscover peripheral: CBPeripheral, + advertisementData: [String: Any], + rssi RSSI: NSNumber + ) { + let name = peripheral.name ?? advertisementData[CBAdvertisementDataLocalNameKey] as? String ?? "" + guard name.hasPrefix("DigiRadio") else { return } + let entry = (name: name, id: peripheral.identifier, rssi: RSSI.intValue) + if !discoveredDevices.contains(where: { $0.id == entry.id }) { + discoveredDevices.append(entry) + logger.debug("Found \(name, privacy: .public) RSSI \(RSSI.intValue)") + } + phase = .found(name: name, identifier: peripheral.identifier) + } +} diff --git a/Software/APP/igiRadio/igiRadio/Services/Device/DigiRadioDiscoveryService.swift b/Software/APP/igiRadio/igiRadio/Services/Device/DigiRadioDiscoveryService.swift new file mode 100644 index 0000000..978816b --- /dev/null +++ b/Software/APP/igiRadio/igiRadio/Services/Device/DigiRadioDiscoveryService.swift @@ -0,0 +1,97 @@ +import Foundation +import Observation +import OSLog + +/// Maps BLE setup names (DigiRadio-XXXX) to HTTP hosts and verifies reachability. +@Observable +final class DigiRadioDiscoveryService { + struct DiscoveredDevice: Identifiable, Equatable { + var id: String { host } + var name: String + var host: String + var isReachable: Bool + var firmware: String? + } + + private(set) var devices: [DiscoveredDevice] = [] + private(set) var isSearching = false + + private let ble = BLEProvisioningService() + private let logger = Logger(subsystem: "com.digiradio.igiRadio", category: "Discovery") + private var probeTasks: [Task] = [] + + func start() { + guard !isSearching else { return } + isSearching = true + devices = [] + ble.startScan() + logger.info("Discovery started") + } + + func stop() { + isSearching = false + ble.stopScan() + probeTasks.forEach { $0.cancel() } + probeTasks = [] + } + + func refreshFromBLE() { + let candidates = ble.discoveredDevices.compactMap { entry -> DiscoveredDevice? in + guard let host = Self.httpHost(fromBLEName: entry.name) else { return nil } + return DiscoveredDevice(name: entry.name, host: host, isReachable: false, firmware: nil) + } + + for candidate in candidates { + guard !devices.contains(where: { $0.host == candidate.host }) else { continue } + devices.append(candidate) + probe(host: candidate.host) + } + } + + func addManualHost(_ raw: String) { + let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines) + guard !trimmed.isEmpty else { return } + var host = trimmed + if !host.hasPrefix("http://") && !host.hasPrefix("https://") { + host = "http://\(host)" + } + guard let url = URL(string: host), let schemeHost = url.host else { return } + let normalized = "http://\(schemeHost)" + guard !devices.contains(where: { $0.host == normalized }) else { return } + let name = schemeHost + devices.append(DiscoveredDevice(name: name, host: normalized, isReachable: false, firmware: nil)) + probe(host: normalized) + } + + static func httpHost(fromBLEName name: String) -> String? { + // SoftAP / BLE name: DigiRadio-CC4DB4 → digiradio-cc4db4.local + guard name.hasPrefix("DigiRadio-") else { return nil } + let suffix = String(name.dropFirst("DigiRadio-".count)).lowercased() + guard !suffix.isEmpty else { return nil } + return "http://digiradio-\(suffix).local" + } + + private func probe(host: String) { + let task = Task { + let client = HTTPDigiRadioClient() + do { + try client.setHost(host) + let health = try await client.fetchHealth() + await MainActor.run { + if let index = devices.firstIndex(where: { $0.host == host }) { + devices[index].isReachable = health.status == "ok" + devices[index].firmware = health.fw + } + } + } catch { + await MainActor.run { + if let index = devices.firstIndex(where: { $0.host == host }) { + devices[index].isReachable = false + } + } + logger.debug("Probe failed for \(host, privacy: .public): \(error.localizedDescription)") + } + } + probeTasks.append(task) + } +} diff --git a/Software/APP/igiRadio/igiRadio/Services/Device/DigiRadioService.swift b/Software/APP/igiRadio/igiRadio/Services/Device/DigiRadioService.swift new file mode 100644 index 0000000..3ee7306 --- /dev/null +++ b/Software/APP/igiRadio/igiRadio/Services/Device/DigiRadioService.swift @@ -0,0 +1,75 @@ +import Foundation + +enum DigiRadioError: LocalizedError, Equatable { + case notConnected + case invalidHost + case httpStatus(Int, String?) + case decodingFailed + case network(Error) + + var errorDescription: String? { + switch self { + case .notConnected: return "Non connesso a DigiRadio." + case .invalidHost: return "Host non valido." + case let .httpStatus(code, reason): return "HTTP \(code): \(reason ?? "errore")" + case .decodingFailed: return "Risposta non valida dal dispositivo." + case let .network(error): return error.localizedDescription + } + } + + static func == (lhs: DigiRadioError, rhs: DigiRadioError) -> Bool { + switch (lhs, rhs) { + case (.notConnected, .notConnected), (.invalidHost, .invalidHost), (.decodingFailed, .decodingFailed): + return true + case let (.httpStatus(a, b), .httpStatus(c, d)): + return a == c && b == d + default: + return false + } + } +} + +/// High-level API for controlling DigiRadio (HTTP REST per firmware docs). +protocol DigiRadioService: AnyObject { + var state: DigiRadioState { get } + + func connect(host: String) async throws + func disconnect() + func refreshHealth() async throws + func refreshTunerStatus() async throws + func refreshAudioProfile() async throws + func refreshBluetoothStatus() async throws + func refreshStations() async throws + func refreshStreaming() async throws + + func tuneFM(frequencyKhz: Int) async throws + func tuneDAB(freqIndex: Int) async throws + func playDAB(serviceId: Int, componentId: Int) async throws + func seekFM(direction: String) async throws + func scanFM(maxSteps: Int, name: String?) async throws -> TunerScanResponse + func scanFullFM() async throws -> [FMScanHit] + func setVolume(_ volume: Int) async throws + func listDABServices() async throws -> [DABService] + + func saveStation(_ station: Station) async throws + func removeStation(at index: Int) async throws + func reorderStation(from: Int, to: Int) async throws + func tuneStation(at index: Int) async throws + + func applyAudioProfile(_ profile: AudioProfileDTO) async throws + func setStereoEnhance(level: Int) async throws + func setBassEnhance(level: Int) async throws + func resetAudio() async throws + + func scanBluetooth(seconds: Int) async throws + func connectBluetooth(mac: String, name: String?, save: Bool) async throws + func reconnectBluetooth() async throws +} + +/// Wire DTO for PUT /api/audio/profile +struct AudioProfileDTO: Codable, Equatable, Sendable { + var mixer: MixerState + var master: MasterVolumeState + var eq: [EQBandState] + var enhancements: EnhancementsState +} diff --git a/Software/APP/igiRadio/igiRadio/Services/Device/RealDigiRadioService.swift b/Software/APP/igiRadio/igiRadio/Services/Device/RealDigiRadioService.swift new file mode 100644 index 0000000..7cbc61e --- /dev/null +++ b/Software/APP/igiRadio/igiRadio/Services/Device/RealDigiRadioService.swift @@ -0,0 +1,192 @@ +import Foundation +import Observation +import OSLog + +@Observable +final class RealDigiRadioService: DigiRadioService { + private(set) var state = DigiRadioState() + private let client = HTTPDigiRadioClient() + private let logger = Logger(subsystem: "com.digiradio.igiRadio", category: "Device") + + func connect(host: String) async throws { + state.connection.isConnecting = true + state.connection.lastError = nil + defer { state.connection.isConnecting = false } + + try client.setHost(host) + state.connection.host = host + try await refreshHealth() + state.connection.isConnected = true + logger.info("Connected to \(host, privacy: .public)") + } + + func disconnect() { + client.clearHost() + state.connection = ConnectionState() + } + + func refreshHealth() async throws { + let health = try await client.fetchHealth() + state.health.status = health.status + state.health.firmware = health.fw + state.health.serialNumber = health.serialNumber + state.health.chips = ChipHealth( + si4684: health.chips.si4684, + adau1701: health.chips.adau1701, + bt1035: health.chips.bt1035 + ) + } + + func refreshTunerStatus() async throws { + let tuner = try await client.fetchTunerStatus() + applyTuner(tuner) + } + + func refreshAudioProfile() async throws { + let profile = try await client.fetchAudioProfile() + state.audio.mixer = profile.mixer + state.audio.master = profile.master + state.audio.eq = profile.eq.enumerated().map { index, band in + EQBandState(index: index, gainDb: band.gainDb, centerHz: band.centerHz, q: band.q) + } + state.audio.enhancements = profile.enhancements + } + + func refreshBluetoothStatus() async throws { + let bt = try await client.fetchBluetoothStatus() + state.bluetooth.booted = bt.booted + state.bluetooth.pairing = bt.pairing + state.bluetooth.a2dpState = A2DPState(apiValue: bt.a2dp) + state.bluetooth.deviceName = bt.deviceName + state.bluetooth.autoReconnect = bt.autoReconnect + + let speaker = try await client.fetchSavedSpeaker() + if speaker.configured, let mac = speaker.mac, let name = speaker.name { + state.bluetooth.savedSpeaker = SavedSpeaker(mac: mac, name: name) + } else { + state.bluetooth.savedSpeaker = nil + } + } + + func refreshStations() async throws { + let list = try await client.fetchStations() + state.stations = list.stations + } + + func refreshStreaming() async throws { + state.streaming = try await client.fetchStreaming() + } + + func tuneFM(frequencyKhz: Int) async throws { + let tuner = try await client.tune(body: TuneRequest(band: "fm", freqIndex: nil, frequencyKhz: frequencyKhz)) + applyTuner(tuner) + } + + func tuneDAB(freqIndex: Int) async throws { + let tuner = try await client.tune(body: TuneRequest(band: "dab", freqIndex: freqIndex, frequencyKhz: nil)) + applyTuner(tuner) + } + + func playDAB(serviceId: Int, componentId: Int) async throws { + try await client.playDAB(body: PlayRequest(serviceId: serviceId, componentId: componentId)) + try await refreshTunerStatus() + } + + func seekFM(direction: String) async throws { + _ = try await client.seekFM(direction: direction) + try await refreshTunerStatus() + } + + func scanFM(maxSteps: Int, name: String?) async throws -> TunerScanResponse { + try await client.scanStation(body: TunerScanRequest(band: "fm", maxSteps: maxSteps, name: name)) + } + + func scanFullFM() async throws -> [FMScanHit] { + let response = try await client.scanFullFM() + return response.stations + } + + func setVolume(_ volume: Int) async throws { + var profile = try await client.fetchAudioProfile() + let clamped = max(0, min(100, volume)) + profile.master.leftDb = Double(clamped) + profile.master.rightDb = Double(clamped) + try await client.putAudioProfile(profile) + state.tuner.volume = clamped + state.audio.master = profile.master + } + + func listDABServices() async throws -> [DABService] { + let response = try await client.fetchDABServices() + return response.services + } + + func saveStation(_ station: Station) async throws { + try await client.addStation(station) + try await refreshStations() + } + + func removeStation(at index: Int) async throws { + try await client.removeStation(index: index) + try await refreshStations() + } + + func reorderStation(from: Int, to: Int) async throws { + try await client.reorderStation(from: from, to: to) + try await refreshStations() + } + + func tuneStation(at index: Int) async throws { + try await client.tuneStation(index: index) + try await refreshTunerStatus() + } + + func applyAudioProfile(_ profile: AudioProfileDTO) async throws { + try await client.putAudioProfile(profile) + state.audio.mixer = profile.mixer + state.audio.master = profile.master + state.audio.eq = profile.eq + state.audio.enhancements = profile.enhancements + } + + func setStereoEnhance(level: Int) async throws { + try await client.setEnhance(path: "/api/audio/stereo-enhance", level: level) + state.audio.enhancements.stereoLevel = level + } + + func setBassEnhance(level: Int) async throws { + try await client.setEnhance(path: "/api/audio/bass-enhance", level: level) + state.audio.enhancements.bassLevel = level + } + + func resetAudio() async throws { + try await client.resetAudio() + try await refreshAudioProfile() + } + + func scanBluetooth(seconds: Int) async throws { + let response = try await client.scanBluetooth(seconds: seconds) + state.bluetooth.nearbyDevices = response.devices.map { + BluetoothDevice(index: $0.index, mac: $0.mac, name: $0.name, rssiDbm: $0.rssiDbm) + } + } + + func connectBluetooth(mac: String, name: String?, save: Bool) async throws { + try await client.connectBluetooth(mac: mac, name: name, save: save) + try await refreshBluetoothStatus() + } + + func reconnectBluetooth() async throws { + try await client.reconnectBluetooth() + try await refreshBluetoothStatus() + } + + private func applyTuner(_ tuner: TunerStatusResponse) { + state.tuner.booted = tuner.booted + state.tuner.band = TunerBand(rawValue: tuner.band) ?? .fm + state.tuner.locked = tuner.locked + state.tuner.volume = tuner.volume + state.tuner.fm = tuner.fm + state.tuner.dab = tuner.dab + } +} diff --git a/Software/APP/igiRadio/igiRadio/Services/DigiRadioProtocol/HTTPDigiRadioClient.swift b/Software/APP/igiRadio/igiRadio/Services/DigiRadioProtocol/HTTPDigiRadioClient.swift new file mode 100644 index 0000000..40d52f9 --- /dev/null +++ b/Software/APP/igiRadio/igiRadio/Services/DigiRadioProtocol/HTTPDigiRadioClient.swift @@ -0,0 +1,411 @@ +import Foundation +import OSLog + +/// HTTP REST client for DigiRadio firmware API (ch-api.tex). +final class HTTPDigiRadioClient { + private let session: URLSession + private let logger = Logger(subsystem: "com.digiradio.igiRadio", category: "HTTP") + private var baseURL: URL? + + init(session: URLSession = .shared) { + self.session = session + } + + func setHost(_ host: String) throws { + let trimmed = host.trimmingCharacters(in: .whitespacesAndNewlines) + guard !trimmed.isEmpty else { throw DigiRadioError.invalidHost } + var normalized = trimmed + if !normalized.hasPrefix("http://") && !normalized.hasPrefix("https://") { + normalized = "http://\(normalized)" + } + guard let url = URL(string: normalized) else { throw DigiRadioError.invalidHost } + baseURL = url + } + + func clearHost() { + baseURL = nil + } + + var isConfigured: Bool { baseURL != nil } + + // MARK: - Health + + func fetchHealth() async throws -> HealthResponse { + try await get("/api/health") + } + + // MARK: - Tuner + + func fetchTunerStatus() async throws -> TunerStatusResponse { + try await get("/api/tuner/status") + } + + func fetchDABServices() async throws -> DABServicesResponse { + try await get("/api/tuner/services") + } + + func tune(body: TuneRequest) async throws -> TunerStatusResponse { + try await post("/api/tuner/tune", body: body) + } + + func playDAB(body: PlayRequest) async throws { + let _: StatusResponse = try await post("/api/tuner/play", body: body) + } + + func seekFM(direction: String) async throws -> SeekResponse { + try await post("/api/tuner/seek", body: SeekRequest(direction: direction)) + } + + func scanStation(body: TunerScanRequest) async throws -> TunerScanResponse { + try await post("/api/tuner/scan", body: body) + } + + func scanFullFM() async throws -> FMScanFullResponse { + try await post("/api/tuner/scan/full", body: EmptyBody()) + } + + // MARK: - Audio + + func fetchAudioProfile() async throws -> AudioProfileDTO { + try await get("/api/audio/profile") + } + + func putAudioProfile(_ profile: AudioProfileDTO) async throws { + let _: StatusResponse = try await put("/api/audio/profile", body: profile) + } + + func resetAudio() async throws { + let _: StatusResponse = try await post("/api/audio/reset", body: EmptyBody()) + } + + func setEnhance(path: String, level: Int) async throws { + let _: StatusResponse = try await post(path, body: EnhanceRequest(level: level)) + } + + // MARK: - Bluetooth + + func fetchBluetoothStatus() async throws -> BluetoothStatusResponse { + try await get("/api/bluetooth/status") + } + + func fetchSavedSpeaker() async throws -> SavedSpeakerResponse { + try await get("/api/bluetooth/speaker") + } + + func scanBluetooth(seconds: Int) async throws -> BluetoothScanResponse { + try await post("/api/bluetooth/scan", body: ScanRequest(seconds: seconds)) + } + + func connectBluetooth(mac: String, name: String?, save: Bool) async throws { + let _: StatusResponse = try await post( + "/api/bluetooth/connect", + body: ConnectRequest(mac: mac, name: name, save: save) + ) + } + + func reconnectBluetooth() async throws { + let _: StatusResponse = try await post("/api/bluetooth/reconnect", body: EmptyBody()) + } + + // MARK: - Stations + + func fetchStations() async throws -> StationListResponse { + try await get("/api/stations") + } + + func addStation(_ station: Station) async throws { + let _: StatusResponse = try await post("/api/stations", body: station) + } + + func removeStation(index: Int) async throws { + let _: StatusResponse = try await post("/api/stations/remove", body: IndexRequest(index: index)) + } + + func reorderStation(from: Int, to: Int) async throws { + let _: StatusResponse = try await post( + "/api/stations/reorder", + body: ReorderRequest(from: from, to: to) + ) + } + + func tuneStation(index: Int) async throws { + let _: StatusResponse = try await post("/api/stations/tune", body: IndexRequest(index: index)) + } + + // MARK: - Streaming + + func fetchStreaming() async throws -> StreamingState { + try await get("/api/streaming") + } + + // MARK: - HTTP helpers + + private func url(for path: String) throws -> URL { + guard let base = baseURL else { throw DigiRadioError.notConnected } + guard let url = URL(string: path, relativeTo: base) else { throw DigiRadioError.invalidHost } + return url + } + + private func get(_ path: String) async throws -> T { + let url = try url(for: path) + var request = URLRequest(url: url) + request.httpMethod = "GET" + return try await perform(request) + } + + private func post(_ path: String, body: B) async throws -> T { + let url = try url(for: path) + var request = URLRequest(url: url) + request.httpMethod = "POST" + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + request.httpBody = try JSONEncoder.api.encode(body) + return try await perform(request) + } + + private func put(_ path: String, body: B) async throws -> T { + let url = try url(for: path) + var request = URLRequest(url: url) + request.httpMethod = "PUT" + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + request.httpBody = try JSONEncoder.api.encode(body) + return try await perform(request) + } + + private func perform(_ request: URLRequest) async throws -> T { + logger.debug("HTTP \(request.httpMethod ?? "?") \(request.url?.absoluteString ?? "")") + do { + let (data, response) = try await session.data(for: request) + guard let http = response as? HTTPURLResponse else { + throw DigiRadioError.decodingFailed + } + guard (200 ... 299).contains(http.statusCode) else { + let reason = String(data: data, encoding: .utf8) + throw DigiRadioError.httpStatus(http.statusCode, reason) + } + do { + return try JSONDecoder.api.decode(T.self, from: data) + } catch { + logger.error("Decode failed: \(error.localizedDescription)") + throw DigiRadioError.decodingFailed + } + } catch let error as DigiRadioError { + throw error + } catch { + throw DigiRadioError.network(error) + } + } +} + +// MARK: - API DTOs + +struct EmptyBody: Codable {} + +struct HealthResponse: Codable { + var status: String + var fw: String + var serialNumber: String + var chips: ChipHealthResponse +} + +struct ChipHealthResponse: Codable { + var si4684: Bool + var adau1701: Bool + var bt1035: Bool +} + +struct TunerStatusResponse: Codable { + var booted: Bool + var band: String + var locked: Bool + var volume: Int + var fm: FMTunerState? + var dab: DABTunerState? + + enum CodingKeys: String, CodingKey { + case booted, band, locked, volume, fm, dab + } +} + +extension TunerStatusResponse { + func dabDecoded() -> DABTunerState? { + guard let dab else { return nil } + return DABTunerState( + freqIndex: dab.freqIndex, + ficQuality: dab.ficQuality, + cnrDb: dab.cnrDb, + playingServiceId: dab.playingServiceId, + playingComponentId: dab.playingComponentId, + dynamicLabel: dab.dynamicLabel + ) + } +} + +struct DABServicesResponse: Codable { + var services: [DABService] +} + +struct TuneRequest: Codable { + var band: String + var freqIndex: Int? + var frequencyKhz: Int? + + enum CodingKeys: String, CodingKey { + case band + case freqIndex = "freq_index" + case frequencyKhz = "frequency_khz" + } +} + +struct PlayRequest: Codable { + var serviceId: Int + var componentId: Int + + enum CodingKeys: String, CodingKey { + case serviceId = "service_id" + case componentId = "component_id" + } +} + +struct SeekRequest: Codable { + var direction: String +} + +struct SeekResponse: Codable { + var frequencyKhz: Int? + + enum CodingKeys: String, CodingKey { + case frequencyKhz = "frequency_khz" + } +} + +struct EnhanceRequest: Codable { + var level: Int +} + +struct StatusResponse: Codable { + var status: String +} + +struct BluetoothStatusResponse: Codable { + var booted: Bool + var pairing: Bool + var a2dp: String + var deviceName: String + var autoReconnect: Int + + enum CodingKeys: String, CodingKey { + case booted, pairing, a2dp + case deviceName = "device_name" + case autoReconnect = "auto_reconnect" + } +} + +struct SavedSpeakerResponse: Codable { + var configured: Bool + var mac: String? + var name: String? +} + +struct ScanRequest: Codable { + var seconds: Int +} + +struct BluetoothScanResponse: Codable { + var devices: [BluetoothScanDevice] +} + +struct BluetoothScanDevice: Codable { + var index: Int + var mac: String + var name: String + var rssiDbm: Int + + enum CodingKeys: String, CodingKey { + case index, mac, name + case rssiDbm = "rssi_dbm" + } +} + +struct ConnectRequest: Codable { + var mac: String + var name: String? + var save: Bool? +} + +struct StationListResponse: Codable { + var stations: [Station] +} + +struct IndexRequest: Codable { + var index: Int +} + +struct ReorderRequest: Codable { + var from: Int + var to: Int +} + +struct TunerScanRequest: Codable { + var band: String + var maxSteps: Int? + var name: String? + + enum CodingKeys: String, CodingKey { + case band + case maxSteps = "max_steps" + case name + } +} + +struct TunerScanResponse: Codable { + var status: String + var band: String? + var steps: Int? + var frequencyKhz: Int? + var stationName: String? + var freqIndex: Int? + var serviceId: Int? + var componentId: Int? + + enum CodingKeys: String, CodingKey { + case status, band, steps + case frequencyKhz = "frequency_khz" + case stationName = "station_name" + case freqIndex = "freq_index" + case serviceId = "service_id" + case componentId = "component_id" + } +} + +struct FMScanHit: Codable, Identifiable, Equatable { + var id: Int { frequencyKhz } + var frequencyKhz: Int + var rssiDbuv: Int? + var snrDb: Int? + var stationName: String? + + enum CodingKeys: String, CodingKey { + case frequencyKhz = "frequency_khz" + case rssiDbuv = "rssi_dbuv" + case snrDb = "snr_db" + case stationName = "station_name" + } +} + +struct FMScanFullResponse: Codable { + var stations: [FMScanHit] +} + +extension JSONEncoder { + static let api: JSONEncoder = { + let e = JSONEncoder() + return e + }() +} + +extension JSONDecoder { + static let api: JSONDecoder = { + let d = JSONDecoder() + return d + }() +} diff --git a/Software/APP/igiRadio/igiRadio/ViewModels/ConnectionViewModel.swift b/Software/APP/igiRadio/igiRadio/ViewModels/ConnectionViewModel.swift new file mode 100644 index 0000000..5c76c74 --- /dev/null +++ b/Software/APP/igiRadio/igiRadio/ViewModels/ConnectionViewModel.swift @@ -0,0 +1,67 @@ +import Foundation +import Observation + +@Observable +final class ConnectionViewModel { + private let environment: AppEnvironment + var hostInput = "digiradio-cc4db4.local" + var isBusy = false + var errorMessage: String? + let discovery = DigiRadioDiscoveryService() + + init(environment: AppEnvironment) { + self.environment = environment + if environment.useMockDevice { + hostInput = "mock.local" + } + } + + var state: DigiRadioState { environment.state } + + func connect() async { + isBusy = true + errorMessage = nil + defer { isBusy = false } + do { + try await environment.digiRadio.connect(host: hostInput) + } catch { + errorMessage = error.localizedDescription + } + } + + func connect(to host: String) async { + hostInput = host.replacingOccurrences(of: "http://", with: "").replacingOccurrences(of: "https://", with: "") + await connect() + } + + func disconnect() { + environment.digiRadio.disconnect() + } + + func toggleMock(_ enabled: Bool) { + environment.setUseMock(enabled) + if enabled { + hostInput = "mock.local" + discovery.stop() + Task { try? await environment.digiRadio.connect(host: hostInput) } + } else { + environment.digiRadio.disconnect() + } + } + + func startDiscovery() { + discovery.start() + discovery.addManualHost(hostInput) + } + + func stopDiscovery() { + discovery.stop() + } + + func pollDiscovery() { + discovery.refreshFromBLE() + if !hostInput.isEmpty { + discovery.addManualHost(hostInput) + } + } +} diff --git a/Software/APP/igiRadio/igiRadio/ViewModels/HomeViewModel.swift b/Software/APP/igiRadio/igiRadio/ViewModels/HomeViewModel.swift new file mode 100644 index 0000000..73f7d22 --- /dev/null +++ b/Software/APP/igiRadio/igiRadio/ViewModels/HomeViewModel.swift @@ -0,0 +1,52 @@ +import Foundation +import Observation + +@Observable +final class HomeViewModel { + private let service: any DigiRadioService + private var refreshTask: Task? + + init(service: any DigiRadioService) { + self.service = service + } + + var state: DigiRadioState { service.state } + + func onAppear() { + refreshTask?.cancel() + refreshTask = Task { + await refreshAll() + while !Task.isCancelled { + try? await Task.sleep(for: .seconds(3)) + guard state.connection.isConnected else { continue } + try? await service.refreshTunerStatus() + } + } + } + + func onDisappear() { + refreshTask?.cancel() + refreshTask = nil + } + + func refreshAll() async { + guard state.connection.isConnected else { return } + try? await service.refreshHealth() + try? await service.refreshTunerStatus() + try? await service.refreshAudioProfile() + try? await service.refreshBluetoothStatus() + try? await service.refreshStations() + } + + func setVolume(_ value: Double) async { + try? await service.setVolume(Int(value)) + } + + func seekFM(_ direction: String) async { + try? await service.seekFM(direction: direction) + } + + func tunePreset(at index: Int) async { + try? await service.tuneStation(at: index) + } +} diff --git a/Software/APP/igiRadio/igiRadio/Views/AudioView.swift b/Software/APP/igiRadio/igiRadio/Views/AudioView.swift new file mode 100644 index 0000000..d5f21f2 --- /dev/null +++ b/Software/APP/igiRadio/igiRadio/Views/AudioView.swift @@ -0,0 +1,86 @@ +import SwiftUI + +struct AudioView: View { + @Environment(AppEnvironment.self) private var environment + @State private var stereoLevel: Double = 0 + @State private var bassLevel: Double = 0 + @State private var masterVolume: Double = 0 + @State private var eqBands: [EQBandState] = [] + + var body: some View { + Form { + Section("Volume master") { + IGIVolumeSlider(value: $masterVolume) { value in + Task { try? await environment.digiRadio.setVolume(Int(value)) } + } + } + + Section("Enhancements") { + VStack(alignment: .leading) { + Text("Stereo enhance") + Slider(value: $stereoLevel, in: 0 ... 100, step: 1) { editing in + if !editing { + Task { try? await environment.digiRadio.setStereoEnhance(level: Int(stereoLevel)) } + } + } + } + VStack(alignment: .leading) { + Text("Bass enhance") + Slider(value: $bassLevel, in: 0 ... 100, step: 1) { editing in + if !editing { + Task { try? await environment.digiRadio.setBassEnhance(level: Int(bassLevel)) } + } + } + } + } + + Section("Equalizzatore (6 bande)") { + ForEach($eqBands) { $band in + VStack(alignment: .leading) { + Text("\(Int(band.centerHz)) Hz") + .font(.caption) + .foregroundStyle(.secondary) + Slider(value: $band.gainDb, in: -12 ... 12, step: 0.5) { editing in + if !editing { Task { await applyEQ() } } + } + } + } + } + + Section { + Button("Reset profilo audio", role: .destructive) { + Task { + try? await environment.digiRadio.resetAudio() + syncFromState() + } + } + } + } + .navigationTitle("Audio") + .onAppear { + Task { + try? await environment.digiRadio.refreshAudioProfile() + syncFromState() + } + } + .onChange(of: environment.state.audio.enhancements.stereoLevel) { _, _ in syncFromState() } + } + + private func syncFromState() { + let audio = environment.state.audio + stereoLevel = Double(audio.enhancements.stereoLevel) + bassLevel = Double(audio.enhancements.bassLevel) + masterVolume = Double(environment.state.tuner.volume) + eqBands = audio.eq + } + + private func applyEQ() async { + let profile = AudioProfileDTO( + mixer: environment.state.audio.mixer, + master: environment.state.audio.master, + eq: eqBands, + enhancements: EnhancementsState(stereoLevel: Int(stereoLevel), bassLevel: Int(bassLevel)) + ) + try? await environment.digiRadio.applyAudioProfile(profile) + } +} diff --git a/Software/APP/igiRadio/igiRadio/Views/BLEProvisioningView.swift b/Software/APP/igiRadio/igiRadio/Views/BLEProvisioningView.swift new file mode 100644 index 0000000..e4261ae --- /dev/null +++ b/Software/APP/igiRadio/igiRadio/Views/BLEProvisioningView.swift @@ -0,0 +1,62 @@ +import SwiftUI + +struct BLEProvisioningView: View { + @State private var service = BLEProvisioningService() + + var body: some View { + List { + Section("Ricerca") { + switch service.phase { + case .idle: + Text("Premi Avvia per cercare dispositivi DigiRadio in setup mode.") + .foregroundStyle(.secondary) + case .scanning: + HStack { + IGIScanningIndicator().frame(width: 28, height: 28) + Text("Cerca DigiRadio...") + } + case let .found(name, _): + Label("Trovato: \(name)", systemImage: "checkmark.circle.fill") + .foregroundStyle(.green) + case .unsupported: + Text("Provisioning BLE non ancora integrato in igiRadio.") + case let .error(message): + Text(message).foregroundStyle(.red) + } + } + + if !service.discoveredDevices.isEmpty { + Section("Dispositivi") { + ForEach(service.discoveredDevices, id: \.id) { device in + VStack(alignment: .leading) { + Text(device.name).font(.headline) + Text("RSSI \(device.rssi) dBm").font(.caption).foregroundStyle(.secondary) + } + } + } + } + + Section { + Button("Avvia scansione") { service.startScan() } + Button("Ferma scansione") { service.stopScan() } + } + + Section("Istruzioni") { + Text("1. Metti DigiRadio in setup mode (SoftAP DigiRadio-).") + Text("2. Il dispositivo espone anche BLE provisioning ESP-IDF.") + Text("3. Proof of Possession (PoP) = serial number del dispositivo.") + Text("4. Dopo il join Wi‑Fi, usa la connessione HTTP in igiRadio.") + } + .font(.footnote) + .foregroundStyle(.secondary) + + Section { + Text("UNKNOWN — specification required: implementazione completa protocomm Security1 in-app (attualmente consigliata app ESP BLE Provisioning di Espressif).") + .font(.footnote) + .foregroundStyle(.orange) + } + } + .navigationTitle("BLE Provisioning") + .onDisappear { service.stopScan() } + } +} diff --git a/Software/APP/igiRadio/igiRadio/Views/BluetoothView.swift b/Software/APP/igiRadio/igiRadio/Views/BluetoothView.swift new file mode 100644 index 0000000..74420f2 --- /dev/null +++ b/Software/APP/igiRadio/igiRadio/Views/BluetoothView.swift @@ -0,0 +1,73 @@ +import SwiftUI + +struct BluetoothView: View { + @Environment(AppEnvironment.self) private var environment + @State private var isScanning = false + + var body: some View { + let bt = environment.state.bluetooth + Form { + Section("Stato speaker (BT1035)") { + LabeledContent("Modulo", value: bt.booted ? "Attivo" : "Non pronto") + LabeledContent("A2DP", value: bt.a2dpState.rawValue.capitalized) + LabeledContent("Dispositivo", value: bt.deviceName.isEmpty ? "—" : bt.deviceName) + if let speaker = bt.savedSpeaker { + LabeledContent("Speaker salvato", value: speaker.name) + Text(speaker.mac).font(.caption).foregroundStyle(.secondary) + } + } + + Section("Azioni") { + Button(isScanning ? "Scansione..." : "Scansiona speaker vicini") { + Task { await scan() } + } + .disabled(isScanning) + + Button("Riconnetti speaker salvato") { + Task { try? await environment.digiRadio.reconnectBluetooth() } + } + } + + if !bt.nearbyDevices.isEmpty { + Section("Dispositivi trovati") { + ForEach(bt.nearbyDevices) { device in + HStack { + VStack(alignment: .leading) { + Text(device.name).font(.headline) + Text(device.mac).font(.caption).foregroundStyle(.secondary) + } + Spacer() + Text("\(device.rssiDbm) dBm") + .font(.caption) + .foregroundStyle(.secondary) + Button("Connetti") { + Task { + try? await environment.digiRadio.connectBluetooth( + mac: device.mac, + name: device.name, + save: true + ) + } + } + .buttonStyle(.borderedProminent) + } + } + } + } + + Section { + Text("Questa sezione configura il modulo Bluetooth classic (BT1035) che invia audio A2DP verso un altoparlante esterno. È distinta dalla connessione BLE usata solo per il provisioning Wi‑Fi.") + .font(.footnote) + .foregroundStyle(.secondary) + } + } + .navigationTitle("Bluetooth Audio") + .onAppear { Task { try? await environment.digiRadio.refreshBluetoothStatus() } } + } + + private func scan() async { + isScanning = true + defer { isScanning = false } + try? await environment.digiRadio.scanBluetooth(seconds: 8) + } +} diff --git a/Software/APP/igiRadio/igiRadio/Views/ConnectionView.swift b/Software/APP/igiRadio/igiRadio/Views/ConnectionView.swift new file mode 100644 index 0000000..ffe6674 --- /dev/null +++ b/Software/APP/igiRadio/igiRadio/Views/ConnectionView.swift @@ -0,0 +1,144 @@ +import SwiftUI + +struct ConnectionView: View { + @Environment(AppEnvironment.self) private var environment + @State private var viewModel: ConnectionViewModel? + @State private var discoveryTimer: Timer? + + var body: some View { + let vm = viewModel ?? ConnectionViewModel(environment: environment) + let state = environment.state + + Form { + Section("Dispositivo") { + Toggle("Modalità demo (Mock)", isOn: Binding( + get: { environment.useMockDevice }, + set: { vm.toggleMock($0) } + )) + } + + if !environment.useMockDevice { + Section("Ricerca in rete") { + if vm.discovery.isSearching { + HStack { + IGIScanningIndicator().frame(width: 28, height: 28) + Text("Cerca DigiRadio...") + } + Button("Ferma ricerca") { stopDiscovery(vm) } + } else { + Button("Cerca dispositivi") { startDiscovery(vm) } + } + + if vm.discovery.devices.isEmpty { + Text("Avvia la ricerca per trovare DigiRadio via BLE (setup) o verificare l'host inserito.") + .font(.footnote) + .foregroundStyle(.secondary) + } else { + ForEach(vm.discovery.devices) { device in + Button { + Task { await vm.connect(to: device.host) } + } label: { + HStack { + VStack(alignment: .leading, spacing: 4) { + Text(device.name).font(.headline) + Text(device.host).font(.caption).foregroundStyle(.secondary) + if let fw = device.firmware { + Text("FW \(fw)").font(.caption2).foregroundStyle(.secondary) + } + } + Spacer() + if device.isReachable { + Image(systemName: "checkmark.circle.fill").foregroundStyle(.green) + } else { + Image(systemName: "questionmark.circle").foregroundStyle(.orange) + } + } + } + .disabled(vm.isBusy) + } + } + } + } + + Section("Connessione HTTP") { + TextField("Host", text: Binding( + get: { vm.hostInput }, + set: { vm.hostInput = $0 } + )) + .textInputAutocapitalization(.never) + .autocorrectionDisabled() + .keyboardType(.URL) + + if state.connection.isConnected { + LabeledContent("Stato", value: "Connesso") + LabeledContent("Host", value: state.connection.host) + } + + if let error = vm.errorMessage ?? state.connection.lastError { + Text(error).foregroundStyle(.red) + } + } + + Section { + if state.connection.isConnected { + Button("Disconnetti", role: .destructive) { + vm.disconnect() + } + } else { + Button { + Task { await vm.connect() } + } label: { + if vm.isBusy { + HStack { + IGIScanningIndicator().frame(width: 24, height: 24) + Text("Connessione...") + } + } else { + Text("Connetti") + } + } + .disabled(vm.isBusy) + } + } + + Section("Provisioning Wi‑Fi (BLE)") { + NavigationLink("Configura Wi‑Fi via BLE") { + BLEProvisioningView() + } + Text("DigiRadio espone BLE solo per il provisioning Wi‑Fi. Il nome BLE (es. DigiRadio-CC4DB4) corrisponde all'host HTTP digiradio-cc4db4.local.") + .font(.footnote) + .foregroundStyle(.secondary) + } + + Section("Suggerimenti host") { + Text("• Setup mode: http://192.168.4.1") + Text("• STA mode: http://digiradio-.local") + Text("• Oppure IP LAN del dispositivo") + } + .font(.footnote) + .foregroundStyle(.secondary) + } + .navigationTitle("Connessione") + .onAppear { + if viewModel == nil { viewModel = vm } + if environment.useMockDevice && !state.connection.isConnected { + Task { await vm.connect() } + } + } + .onDisappear { stopDiscovery(vm) } + } + + private func startDiscovery(_ vm: ConnectionViewModel) { + vm.startDiscovery() + discoveryTimer?.invalidate() + discoveryTimer = Timer.scheduledTimer(withTimeInterval: 2, repeats: true) { _ in + vm.pollDiscovery() + } + } + + private func stopDiscovery(_ vm: ConnectionViewModel) { + discoveryTimer?.invalidate() + discoveryTimer = nil + vm.stopDiscovery() + } +} diff --git a/Software/APP/igiRadio/igiRadio/Views/DABRadioView.swift b/Software/APP/igiRadio/igiRadio/Views/DABRadioView.swift new file mode 100644 index 0000000..fc248f2 --- /dev/null +++ b/Software/APP/igiRadio/igiRadio/Views/DABRadioView.swift @@ -0,0 +1,77 @@ +import SwiftUI + +struct DABRadioView: View { + @Environment(AppEnvironment.self) private var environment + @State private var freqIndex: Double = 12 + @State private var services: [DABService] = [] + @State private var loadError: String? + + var body: some View { + let dab = environment.state.tuner.dab + Form { + Section("Ensemble") { + Stepper("Freq index: \(Int(freqIndex))", value: Binding( + get: { Int(freqIndex) }, + set: { freqIndex = Double($0) } + ), in: 0 ... 37) + Button("Sintonizza ensemble") { + Task { try? await environment.digiRadio.tuneDAB(freqIndex: Int(freqIndex)) } + } + } + + if let dab { + Section("Stato DAB") { + LabeledContent("Freq index", value: "\(dab.freqIndex)") + if let fic = dab.ficQuality { LabeledContent("FIC quality", value: "\(fic)") } + if let cnr = dab.cnrDb { LabeledContent("CNR", value: "\(cnr) dB") } + if let label = dab.dynamicLabel { LabeledContent("DLS", value: label) } + } + } + + Section("Servizi") { + if let loadError { + Text(loadError).foregroundStyle(.red) + } + if services.isEmpty { + Text("Nessun servizio caricato") + .foregroundStyle(.secondary) + } else { + ForEach(services) { service in + Button { + Task { + try? await environment.digiRadio.playDAB( + serviceId: service.serviceId, + componentId: service.componentId + ) + } + } label: { + VStack(alignment: .leading) { + Text(service.label).font(.headline) + Text("SID \(service.serviceId) · CID \(service.componentId)") + .font(.caption) + .foregroundStyle(.secondary) + } + } + } + } + Button("Carica servizi") { Task { await loadServices() } } + } + } + .navigationTitle("DAB") + .onAppear { + if let idx = environment.state.tuner.dab?.freqIndex { + freqIndex = Double(idx) + } + Task { try? await environment.digiRadio.refreshTunerStatus() } + } + } + + private func loadServices() async { + loadError = nil + do { + services = try await environment.digiRadio.listDABServices() + } catch { + loadError = error.localizedDescription + } + } +} diff --git a/Software/APP/igiRadio/igiRadio/Views/FMRadioView.swift b/Software/APP/igiRadio/igiRadio/Views/FMRadioView.swift new file mode 100644 index 0000000..9c5c8b7 --- /dev/null +++ b/Software/APP/igiRadio/igiRadio/Views/FMRadioView.swift @@ -0,0 +1,150 @@ +import SwiftUI + +struct FMRadioView: View { + @Environment(AppEnvironment.self) private var environment + @State private var frequencyMHz: Double = 102.3 + @State private var isTuning = false + @State private var isScanning = false + @State private var scanResults: [FMScanHit] = [] + @State private var scanError: String? + + var body: some View { + let fm = environment.state.tuner.fm + Form { + Section { + VStack(spacing: IGITheme.spacingM) { + Text(String(format: "%.2f", frequencyMHz)) + .font(.system(size: 56, weight: .bold, design: .rounded)) + .monospacedDigit() + .accessibilityLabel("Frequenza \(frequencyMHz) megahertz") + Slider(value: $frequencyMHz, in: 64 ... 108, step: 0.05) + HStack { + Button("−") { frequencyMHz = max(64, frequencyMHz - 0.1) } + Spacer() + Button("Sintonizza") { Task { await tune() } } + .buttonStyle(.borderedProminent) + .disabled(isTuning) + Spacer() + Button("+") { frequencyMHz = min(108, frequencyMHz + 0.1) } + } + } + .padding(.vertical, IGITheme.spacingS) + } + + if let fm { + Section("Stato") { + LabeledContent("Frequenza", value: String(format: "%.2f MHz", Double(fm.frequencyKhz) / 1000)) + if let rssi = fm.rssiDbuv { LabeledContent("RSSI", value: "\(rssi) dBµV") } + if let snr = fm.snrDb { LabeledContent("SNR", value: "\(snr) dB") } + if let stereo = fm.stereo { + LabeledContent("Stereo", value: stereo ? "Sì" : "Mono") + } + } + if let ps = fm.stationName, !ps.isEmpty { + Section("RDS") { + LabeledContent("PS", value: ps) + if let rt = fm.radiotext, !rt.isEmpty { + Text(rt).font(.body) + } + } + } + } + + Section("Seek") { + Button("Seek su") { Task { try? await environment.digiRadio.seekFM(direction: "up") } } + Button("Seek giù") { Task { try? await environment.digiRadio.seekFM(direction: "down") } } + } + + Section("Scan") { + if isScanning { + HStack { + IGIScanningIndicator().frame(width: 24, height: 24) + Text("Scansione in corso…") + } + } + if let scanError { + Text(scanError).foregroundStyle(.red) + } + Button("Cerca prossima stazione") { + Task { await scanNext() } + } + .disabled(isScanning) + Button("Scansione completa banda FM") { + Task { await scanFull() } + } + .disabled(isScanning) + + if !scanResults.isEmpty { + ForEach(scanResults) { hit in + Button { + Task { try? await environment.digiRadio.tuneFM(frequencyKhz: hit.frequencyKhz) } + } label: { + HStack { + VStack(alignment: .leading) { + Text(hit.stationName ?? "Stazione FM") + .font(.headline) + Text(String(format: "%.2f MHz", Double(hit.frequencyKhz) / 1000)) + .font(.caption) + .foregroundStyle(.secondary) + } + Spacer() + if let rssi = hit.rssiDbuv { + Text("\(rssi) dBµV") + .font(.caption) + .foregroundStyle(.secondary) + } + } + } + } + } + } + } + .navigationTitle("FM") + .onAppear { + if let khz = environment.state.tuner.fm?.frequencyKhz { + frequencyMHz = Double(khz) / 1000.0 + } + Task { try? await environment.digiRadio.refreshTunerStatus() } + } + .onChange(of: environment.state.tuner.fm?.frequencyKhz) { _, newValue in + if let khz = newValue { frequencyMHz = Double(khz) / 1000.0 } + } + } + + private func tune() async { + isTuning = true + defer { isTuning = false } + let khz = Int((frequencyMHz * 1000).rounded()) + try? await environment.digiRadio.tuneFM(frequencyKhz: khz) + } + + private func scanNext() async { + isScanning = true + scanError = nil + defer { isScanning = false } + do { + let result = try await environment.digiRadio.scanFM(maxSteps: 45, name: nil) + if result.status == "found", let khz = result.frequencyKhz { + try? await environment.digiRadio.tuneFM(frequencyKhz: khz) + } else { + scanError = "Nessuna stazione trovata" + } + } catch { + scanError = error.localizedDescription + } + } + + private func scanFull() async { + isScanning = true + scanError = nil + defer { isScanning = false } + do { + scanResults = try await environment.digiRadio.scanFullFM() + if scanResults.isEmpty { + scanError = "Nessuna stazione nella banda" + } + } catch { + scanError = error.localizedDescription + } + } +} diff --git a/Software/APP/igiRadio/igiRadio/Views/HomeView.swift b/Software/APP/igiRadio/igiRadio/Views/HomeView.swift new file mode 100644 index 0000000..ed1aaa6 --- /dev/null +++ b/Software/APP/igiRadio/igiRadio/Views/HomeView.swift @@ -0,0 +1,221 @@ +import SwiftUI + +struct HomeView: View { + @Environment(AppEnvironment.self) private var environment + @State private var viewModel: HomeViewModel? + @State private var volume: Double = 42 + + var body: some View { + let state = environment.state + ScrollView { + VStack(alignment: .leading, spacing: IGITheme.spacingL) { + header(state: state) + nowPlayingCard(state: state) + transportControls + volumeSection + presetsSection(state: state) + } + .padding(IGITheme.spacingM) + } + .background(IGITheme.screenBackground) + .navigationTitle("igiRadio") + .safeAreaInset(edge: .top) { + if !state.connection.isConnected { + NavigationLink { + ConnectionView() + } label: { + HStack { + Image(systemName: "wifi.exclamationmark") + Text("DigiRadio non connesso — tocca per collegare") + .font(.subheadline.weight(.medium)) + Spacer() + Image(systemName: "chevron.right") + } + .padding(.horizontal, IGITheme.spacingM) + .padding(.vertical, 10) + .background(.orange.opacity(0.15)) + } + .buttonStyle(.plain) + } + } + .toolbar { + ToolbarItem(placement: .topBarTrailing) { + NavigationLink { + ConnectionView() + } label: { + Label(state.connection.isConnected ? "Connesso" : "Connetti", systemImage: "link") + } + } + } + .onAppear { + if viewModel == nil { + viewModel = HomeViewModel(service: environment.digiRadio) + } + viewModel?.onAppear() + volume = Double(state.tuner.volume) + } + .onDisappear { viewModel?.onDisappear() } + .onChange(of: environment.state.tuner.volume) { _, newValue in + volume = Double(newValue) + } + } + + @ViewBuilder + private func header(state: DigiRadioState) -> some View { + HStack { + VStack(alignment: .leading, spacing: 4) { + Text("DigiRadio") + .font(.largeTitle.bold()) + HStack(spacing: 8) { + IGIStatusDot(isConnected: state.connection.isConnected) + Text(state.connection.isConnected ? "Connesso" : "Non connesso") + .foregroundStyle(.secondary) + } + } + Spacer() + if let rssi = state.tuner.fm?.rssiDbuv { + VStack(alignment: .trailing) { + Text("Segnale") + .font(.caption) + .foregroundStyle(.secondary) + IGISignalBar(level: Double(rssi) / 80.0) + .frame(width: 120) + } + } + } + } + + @ViewBuilder + private func nowPlayingCard(state: DigiRadioState) -> some View { + IGICard { + VStack(spacing: IGITheme.spacingM) { + ZStack { + RoundedRectangle(cornerRadius: 24, style: .continuous) + .fill( + LinearGradient( + colors: [.accentColor.opacity(0.35), .purple.opacity(0.25)], + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + ) + .frame(height: 220) + Image(systemName: state.tuner.band == .fm ? "radio.fill" : "antenna.radiowaves.left.and.right") + .font(.system(size: 64)) + .foregroundStyle(.white.opacity(0.9)) + } + + VStack(spacing: 6) { + Text(primaryTitle(state: state)) + .font(.title2.weight(.semibold)) + .multilineTextAlignment(.center) + Text(secondaryLine(state: state)) + .font(.subheadline) + .foregroundStyle(.secondary) + .multilineTextAlignment(.center) + } + } + } + } + + private var transportControls: some View { + HStack(spacing: IGITheme.spacingL) { + Button { + Task { await viewModel?.seekFM("down") } + } label: { + Image(systemName: "backward.fill") + .font(.title2) + .frame(width: 56, height: 56) + } + .buttonStyle(.bordered) + + Button { + Task { await viewModel?.refreshAll() } + } label: { + Image(systemName: "play.fill") + .font(.largeTitle) + .frame(width: 72, height: 72) + } + .buttonStyle(.borderedProminent) + .clipShape(Circle()) + + Button { + Task { await viewModel?.seekFM("up") } + } label: { + Image(systemName: "forward.fill") + .font(.title2) + .frame(width: 56, height: 56) + } + .buttonStyle(.bordered) + } + .frame(maxWidth: .infinity) + } + + private var volumeSection: some View { + IGICard { + IGIVolumeSlider(value: $volume) { value in + Task { await viewModel?.setVolume(value) } + } + } + } + + @ViewBuilder + private func presetsSection(state: DigiRadioState) -> some View { + IGISectionHeader(title: "Preset") + if state.stations.isEmpty { + IGIEmptyState( + title: "Nessun preset", + message: "Salva le tue stazioni preferite dalla schermata Preset.", + systemImage: "star" + ) + } else { + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: IGITheme.spacingS) { + ForEach(Array(state.stations.enumerated()), id: \.offset) { index, station in + Button { + Task { await viewModel?.tunePreset(at: index) } + } label: { + VStack(alignment: .leading) { + Text(station.name) + .font(.headline) + Text(station.band.rawValue.uppercased()) + .font(.caption) + .foregroundStyle(.secondary) + } + .padding() + .background(IGITheme.cardBackground, in: RoundedRectangle(cornerRadius: 14)) + } + .buttonStyle(.plain) + } + } + } + } + } + + private func primaryTitle(state: DigiRadioState) -> String { + switch state.tuner.band { + case .fm: + if let name = state.tuner.fm?.stationName, !name.isEmpty { return name } + if let khz = state.tuner.fm?.frequencyKhz { + return String(format: "%.2f MHz", Double(khz) / 1000.0) + } + return "FM" + case .dab: + return state.tuner.dab?.dynamicLabel ?? "DAB" + } + } + + private func secondaryLine(state: DigiRadioState) -> String { + switch state.tuner.band { + case .fm: + if let khz = state.tuner.fm?.frequencyKhz { + return String(format: "%.2f MHz", Double(khz) / 1000.0) + } + return state.tuner.fm?.radiotext ?? "FM Radio" + case .dab: + if let cnr = state.tuner.dab?.cnrDb { + return "CNR \(cnr) dB" + } + return "Digital Radio" + } + } +} diff --git a/Software/APP/igiRadio/igiRadio/Views/PresetsView.swift b/Software/APP/igiRadio/igiRadio/Views/PresetsView.swift new file mode 100644 index 0000000..3712e81 --- /dev/null +++ b/Software/APP/igiRadio/igiRadio/Views/PresetsView.swift @@ -0,0 +1,86 @@ +import SwiftUI + +struct PresetsView: View { + @Environment(AppEnvironment.self) private var environment + @State private var newName = "" + @State private var newBand: TunerBand = .fm + @State private var newFMKhz = "102300" + + var body: some View { + List { + Section { + ForEach(Array(environment.state.stations.enumerated()), id: \.offset) { index, station in + HStack { + VStack(alignment: .leading) { + Text(station.name).font(.headline) + Text(detail(for: station)).font(.caption).foregroundStyle(.secondary) + } + Spacer() + Button("Play") { + Task { try? await environment.digiRadio.tuneStation(at: index) } + } + .buttonStyle(.bordered) + } + } + .onDelete { offsets in + for index in offsets { + Task { try? await environment.digiRadio.removeStation(at: index) } + } + } + .onMove { from, to in + guard let source = from.first else { return } + Task { try? await environment.digiRadio.reorderStation(from: source, to: to) } + } + } + + Section("Aggiungi preset") { + TextField("Nome", text: $newName) + Picker("Banda", selection: $newBand) { + ForEach(TunerBand.allCases, id: \.self) { band in + Text(band.rawValue.uppercased()).tag(band) + } + } + if newBand == .fm { + TextField("Frequenza kHz", text: $newFMKhz) + .keyboardType(.numberPad) + } + Button("Salva") { Task { await savePreset() } } + .disabled(newName.isEmpty) + } + } + .navigationTitle("Preset") + .toolbar { EditButton() } + .onAppear { Task { try? await environment.digiRadio.refreshStations() } } + } + + private func detail(for station: Station) -> String { + switch station.band { + case .fm: + if let khz = station.fmFrequencyKhz { + return String(format: "FM %.2f MHz", Double(khz) / 1000) + } + return "FM" + case .dab: + return "DAB index \(station.dabFreqIndex ?? 0)" + } + } + + private func savePreset() async { + let station: Station + switch newBand { + case .fm: + guard let khz = Int(newFMKhz) else { return } + station = Station(name: newName, band: .fm, fmFrequencyKhz: khz) + case .dab: + station = Station( + name: newName, + band: .dab, + dabFreqIndex: Int(environment.state.tuner.dab?.freqIndex ?? 0), + dabServiceId: environment.state.tuner.dab?.playingServiceId, + dabComponentId: environment.state.tuner.dab?.playingComponentId + ) + } + try? await environment.digiRadio.saveStation(station) + newName = "" + } +} diff --git a/Software/APP/igiRadio/igiRadio/Views/RootView.swift b/Software/APP/igiRadio/igiRadio/Views/RootView.swift new file mode 100644 index 0000000..d11c887 --- /dev/null +++ b/Software/APP/igiRadio/igiRadio/Views/RootView.swift @@ -0,0 +1,95 @@ +import SwiftUI + +struct RootView: View { + @Environment(AppEnvironment.self) private var environment + @Environment(\.horizontalSizeClass) private var horizontalSizeClass + + var body: some View { + Group { + if horizontalSizeClass == .regular { + IPadRootView() + } else { + IPhoneRootView() + } + } + .tint(IGITheme.accent) + } +} + +private struct IPhoneRootView: View { + @Environment(AppEnvironment.self) private var environment + + var body: some View { + TabView { + NavigationStack { HomeView() } + .tabItem { Label("Home", systemImage: "house.fill") } + + NavigationStack { FMRadioView() } + .tabItem { Label("FM", systemImage: "dot.radiowaves.left.and.right") } + + NavigationStack { DABRadioView() } + .tabItem { Label("DAB", systemImage: "antenna.radiowaves.left.and.right") } + + NavigationStack { PresetsView() } + .tabItem { Label("Preset", systemImage: "star.fill") } + + NavigationStack { SettingsRootView() } + .tabItem { Label("Impostazioni", systemImage: "gearshape.fill") } + } + } +} + +private struct IPadRootView: View { + @State private var selection: SidebarItem? = .home + + var body: some View { + NavigationSplitView { + List(SidebarItem.allCases, selection: $selection) { item in + NavigationLink(value: item) { + Label(item.title, systemImage: item.systemImage) + } + } + .navigationTitle("igiRadio") + } detail: { + switch selection ?? .home { + case .home: HomeView() + case .fm: FMRadioView() + case .dab: DABRadioView() + case .bluetooth: BluetoothView() + case .audio: AudioView() + case .presets: PresetsView() + case .settings: SettingsRootView() + } + } + } +} + +enum SidebarItem: String, CaseIterable, Identifiable { + case home, fm, dab, bluetooth, audio, presets, settings + + var id: String { rawValue } + + var title: String { + switch self { + case .home: "Home" + case .fm: "FM" + case .dab: "DAB" + case .bluetooth: "Bluetooth" + case .audio: "Audio" + case .presets: "Preset" + case .settings: "Impostazioni" + } + } + + var systemImage: String { + switch self { + case .home: "house.fill" + case .fm: "dot.radiowaves.left.and.right" + case .dab: "antenna.radiowaves.left.and.right" + case .bluetooth: "dot.radiowaves.left.and.right" + case .audio: "waveform" + case .presets: "star.fill" + case .settings: "gearshape.fill" + } + } +} diff --git a/Software/APP/igiRadio/igiRadio/Views/SettingsViews.swift b/Software/APP/igiRadio/igiRadio/Views/SettingsViews.swift new file mode 100644 index 0000000..f841ed9 --- /dev/null +++ b/Software/APP/igiRadio/igiRadio/Views/SettingsViews.swift @@ -0,0 +1,100 @@ +import SwiftUI + +struct SettingsRootView: View { + var body: some View { + List { + Section("DigiRadio") { + NavigationLink("Connessione") { ConnectionView() } + NavigationLink("Informazioni dispositivo") { DeviceInfoView() } + NavigationLink("Diagnostica") { DiagnosticsView() } + NavigationLink("Firmware") { FirmwareView() } + } + Section("Radio") { + NavigationLink("FM") { FMRadioView() } + NavigationLink("DAB") { DABRadioView() } + NavigationLink("Preset") { PresetsView() } + } + Section("Audio") { + NavigationLink("Profilo audio") { AudioView() } + NavigationLink("Bluetooth speaker") { BluetoothView() } + } + } + .navigationTitle("Impostazioni") + } +} + +struct DeviceInfoView: View { + @Environment(AppEnvironment.self) private var environment + + var body: some View { + let health = environment.state.health + let chips = health.chips + Form { + LabeledContent("Nome", value: "DigiRadio") + LabeledContent("Firmware", value: health.firmware.isEmpty ? "—" : health.firmware) + LabeledContent("Serial number", value: health.serialNumber.isEmpty ? "—" : health.serialNumber) + LabeledContent("Stato", value: health.status.isEmpty ? "—" : health.status) + Section("Chip") { + LabeledContent("Si4684", value: chips.si4684 ? "OK" : "—") + LabeledContent("ADAU1701", value: chips.adau1701 ? "OK" : "—") + LabeledContent("BT1035", value: chips.bt1035 ? "OK" : "—") + } + Section("Connessione") { + LabeledContent("Host", value: environment.state.connection.host.isEmpty ? "—" : environment.state.connection.host) + LabeledContent("HTTP", value: environment.state.connection.isConnected ? "Connesso" : "Non connesso") + } + } + .navigationTitle("Dispositivo") + .onAppear { Task { try? await environment.digiRadio.refreshHealth() } } + } +} + +struct DiagnosticsView: View { + @Environment(AppEnvironment.self) private var environment + + var body: some View { + Form { + Section("Tuner") { + LabeledContent("Booted", value: environment.state.tuner.booted ? "Sì" : "No") + LabeledContent("Banda", value: environment.state.tuner.band.rawValue.uppercased()) + LabeledContent("Locked", value: environment.state.tuner.locked ? "Sì" : "No") + } + Section("Bluetooth") { + LabeledContent("A2DP", value: environment.state.bluetooth.a2dpState.rawValue) + LabeledContent("Pairing", value: environment.state.bluetooth.pairing ? "Sì" : "No") + } + Section { + Button("Aggiorna snapshot") { + Task { + try? await environment.digiRadio.refreshHealth() + try? await environment.digiRadio.refreshTunerStatus() + try? await environment.digiRadio.refreshBluetoothStatus() + } + } + } + } + .navigationTitle("Diagnostica") + } +} + +struct FirmwareView: View { + @Environment(AppEnvironment.self) private var environment + + var body: some View { + Form { + Section("Versione corrente") { + LabeledContent("Firmware", value: environment.state.health.firmware.isEmpty ? "—" : environment.state.health.firmware) + } + Section("Aggiornamento OTA") { + Text("L'aggiornamento firmware è supportato via POST /api/system/ota con un file .bin ESP-IDF valido. L'interfaccia di upload file sarà aggiunta in una versione successiva.") + .font(.footnote) + .foregroundStyle(.secondary) + Text("UNKNOWN — specification required: progress reporting durante OTA stream.") + .font(.footnote) + .foregroundStyle(.orange) + } + } + .navigationTitle("Firmware") + .onAppear { Task { try? await environment.digiRadio.refreshHealth() } } + } +} From 3afcba0d3658c0b6c86bb8f17ee50934c154f1d7 Mon Sep 17 00:00:00 2001 From: Michele Bigi Date: Tue, 18 Aug 2026 15:56:51 +0200 Subject: [PATCH 02/12] Polish igiRadio UI with premium audio, home, and FM screens. Adds graphic EQ, vertical mixer faders, redesigned home and FM layouts, and an Audio tab for quicker access to DSP controls. Co-authored-by: Cursor --- .../igiRadio.xcodeproj/project.pbxproj | 134 +++--- .../igiRadio/Components/AudioComponents.swift | 435 ++++++++++++++++++ .../igiRadio/Components/RadioComponents.swift | 270 +++++++++++ .../igiRadio/ViewModels/AudioViewModel.swift | 75 +++ .../igiRadio/igiRadio/Views/AudioView.swift | 279 ++++++++--- .../igiRadio/igiRadio/Views/FMRadioView.swift | 220 +++++---- .../igiRadio/igiRadio/Views/HomeView.swift | 209 +++++---- .../igiRadio/igiRadio/Views/RootView.swift | 3 + .../igiRadio/Views/SettingsViews.swift | 6 +- 9 files changed, 1311 insertions(+), 320 deletions(-) create mode 100644 Software/APP/igiRadio/igiRadio/Components/AudioComponents.swift create mode 100644 Software/APP/igiRadio/igiRadio/Components/RadioComponents.swift create mode 100644 Software/APP/igiRadio/igiRadio/ViewModels/AudioViewModel.swift diff --git a/Software/APP/igiRadio/igiRadio.xcodeproj/project.pbxproj b/Software/APP/igiRadio/igiRadio.xcodeproj/project.pbxproj index cd105b9..c944f63 100644 --- a/Software/APP/igiRadio/igiRadio.xcodeproj/project.pbxproj +++ b/Software/APP/igiRadio/igiRadio.xcodeproj/project.pbxproj @@ -3,12 +3,9 @@ archiveVersion = 1; classes = { }; - objectVersion = 77; + objectVersion = 71; objects = { -/* Begin PBXBuildFile section */ -/* End PBXBuildFile section */ - /* Begin PBXContainerItemProxy section */ C050F319BDFADBF7CE6700F8 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; @@ -25,7 +22,7 @@ /* End PBXFileReference section */ /* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ - 7D085337BF0EA6EB04337F45 /* Exceptions for "igiRadio" folder in "igiRadio" target */ = { + 7D085337BF0EA6EB04337F45 /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = { isa = PBXFileSystemSynchronizedBuildFileExceptionSet; membershipExceptions = ( Resources/Info.plist, @@ -35,30 +32,19 @@ /* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ /* Begin PBXFileSystemSynchronizedRootGroup section */ - F7E3CB0B027CB68EA63A4AB8 /* igiRadio */ = { - isa = PBXFileSystemSynchronizedRootGroup; - exceptions = ( - 7D085337BF0EA6EB04337F45 /* Exceptions for "igiRadio" folder in "igiRadio" target */, - ); - path = igiRadio; - sourceTree = ""; - }; - 693436FA1780106DA05D06E3 /* igiRadioTests */ = { - isa = PBXFileSystemSynchronizedRootGroup; - path = Tests/igiRadioTests; - sourceTree = ""; - }; + 693436FA1780106DA05D06E3 /* Tests/igiRadioTests */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = Tests/igiRadioTests; sourceTree = ""; }; + F7E3CB0B027CB68EA63A4AB8 /* igiRadio */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (7D085337BF0EA6EB04337F45 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = igiRadio; sourceTree = ""; }; /* End PBXFileSystemSynchronizedRootGroup section */ /* Begin PBXFrameworksBuildPhase section */ - 71F357EA3EEC95DAEFCF7345 /* Frameworks */ = { + 0A8032441559F0A3B8F103F8 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; - 0A8032441559F0A3B8F103F8 /* Frameworks */ = { + 71F357EA3EEC95DAEFCF7345 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( @@ -81,7 +67,7 @@ isa = PBXGroup; children = ( F7E3CB0B027CB68EA63A4AB8 /* igiRadio */, - 693436FA1780106DA05D06E3 /* igiRadioTests */, + 693436FA1780106DA05D06E3 /* Tests/igiRadioTests */, 2761999F42D5E1910DD2B679 /* Products */, ); sourceTree = ""; @@ -122,7 +108,7 @@ 0E0716BAD2A3DFD42CA27715 /* PBXTargetDependency */, ); fileSystemSynchronizedGroups = ( - 693436FA1780106DA05D06E3 /* igiRadioTests */, + 693436FA1780106DA05D06E3 /* Tests/igiRadioTests */, ); name = igiRadioTests; productName = igiRadioTests; @@ -203,24 +189,6 @@ /* End PBXTargetDependency section */ /* Begin XCBuildConfiguration section */ - FDBD275B398C4976FD6BC80D /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ENABLE_MODULES = YES; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = dwarf; - ENABLE_TESTABILITY = YES; - GCC_OPTIMIZATION_LEVEL = 0; - IPHONEOS_DEPLOYMENT_TARGET = 17.0; - MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = iphoneos; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - }; - name = Debug; - }; 54274B7C1572A50D5BE0DEA8 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { @@ -262,31 +230,6 @@ }; name = Debug; }; - FA1BD134133059F9163AC34F /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - ENABLE_PREVIEWS = YES; - GENERATE_INFOPLIST_FILE = NO; - INFOPLIST_FILE = igiRadio/Resources/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 17.0; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.digiradio.igiRadio; - PRODUCT_NAME = "$(TARGET_NAME)"; - SDKROOT = iphoneos; - SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; - SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Release; - }; 90735310944459417946066C /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -327,9 +270,61 @@ }; name = Release; }; + FA1BD134133059F9163AC34F /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = NO; + INFOPLIST_FILE = igiRadio/Resources/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.digiradio.igiRadio; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + FDBD275B398C4976FD6BC80D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ENABLE_MODULES = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_TESTABILITY = YES; + GCC_OPTIMIZATION_LEVEL = 0; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ + 61D281E9594B71DA372FEE34 /* Build configuration list for PBXNativeTarget "igiRadioTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 90735310944459417946066C /* Debug */, + E6AD029DFF2D9078F77BDCBA /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; C8A2C2BD01CBC6B9DFC6F199 /* Build configuration list for PBXProject "igiRadio" */ = { isa = XCConfigurationList; buildConfigurations = ( @@ -348,15 +343,6 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - 61D281E9594B71DA372FEE34 /* Build configuration list for PBXNativeTarget "igiRadioTests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 90735310944459417946066C /* Debug */, - E6AD029DFF2D9078F77BDCBA /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; /* End XCConfigurationList section */ }; rootObject = D7ACBDA7D90DDE7BBA02A41B /* Project object */; diff --git a/Software/APP/igiRadio/igiRadio/Components/AudioComponents.swift b/Software/APP/igiRadio/igiRadio/Components/AudioComponents.swift new file mode 100644 index 0000000..b0954ea --- /dev/null +++ b/Software/APP/igiRadio/igiRadio/Components/AudioComponents.swift @@ -0,0 +1,435 @@ +import SwiftUI + +// MARK: - Vertical fader (mixer channels) + +struct IGIVerticalFader: View { + var label: String + var icon: String + @Binding var valueDb: Double + var range: ClosedRange = -40 ... 12 + var onCommit: () -> Void + + private let trackHeight: CGFloat = 160 + + var body: some View { + VStack(spacing: IGITheme.spacingS) { + Text(formattedDb) + .font(.caption.monospacedDigit().weight(.semibold)) + .foregroundStyle(valueDb > 0 ? IGITheme.accent : .secondary) + .contentTransition(.numericText()) + .animation(.snappy, value: valueDb) + + GeometryReader { geo in + let h = geo.size.height + ZStack(alignment: .bottom) { + Capsule() + .fill(Color.primary.opacity(0.08)) + Capsule() + .fill( + LinearGradient( + colors: [IGITheme.accent.opacity(0.35), IGITheme.accent], + startPoint: .bottom, + endPoint: .top + ) + ) + .frame(height: fillHeight(total: h)) + } + .overlay(alignment: .top) { + Circle() + .fill(.background) + .shadow(color: .black.opacity(0.18), radius: 4, y: 2) + .overlay(Circle().stroke(IGITheme.accent.opacity(0.5), lineWidth: 2)) + .frame(width: 28, height: 28) + .offset(y: thumbOffset(total: h)) + } + .gesture( + DragGesture(minimumDistance: 0) + .onChanged { drag in + valueDb = db(from: drag.location.y, height: h) + } + .onEnded { _ in onCommit() } + ) + .accessibilityElement(children: .ignore) + .accessibilityLabel("\(label), \(formattedDb)") + .accessibilityAdjustableAction { direction in + let step = 0.5 + switch direction { + case .increment: valueDb = min(range.upperBound, valueDb + step) + case .decrement: valueDb = max(range.lowerBound, valueDb - step) + @unknown default: break + } + onCommit() + } + } + .frame(height: trackHeight) + + Image(systemName: icon) + .font(.caption) + .foregroundStyle(.secondary) + Text(label) + .font(.caption2.weight(.medium)) + .multilineTextAlignment(.center) + .lineLimit(2) + .frame(width: 52) + } + .frame(width: 64) + } + + private var formattedDb: String { + valueDb >= 0 ? String(format: "+%.1f", valueDb) : String(format: "%.1f", valueDb) + } + + private func normalized(_ db: Double) -> Double { + (db - range.lowerBound) / (range.upperBound - range.lowerBound) + } + + private func fillHeight(total: CGFloat) -> CGFloat { + max(4, total * normalized(valueDb)) + } + + private func thumbOffset(total: CGFloat) -> CGFloat { + let travel = total - 28 + return travel * (1 - normalized(valueDb)) + } + + private func db(from y: CGFloat, height: CGFloat) -> Double { + let clamped = max(0, min(height, y)) + let norm = 1 - (clamped / height) + let raw = range.lowerBound + norm * (range.upperBound - range.lowerBound) + return (raw * 2).rounded() / 2 + } +} + +// MARK: - Mixer channel group + +struct IGIMixerChannelGroup: View { + var title: String + var subtitle: String + var systemImage: String + @Binding var leftDb: Double + @Binding var rightDb: Double + var onCommit: () -> Void + + var body: some View { + VStack(alignment: .leading, spacing: IGITheme.spacingM) { + HStack(spacing: IGITheme.spacingS) { + Image(systemName: systemImage) + .font(.title3) + .foregroundStyle(IGITheme.accent) + .frame(width: 36, height: 36) + .background(IGITheme.accent.opacity(0.12), in: RoundedRectangle(cornerRadius: 10)) + VStack(alignment: .leading, spacing: 2) { + Text(title).font(.headline) + Text(subtitle).font(.caption).foregroundStyle(.secondary) + } + } + + HStack(spacing: IGITheme.spacingL) { + Spacer() + IGIVerticalFader(label: "Sinistra", icon: "l.circle", valueDb: $leftDb, onCommit: onCommit) + IGIVerticalFader(label: "Destra", icon: "r.circle", valueDb: $rightDb, onCommit: onCommit) + Spacer() + } + } + .igiAudioCard() + } +} + +// MARK: - Graphic equalizer + +struct IGIGraphicEqualizer: View { + @Binding var bands: [EQBandState] + var onCommit: () -> Void + + private let gainRange: ClosedRange = -12 ... 12 + private let barMaxHeight: CGFloat = 120 + + var body: some View { + VStack(spacing: IGITheme.spacingM) { + // Curve preview + GeometryReader { geo in + let w = geo.size.width + let h = geo.size.height + let sorted = bands.sorted { $0.centerHz < $1.centerHz } + if sorted.count >= 2 { + Path { path in + for (i, band) in sorted.enumerated() { + let x = w * CGFloat(i) / CGFloat(max(1, sorted.count - 1)) + let y = h - normalizedGain(band.gainDb) * h + if i == 0 { path.move(to: CGPoint(x: x, y: y)) } + else { path.addLine(to: CGPoint(x: x, y: y)) } + } + } + .stroke( + LinearGradient(colors: [IGITheme.accent, .purple], startPoint: .leading, endPoint: .trailing), + style: StrokeStyle(lineWidth: 2.5, lineCap: .round, lineJoin: .round) + ) + } + } + .frame(height: 48) + .padding(.horizontal, IGITheme.spacingS) + + HStack(alignment: .bottom, spacing: IGITheme.spacingS) { + ForEach($bands) { $band in + EQBar( + centerHz: band.centerHz, + gainDb: $band.gainDb, + maxHeight: barMaxHeight, + range: gainRange, + onCommit: onCommit + ) + } + } + .frame(maxWidth: .infinity) + + HStack { + Text("-12 dB").font(.caption2).foregroundStyle(.secondary) + Spacer() + Text("0").font(.caption2.weight(.medium)) + Spacer() + Text("+12 dB").font(.caption2).foregroundStyle(.secondary) + } + } + .igiAudioCard() + } + + private func normalizedGain(_ db: Double) -> CGFloat { + CGFloat((db - gainRange.lowerBound) / (gainRange.upperBound - gainRange.lowerBound)) + } +} + +private struct EQBar: View { + var centerHz: Double + @Binding var gainDb: Double + var maxHeight: CGFloat + var range: ClosedRange + var onCommit: () -> Void + + var body: some View { + VStack(spacing: 6) { + Text(gainLabel) + .font(.system(size: 10, weight: .semibold, design: .rounded)) + .monospacedDigit() + .foregroundStyle(gainDb == 0 ? .secondary : IGITheme.accent) + .frame(height: 14) + + GeometryReader { geo in + let h = geo.size.height + let mid = h * 0.5 + ZStack(alignment: .bottom) { + RoundedRectangle(cornerRadius: 8, style: .continuous) + .fill(Color.primary.opacity(0.06)) + + if gainDb >= 0 { + RoundedRectangle(cornerRadius: 8, style: .continuous) + .fill(barGradient) + .frame(height: max(4, normalized(gainDb) * mid)) + .offset(y: -mid) + } else { + RoundedRectangle(cornerRadius: 8, style: .continuous) + .fill(barGradient) + .frame(height: max(4, normalized(abs(gainDb)) * mid)) + .offset(y: -(mid - max(4, normalized(abs(gainDb)) * mid))) + } + } + .overlay { + Rectangle() + .fill(Color.primary.opacity(0.2)) + .frame(height: 1) + .position(x: geo.size.width / 2, y: mid) + } + .gesture( + DragGesture(minimumDistance: 0) + .onChanged { drag in + gainDb = db(from: drag.location.y, height: h) + } + .onEnded { _ in onCommit() } + ) + } + .frame(height: maxHeight) + + Text(freqLabel) + .font(.system(size: 9, weight: .medium, design: .rounded)) + .foregroundStyle(.secondary) + .lineLimit(1) + .minimumScaleFactor(0.7) + } + .frame(maxWidth: .infinity) + .accessibilityElement(children: .ignore) + .accessibilityLabel("Banda \(freqLabel)") + .accessibilityValue(gainLabel) + } + + private var gainLabel: String { + gainDb >= 0 ? String(format: "+%.1f", gainDb) : String(format: "%.1f", gainDb) + } + + private var freqLabel: String { + if centerHz >= 1000 { + return String(format: "%.0fk", centerHz / 1000) + } + return String(format: "%.0f", centerHz) + } + + private var barGradient: LinearGradient { + if gainDb >= 0 { + return LinearGradient(colors: [IGITheme.accent.opacity(0.5), IGITheme.accent], startPoint: .bottom, endPoint: .top) + } + return LinearGradient(colors: [.orange.opacity(0.4), .orange], startPoint: .top, endPoint: .bottom) + } + + private func normalized(_ db: Double) -> CGFloat { + CGFloat(db / range.upperBound) + } + + private func db(from y: CGFloat, height: CGFloat) -> Double { + let clamped = max(0, min(height, y)) + let norm = 1 - (clamped / height) + let raw = range.lowerBound + norm * (range.upperBound - range.lowerBound) + return (raw * 2).rounded() / 2 + } +} + +// MARK: - Enhancement arc dial + +struct IGIEnhancementDial: View { + var title: String + var systemImage: String + @Binding var level: Double + var onCommit: () -> Void + + @State private var dragOrigin: Double? + + var body: some View { + VStack(spacing: IGITheme.spacingS) { + ZStack { + Circle() + .stroke(Color.primary.opacity(0.08), lineWidth: 10) + Circle() + .trim(from: 0, to: level / 100) + .stroke( + AngularGradient(colors: [IGITheme.accent.opacity(0.4), IGITheme.accent], center: .center), + style: StrokeStyle(lineWidth: 10, lineCap: .round) + ) + .rotationEffect(.degrees(-90)) + VStack(spacing: 2) { + Image(systemName: systemImage) + .font(.title3) + .foregroundStyle(IGITheme.accent) + Text("\(Int(level))") + .font(.title2.weight(.bold).monospacedDigit()) + } + } + .frame(width: 88, height: 88) + .gesture( + DragGesture(minimumDistance: 0) + .onChanged { drag in + if dragOrigin == nil { dragOrigin = level } + let base = dragOrigin ?? level + level = max(0, min(100, base - drag.translation.height / 2)) + } + .onEnded { _ in + dragOrigin = nil + onCommit() + } + ) + .accessibilityLabel(title) + .accessibilityValue("\(Int(level)) su cento") + + Text(title) + .font(.caption.weight(.medium)) + .foregroundStyle(.secondary) + } + .frame(maxWidth: .infinity) + } +} + +// MARK: - Master volume hero + +struct IGIMasterVolumeHero: View { + @Binding var leftDb: Double + @Binding var rightDb: Double + var onCommit: () -> Void + + private var linkedDb: Binding { + Binding( + get: { (leftDb + rightDb) / 2 }, + set: { newValue in + leftDb = newValue + rightDb = newValue + } + ) + } + + var body: some View { + VStack(spacing: IGITheme.spacingM) { + HStack { + VStack(alignment: .leading, spacing: 4) { + Text("Volume master") + .font(.subheadline.weight(.medium)) + .foregroundStyle(.secondary) + Text("\(Int(linkedDb.wrappedValue))") + .font(.system(size: 48, weight: .bold, design: .rounded)) + .monospacedDigit() + .contentTransition(.numericText()) + } + Spacer() + Image(systemName: "speaker.wave.3.fill") + .font(.largeTitle) + .foregroundStyle(IGITheme.accent.gradient) + .symbolEffect(.variableColor.iterative, options: .repeating, value: linkedDb.wrappedValue) + } + + Slider(value: linkedDb, in: 0 ... 100, step: 1) { editing in + if !editing { onCommit() } + } + .tint(IGITheme.accent) + + HStack(spacing: IGITheme.spacingL) { + stereoBalance(label: "L", value: $leftDb) + stereoBalance(label: "R", value: $rightDb) + } + } + .igiAudioCard() + } + + @ViewBuilder + private func stereoBalance(label: String, value: Binding) -> some View { + VStack(alignment: .leading, spacing: 4) { + Text("Canale \(label)") + .font(.caption2) + .foregroundStyle(.secondary) + HStack { + Text(label).font(.caption.weight(.bold)) + Slider(value: value, in: 0 ... 100, step: 1) { editing in + if !editing { onCommit() } + } + .tint(.secondary) + } + } + } +} + +// MARK: - Card style + +extension View { + func igiAudioCard() -> some View { + igiPremiumCard() + } +} + +enum AudioPanel: String, CaseIterable, Identifiable { + case mixer = "Mixer" + case equalizer = "EQ" + case enhance = "FX" + + var id: String { rawValue } + + var icon: String { + switch self { + case .mixer: "slider.vertical.3" + case .equalizer: "waveform.path.ecg" + case .enhance: "sparkles" + } + } +} diff --git a/Software/APP/igiRadio/igiRadio/Components/RadioComponents.swift b/Software/APP/igiRadio/igiRadio/Components/RadioComponents.swift new file mode 100644 index 0000000..f01f705 --- /dev/null +++ b/Software/APP/igiRadio/igiRadio/Components/RadioComponents.swift @@ -0,0 +1,270 @@ +import SwiftUI + +// MARK: - Shared premium surface + +extension View { + func igiPremiumCard() -> some View { + self + .padding(IGITheme.spacingM) + .background { + RoundedRectangle(cornerRadius: IGITheme.cardRadius, style: .continuous) + .fill(.ultraThinMaterial) + .shadow(color: .black.opacity(0.06), radius: 12, y: 4) + } + } +} + +struct IGIHeroBackground: View { + var body: some View { + LinearGradient( + colors: [ + IGITheme.screenBackground, + IGITheme.accent.opacity(0.08), + IGITheme.screenBackground + ], + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + .ignoresSafeArea() + } +} + +// MARK: - Transport + +struct IGITransportCluster: View { + var onPrevious: () -> Void + var onPlay: () -> Void + var onNext: () -> Void + + var body: some View { + HStack(spacing: IGITheme.spacingL) { + transportButton(icon: "backward.fill", size: 56, prominent: false, action: onPrevious) + transportButton(icon: "play.fill", size: 76, prominent: true, action: onPlay) + transportButton(icon: "forward.fill", size: 56, prominent: false, action: onNext) + } + .frame(maxWidth: .infinity) + } + + @ViewBuilder + private func transportButton(icon: String, size: CGFloat, prominent: Bool, action: @escaping () -> Void) -> some View { + Button(action: action) { + Image(systemName: icon) + .font(prominent ? .largeTitle : .title2) + .foregroundStyle(prominent ? .white : .primary) + .frame(width: size, height: size) + .background { + if prominent { + Circle().fill(IGITheme.accent.gradient) + .shadow(color: IGITheme.accent.opacity(0.35), radius: 12, y: 6) + } else { + Circle().fill(.ultraThinMaterial) + } + } + } + .buttonStyle(.plain) + } +} + +// MARK: - Band badge + +struct IGIBandBadge: View { + var band: TunerBand + var locked: Bool + + var body: some View { + HStack(spacing: 6) { + Image(systemName: band == .fm ? "dot.radiowaves.left.and.right" : "antenna.radiowaves.left.and.right") + Text(band.rawValue.uppercased()) + .fontWeight(.bold) + if locked { + Image(systemName: "lock.fill") + .font(.caption2) + } + } + .font(.caption) + .padding(.horizontal, 12) + .padding(.vertical, 6) + .background(IGITheme.accent.opacity(0.15), in: Capsule()) + .foregroundStyle(IGITheme.accent) + } +} + +// MARK: - Preset chip + +struct IGIPresetChip: View { + var name: String + var subtitle: String + var action: () -> Void + + var body: some View { + Button(action: action) { + VStack(alignment: .leading, spacing: 4) { + Text(name) + .font(.subheadline.weight(.semibold)) + .lineLimit(1) + Text(subtitle) + .font(.caption2) + .foregroundStyle(.secondary) + } + .padding(.horizontal, 14) + .padding(.vertical, 12) + .frame(minWidth: 110, alignment: .leading) + .background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 14, style: .continuous)) + .overlay( + RoundedRectangle(cornerRadius: 14, style: .continuous) + .strokeBorder(Color.primary.opacity(0.06)) + ) + } + .buttonStyle(.plain) + } +} + +// MARK: - Metric bar + +struct IGIMetricBar: View { + var label: String + var value: String + var level: Double + + var body: some View { + VStack(alignment: .leading, spacing: 6) { + HStack { + Text(label).font(.caption).foregroundStyle(.secondary) + Spacer() + Text(value).font(.caption.monospacedDigit().weight(.medium)) + } + GeometryReader { geo in + ZStack(alignment: .leading) { + Capsule().fill(Color.primary.opacity(0.08)) + Capsule() + .fill(IGITheme.accent.gradient) + .frame(width: geo.size.width * max(0, min(1, level))) + } + } + .frame(height: 6) + } + } +} + +// MARK: - FM frequency hero + +struct IGIFrequencyHero: View { + @Binding var frequencyMHz: Double + var stationName: String? + var isTuning: Bool + var onTune: () -> Void + + var body: some View { + VStack(spacing: IGITheme.spacingL) { + if let stationName, !stationName.isEmpty { + Text(stationName) + .font(.title3.weight(.semibold)) + .foregroundStyle(IGITheme.accent) + .lineLimit(1) + } + + ZStack { + Circle() + .stroke(Color.primary.opacity(0.06), lineWidth: 20) + Circle() + .trim(from: 0, to: normalizedFrequency) + .stroke( + AngularGradient(colors: [IGITheme.accent.opacity(0.3), IGITheme.accent], center: .center), + style: StrokeStyle(lineWidth: 20, lineCap: .round) + ) + .rotationEffect(.degrees(-90)) + .animation(.snappy, value: frequencyMHz) + + VStack(spacing: 4) { + Text(String(format: "%.2f", frequencyMHz)) + .font(.system(size: 44, weight: .bold, design: .rounded)) + .monospacedDigit() + .contentTransition(.numericText()) + Text("MHz") + .font(.caption.weight(.semibold)) + .foregroundStyle(.secondary) + } + } + .frame(width: 220, height: 220) + + Slider(value: $frequencyMHz, in: 64 ... 108, step: 0.05) + .tint(IGITheme.accent) + + HStack(spacing: IGITheme.spacingM) { + stepButton("−0.1") { frequencyMHz = max(64, frequencyMHz - 0.1) } + Button(action: onTune) { + Group { + if isTuning { + ProgressView().tint(.white) + } else { + Text("Sintonizza") + .fontWeight(.semibold) + } + } + .frame(maxWidth: .infinity) + .padding(.vertical, 14) + } + .buttonStyle(.borderedProminent) + .disabled(isTuning) + stepButton("+0.1") { frequencyMHz = min(108, frequencyMHz + 0.1) } + } + } + .igiPremiumCard() + } + + private var normalizedFrequency: Double { + (frequencyMHz - 64) / (108 - 64) + } + + @ViewBuilder + private func stepButton(_ title: String, action: @escaping () -> Void) -> some View { + Button(title, action: action) + .font(.subheadline.weight(.semibold).monospacedDigit()) + .frame(width: 56, height: 48) + .background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 12)) + .buttonStyle(.plain) + } +} + +// MARK: - Scan result row + +struct IGIScanResultRow: View { + var title: String + var frequencyMHz: Double + var rssi: Int? + var action: () -> Void + + var body: some View { + Button(action: action) { + HStack(spacing: IGITheme.spacingM) { + ZStack { + RoundedRectangle(cornerRadius: 10, style: .continuous) + .fill(IGITheme.accent.opacity(0.12)) + .frame(width: 44, height: 44) + Image(systemName: "radio.fill") + .foregroundStyle(IGITheme.accent) + } + VStack(alignment: .leading, spacing: 2) { + Text(title).font(.headline).lineLimit(1) + Text(String(format: "%.2f MHz", frequencyMHz)) + .font(.caption) + .foregroundStyle(.secondary) + } + Spacer() + if let rssi { + Text("\(rssi)") + .font(.caption.monospacedDigit().weight(.semibold)) + .padding(.horizontal, 8) + .padding(.vertical, 4) + .background(Color.primary.opacity(0.06), in: Capsule()) + } + Image(systemName: "play.circle.fill") + .font(.title2) + .foregroundStyle(IGITheme.accent) + } + .padding(IGITheme.spacingS) + .background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 14, style: .continuous)) + } + .buttonStyle(.plain) + } +} diff --git a/Software/APP/igiRadio/igiRadio/ViewModels/AudioViewModel.swift b/Software/APP/igiRadio/igiRadio/ViewModels/AudioViewModel.swift new file mode 100644 index 0000000..fb7f875 --- /dev/null +++ b/Software/APP/igiRadio/igiRadio/ViewModels/AudioViewModel.swift @@ -0,0 +1,75 @@ +import Foundation +import Observation + +@Observable +final class AudioViewModel { + private let service: any DigiRadioService + private var applyTask: Task? + + var panel: AudioPanel = .mixer + var mixer = MixerState() + var master = MasterVolumeState() + var eqBands: [EQBandState] = [] + var stereoLevel: Double = 0 + var bassLevel: Double = 0 + var isLoading = false + var isSaving = false + + init(service: any DigiRadioService) { + self.service = service + } + + func load() async { + isLoading = true + defer { isLoading = false } + try? await service.refreshAudioProfile() + syncFromDevice() + } + + func syncFromDevice() { + let audio = service.state.audio + mixer = audio.mixer + master = audio.master + eqBands = audio.eq + stereoLevel = Double(audio.enhancements.stereoLevel) + bassLevel = Double(audio.enhancements.bassLevel) + if master.leftDb == 0 && master.rightDb == 0 { + let vol = Double(service.state.tuner.volume) + master = MasterVolumeState(leftDb: vol, rightDb: vol) + } + } + + func scheduleApply() { + applyTask?.cancel() + applyTask = Task { + try? await Task.sleep(for: .milliseconds(350)) + guard !Task.isCancelled else { return } + await applyNow() + } + } + + func applyNow() async { + isSaving = true + defer { isSaving = false } + let profile = AudioProfileDTO( + mixer: mixer, + master: master, + eq: eqBands, + enhancements: EnhancementsState(stereoLevel: Int(stereoLevel), bassLevel: Int(bassLevel)) + ) + try? await service.applyAudioProfile(profile) + try? await service.setVolume(Int((master.leftDb + master.rightDb) / 2)) + syncFromDevice() + } + + func reset() async { + try? await service.resetAudio() + syncFromDevice() + } + + func applyEnhancements() async { + try? await service.setStereoEnhance(level: Int(stereoLevel)) + try? await service.setBassEnhance(level: Int(bassLevel)) + syncFromDevice() + } +} diff --git a/Software/APP/igiRadio/igiRadio/Views/AudioView.swift b/Software/APP/igiRadio/igiRadio/Views/AudioView.swift index d5f21f2..8518bd0 100644 --- a/Software/APP/igiRadio/igiRadio/Views/AudioView.swift +++ b/Software/APP/igiRadio/igiRadio/Views/AudioView.swift @@ -2,85 +2,224 @@ import SwiftUI struct AudioView: View { @Environment(AppEnvironment.self) private var environment - @State private var stereoLevel: Double = 0 - @State private var bassLevel: Double = 0 - @State private var masterVolume: Double = 0 - @State private var eqBands: [EQBandState] = [] + @State private var viewModel: AudioViewModel? var body: some View { - Form { - Section("Volume master") { - IGIVolumeSlider(value: $masterVolume) { value in - Task { try? await environment.digiRadio.setVolume(Int(value)) } - } - } + let vm = viewModel ?? AudioViewModel(service: environment.digiRadio) - Section("Enhancements") { - VStack(alignment: .leading) { - Text("Stereo enhance") - Slider(value: $stereoLevel, in: 0 ... 100, step: 1) { editing in - if !editing { - Task { try? await environment.digiRadio.setStereoEnhance(level: Int(stereoLevel)) } - } - } - } - VStack(alignment: .leading) { - Text("Bass enhance") - Slider(value: $bassLevel, in: 0 ... 100, step: 1) { editing in - if !editing { - Task { try? await environment.digiRadio.setBassEnhance(level: Int(bassLevel)) } - } - } - } - } + ScrollView { + VStack(alignment: .leading, spacing: IGITheme.spacingL) { + header(vm) - Section("Equalizzatore (6 bande)") { - ForEach($eqBands) { $band in - VStack(alignment: .leading) { - Text("\(Int(band.centerHz)) Hz") - .font(.caption) - .foregroundStyle(.secondary) - Slider(value: $band.gainDb, in: -12 ... 12, step: 0.5) { editing in - if !editing { Task { await applyEQ() } } - } - } - } - } + if vm.isLoading { + ProgressView("Caricamento profilo audio…") + .frame(maxWidth: .infinity) + .padding(.vertical, 40) + } else { + IGIMasterVolumeHero( + leftDb: Binding( + get: { vm.master.leftDb }, + set: { vm.master.leftDb = $0; vm.scheduleApply() } + ), + rightDb: Binding( + get: { vm.master.rightDb }, + set: { vm.master.rightDb = $0; vm.scheduleApply() } + ), + onCommit: { vm.scheduleApply() } + ) - Section { - Button("Reset profilo audio", role: .destructive) { - Task { - try? await environment.digiRadio.resetAudio() - syncFromState() + panelPicker(vm) + + switch vm.panel { + case .mixer: + mixerPanel(vm) + case .equalizer: + equalizerPanel(vm) + case .enhance: + enhancePanel(vm) } + + resetButton(vm) } } + .padding(IGITheme.spacingM) } - .navigationTitle("Audio") - .onAppear { - Task { - try? await environment.digiRadio.refreshAudioProfile() - syncFromState() - } - } - .onChange(of: environment.state.audio.enhancements.stereoLevel) { _, _ in syncFromState() } - } - - private func syncFromState() { - let audio = environment.state.audio - stereoLevel = Double(audio.enhancements.stereoLevel) - bassLevel = Double(audio.enhancements.bassLevel) - masterVolume = Double(environment.state.tuner.volume) - eqBands = audio.eq - } - - private func applyEQ() async { - let profile = AudioProfileDTO( - mixer: environment.state.audio.mixer, - master: environment.state.audio.master, - eq: eqBands, - enhancements: EnhancementsState(stereoLevel: Int(stereoLevel), bassLevel: Int(bassLevel)) + .background( + LinearGradient( + colors: [ + IGITheme.screenBackground, + IGITheme.accent.opacity(0.06), + IGITheme.screenBackground + ], + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + .ignoresSafeArea() ) - try? await environment.digiRadio.applyAudioProfile(profile) + .navigationTitle("Audio") + .navigationBarTitleDisplayMode(.large) + .onAppear { + if viewModel == nil { viewModel = vm } + Task { await vm.load() } + } + } + + @ViewBuilder + private func header(_ vm: AudioViewModel) -> some View { + HStack { + VStack(alignment: .leading, spacing: 4) { + Text("ADAU1701") + .font(.subheadline.weight(.semibold)) + .foregroundStyle(IGITheme.accent) + Text("Mixer · EQ · DSP") + .font(.caption) + .foregroundStyle(.secondary) + } + Spacer() + if vm.isSaving { + HStack(spacing: 6) { + ProgressView().controlSize(.small) + Text("Salvataggio") + .font(.caption) + .foregroundStyle(.secondary) + } + } + } + } + + @ViewBuilder + private func panelPicker(_ vm: AudioViewModel) -> some View { + HStack(spacing: IGITheme.spacingS) { + ForEach(AudioPanel.allCases) { panel in + Button { + withAnimation(.snappy) { vm.panel = panel } + } label: { + Label(panel.rawValue, systemImage: panel.icon) + .font(.subheadline.weight(.semibold)) + .frame(maxWidth: .infinity) + .padding(.vertical, 12) + .background( + vm.panel == panel + ? AnyShapeStyle(IGITheme.accent.gradient) + : AnyShapeStyle(Color.primary.opacity(0.06)), + in: RoundedRectangle(cornerRadius: 12, style: .continuous) + ) + .foregroundStyle(vm.panel == panel ? .white : .primary) + } + .buttonStyle(.plain) + } + } + } + + @ViewBuilder + private func mixerPanel(_ vm: AudioViewModel) -> some View { + VStack(spacing: IGITheme.spacingM) { + IGIMixerChannelGroup( + title: "Radio Si4684", + subtitle: "FM / DAB — ingresso tuner", + systemImage: "antenna.radiowaves.left.and.right", + leftDb: Binding( + get: { vm.mixer.si4684LeftDb }, + set: { vm.mixer.si4684LeftDb = $0 } + ), + rightDb: Binding( + get: { vm.mixer.si4684RightDb }, + set: { vm.mixer.si4684RightDb = $0 } + ), + onCommit: { vm.scheduleApply() } + ) + + IGIMixerChannelGroup( + title: "Stream ESP32", + subtitle: "Web radio / phone push", + systemImage: "waveform", + leftDb: Binding( + get: { vm.mixer.esp32LeftDb }, + set: { vm.mixer.esp32LeftDb = $0 } + ), + rightDb: Binding( + get: { vm.mixer.esp32RightDb }, + set: { vm.mixer.esp32RightDb = $0 } + ), + onCommit: { vm.scheduleApply() } + ) + + IGIMixerChannelGroup( + title: "Mix bus", + subtitle: "Uscita mixer ADAU", + systemImage: "speaker.wave.2.fill", + leftDb: Binding( + get: { vm.mixer.mixLeftDb }, + set: { vm.mixer.mixLeftDb = $0 } + ), + rightDb: Binding( + get: { vm.mixer.mixRightDb }, + set: { vm.mixer.mixRightDb = $0 } + ), + onCommit: { vm.scheduleApply() } + ) + } + } + + @ViewBuilder + private func equalizerPanel(_ vm: AudioViewModel) -> some View { + VStack(alignment: .leading, spacing: IGITheme.spacingM) { + Text("Equalizzatore 6 bande") + .font(.headline) + Text("Trascina le barre per regolare il guadagno. Rilascia per applicare al DSP.") + .font(.caption) + .foregroundStyle(.secondary) + + if vm.eqBands.isEmpty { + ProgressView() + .frame(maxWidth: .infinity) + } else { + IGIGraphicEqualizer( + bands: Binding( + get: { vm.eqBands }, + set: { vm.eqBands = $0 } + ), + onCommit: { vm.scheduleApply() } + ) + } + } + } + + @ViewBuilder + private func enhancePanel(_ vm: AudioViewModel) -> some View { + HStack(spacing: IGITheme.spacingL) { + IGIEnhancementDial( + title: "Stereo", + systemImage: "circle.lefthalf.filled", + level: Binding( + get: { vm.stereoLevel }, + set: { vm.stereoLevel = $0 } + ), + onCommit: { Task { await vm.applyEnhancements() } } + ) + IGIEnhancementDial( + title: "Bass", + systemImage: "waveform.path", + level: Binding( + get: { vm.bassLevel }, + set: { vm.bassLevel = $0 } + ), + onCommit: { Task { await vm.applyEnhancements() } } + ) + } + .igiAudioCard() + } + + @ViewBuilder + private func resetButton(_ vm: AudioViewModel) -> some View { + Button(role: .destructive) { + Task { await vm.reset() } + } label: { + Label("Ripristina profilo audio", systemImage: "arrow.counterclockwise") + .frame(maxWidth: .infinity) + .padding(.vertical, 14) + } + .buttonStyle(.bordered) + .padding(.top, IGITheme.spacingS) } } diff --git a/Software/APP/igiRadio/igiRadio/Views/FMRadioView.swift b/Software/APP/igiRadio/igiRadio/Views/FMRadioView.swift index 9c5c8b7..82d8ee7 100644 --- a/Software/APP/igiRadio/igiRadio/Views/FMRadioView.swift +++ b/Software/APP/igiRadio/igiRadio/Views/FMRadioView.swift @@ -10,96 +10,30 @@ struct FMRadioView: View { var body: some View { let fm = environment.state.tuner.fm - Form { - Section { - VStack(spacing: IGITheme.spacingM) { - Text(String(format: "%.2f", frequencyMHz)) - .font(.system(size: 56, weight: .bold, design: .rounded)) - .monospacedDigit() - .accessibilityLabel("Frequenza \(frequencyMHz) megahertz") - Slider(value: $frequencyMHz, in: 64 ... 108, step: 0.05) - HStack { - Button("−") { frequencyMHz = max(64, frequencyMHz - 0.1) } - Spacer() - Button("Sintonizza") { Task { await tune() } } - .buttonStyle(.borderedProminent) - .disabled(isTuning) - Spacer() - Button("+") { frequencyMHz = min(108, frequencyMHz + 0.1) } - } - } - .padding(.vertical, IGITheme.spacingS) - } + ScrollView { + VStack(alignment: .leading, spacing: IGITheme.spacingL) { + IGIFrequencyHero( + frequencyMHz: $frequencyMHz, + stationName: fm?.stationName, + isTuning: isTuning, + onTune: { Task { await tune() } } + ) - if let fm { - Section("Stato") { - LabeledContent("Frequenza", value: String(format: "%.2f MHz", Double(fm.frequencyKhz) / 1000)) - if let rssi = fm.rssiDbuv { LabeledContent("RSSI", value: "\(rssi) dBµV") } - if let snr = fm.snrDb { LabeledContent("SNR", value: "\(snr) dB") } - if let stereo = fm.stereo { - LabeledContent("Stereo", value: stereo ? "Sì" : "Mono") + if let fm { + signalCard(fm) + if let ps = fm.stationName, !ps.isEmpty { + rdsCard(fm, ps: ps) } } - if let ps = fm.stationName, !ps.isEmpty { - Section("RDS") { - LabeledContent("PS", value: ps) - if let rt = fm.radiotext, !rt.isEmpty { - Text(rt).font(.body) - } - } - } - } - Section("Seek") { - Button("Seek su") { Task { try? await environment.digiRadio.seekFM(direction: "up") } } - Button("Seek giù") { Task { try? await environment.digiRadio.seekFM(direction: "down") } } - } - - Section("Scan") { - if isScanning { - HStack { - IGIScanningIndicator().frame(width: 24, height: 24) - Text("Scansione in corso…") - } - } - if let scanError { - Text(scanError).foregroundStyle(.red) - } - Button("Cerca prossima stazione") { - Task { await scanNext() } - } - .disabled(isScanning) - Button("Scansione completa banda FM") { - Task { await scanFull() } - } - .disabled(isScanning) - - if !scanResults.isEmpty { - ForEach(scanResults) { hit in - Button { - Task { try? await environment.digiRadio.tuneFM(frequencyKhz: hit.frequencyKhz) } - } label: { - HStack { - VStack(alignment: .leading) { - Text(hit.stationName ?? "Stazione FM") - .font(.headline) - Text(String(format: "%.2f MHz", Double(hit.frequencyKhz) / 1000)) - .font(.caption) - .foregroundStyle(.secondary) - } - Spacer() - if let rssi = hit.rssiDbuv { - Text("\(rssi) dBµV") - .font(.caption) - .foregroundStyle(.secondary) - } - } - } - } - } + seekSection + scanSection } + .padding(IGITheme.spacingM) } + .background(IGIHeroBackground()) .navigationTitle("FM") + .navigationBarTitleDisplayMode(.large) .onAppear { if let khz = environment.state.tuner.fm?.frequencyKhz { frequencyMHz = Double(khz) / 1000.0 @@ -107,10 +41,128 @@ struct FMRadioView: View { Task { try? await environment.digiRadio.refreshTunerStatus() } } .onChange(of: environment.state.tuner.fm?.frequencyKhz) { _, newValue in - if let khz = newValue { frequencyMHz = Double(khz) / 1000.0 } + if let khz = newValue { + withAnimation(.snappy) { + frequencyMHz = Double(khz) / 1000.0 + } + } } } + @ViewBuilder + private func signalCard(_ fm: FMTunerState) -> some View { + VStack(alignment: .leading, spacing: IGITheme.spacingM) { + Text("Segnale") + .font(.headline) + if let rssi = fm.rssiDbuv { + IGIMetricBar(label: "RSSI", value: "\(rssi) dBµV", level: Double(rssi) / 80.0) + } + if let snr = fm.snrDb { + IGIMetricBar(label: "SNR", value: "\(snr) dB", level: Double(snr) / 40.0) + } + if let stereo = fm.stereo { + HStack { + Text("Stereo") + .font(.caption) + .foregroundStyle(.secondary) + Spacer() + Text(stereo ? "Sì" : "Mono") + .font(.caption.weight(.semibold)) + .foregroundStyle(stereo ? .green : .secondary) + } + } + } + .igiPremiumCard() + } + + @ViewBuilder + private func rdsCard(_ fm: FMTunerState, ps: String) -> some View { + VStack(alignment: .leading, spacing: IGITheme.spacingS) { + HStack { + Image(systemName: "text.bubble.fill") + .foregroundStyle(IGITheme.accent) + Text("RDS") + .font(.headline) + } + Text(ps) + .font(.title3.weight(.semibold)) + if let rt = fm.radiotext, !rt.isEmpty { + Text(rt) + .font(.subheadline) + .foregroundStyle(.secondary) + .italic() + } + } + .frame(maxWidth: .infinity, alignment: .leading) + .igiPremiumCard() + } + + private var seekSection: some View { + HStack(spacing: IGITheme.spacingM) { + Button { + Task { try? await environment.digiRadio.seekFM(direction: "down") } + } label: { + Label("Giù", systemImage: "chevron.down") + .frame(maxWidth: .infinity) + .padding(.vertical, 14) + } + .buttonStyle(.bordered) + + Button { + Task { try? await environment.digiRadio.seekFM(direction: "up") } + } label: { + Label("Su", systemImage: "chevron.up") + .frame(maxWidth: .infinity) + .padding(.vertical, 14) + } + .buttonStyle(.bordered) + } + } + + @ViewBuilder + private var scanSection: some View { + VStack(alignment: .leading, spacing: IGITheme.spacingM) { + Text("Scan") + .font(.headline) + + if isScanning { + HStack(spacing: IGITheme.spacingS) { + IGIScanningIndicator().frame(width: 28, height: 28) + Text("Scansione in corso…") + .foregroundStyle(.secondary) + } + } + + if let scanError { + Text(scanError).font(.caption).foregroundStyle(.red) + } + + HStack(spacing: IGITheme.spacingS) { + Button("Prossima stazione") { Task { await scanNext() } } + .buttonStyle(.borderedProminent) + .disabled(isScanning) + Button("Banda completa") { Task { await scanFull() } } + .buttonStyle(.bordered) + .disabled(isScanning) + } + + if !scanResults.isEmpty { + VStack(spacing: IGITheme.spacingS) { + ForEach(scanResults) { hit in + IGIScanResultRow( + title: hit.stationName ?? "Stazione FM", + frequencyMHz: Double(hit.frequencyKhz) / 1000, + rssi: hit.rssiDbuv + ) { + Task { try? await environment.digiRadio.tuneFM(frequencyKhz: hit.frequencyKhz) } + } + } + } + } + } + .igiPremiumCard() + } + private func tune() async { isTuning = true defer { isTuning = false } diff --git a/Software/APP/igiRadio/igiRadio/Views/HomeView.swift b/Software/APP/igiRadio/igiRadio/Views/HomeView.swift index ed1aaa6..8e6b71c 100644 --- a/Software/APP/igiRadio/igiRadio/Views/HomeView.swift +++ b/Software/APP/igiRadio/igiRadio/Views/HomeView.swift @@ -11,31 +11,23 @@ struct HomeView: View { VStack(alignment: .leading, spacing: IGITheme.spacingL) { header(state: state) nowPlayingCard(state: state) - transportControls + quickLinks + IGITransportCluster( + onPrevious: { Task { await viewModel?.seekFM("down") } }, + onPlay: { Task { await viewModel?.refreshAll() } }, + onNext: { Task { await viewModel?.seekFM("up") } } + ) volumeSection presetsSection(state: state) } .padding(IGITheme.spacingM) } - .background(IGITheme.screenBackground) + .background(IGIHeroBackground()) .navigationTitle("igiRadio") + .navigationBarTitleDisplayMode(.large) .safeAreaInset(edge: .top) { if !state.connection.isConnected { - NavigationLink { - ConnectionView() - } label: { - HStack { - Image(systemName: "wifi.exclamationmark") - Text("DigiRadio non connesso — tocca per collegare") - .font(.subheadline.weight(.medium)) - Spacer() - Image(systemName: "chevron.right") - } - .padding(.horizontal, IGITheme.spacingM) - .padding(.vertical, 10) - .background(.orange.opacity(0.15)) - } - .buttonStyle(.plain) + connectionBanner } } .toolbar { @@ -60,26 +52,46 @@ struct HomeView: View { } } + private var connectionBanner: some View { + NavigationLink { + ConnectionView() + } label: { + HStack { + Image(systemName: "wifi.exclamationmark") + Text("DigiRadio non connesso — tocca per collegare") + .font(.subheadline.weight(.medium)) + Spacer() + Image(systemName: "chevron.right") + } + .padding(.horizontal, IGITheme.spacingM) + .padding(.vertical, 10) + .background(.orange.opacity(0.15)) + } + .buttonStyle(.plain) + } + @ViewBuilder private func header(state: DigiRadioState) -> some View { - HStack { - VStack(alignment: .leading, spacing: 4) { + HStack(alignment: .top) { + VStack(alignment: .leading, spacing: 8) { Text("DigiRadio") - .font(.largeTitle.bold()) + .font(.title.weight(.bold)) HStack(spacing: 8) { IGIStatusDot(isConnected: state.connection.isConnected) Text(state.connection.isConnected ? "Connesso" : "Non connesso") + .font(.subheadline) .foregroundStyle(.secondary) } + IGIBandBadge(band: state.tuner.band, locked: state.tuner.locked) } Spacer() if let rssi = state.tuner.fm?.rssiDbuv { - VStack(alignment: .trailing) { + VStack(alignment: .trailing, spacing: 6) { Text("Segnale") .font(.caption) .foregroundStyle(.secondary) IGISignalBar(level: Double(rssi) / 80.0) - .frame(width: 120) + .frame(width: 100) } } } @@ -87,75 +99,96 @@ struct HomeView: View { @ViewBuilder private func nowPlayingCard(state: DigiRadioState) -> some View { - IGICard { - VStack(spacing: IGITheme.spacingM) { - ZStack { - RoundedRectangle(cornerRadius: 24, style: .continuous) - .fill( - LinearGradient( - colors: [.accentColor.opacity(0.35), .purple.opacity(0.25)], - startPoint: .topLeading, - endPoint: .bottomTrailing - ) + VStack(spacing: IGITheme.spacingM) { + ZStack { + RoundedRectangle(cornerRadius: 28, style: .continuous) + .fill( + LinearGradient( + colors: [ + IGITheme.accent.opacity(0.45), + .purple.opacity(0.35), + IGITheme.accent.opacity(0.25) + ], + startPoint: .topLeading, + endPoint: .bottomTrailing ) - .frame(height: 220) - Image(systemName: state.tuner.band == .fm ? "radio.fill" : "antenna.radiowaves.left.and.right") - .font(.system(size: 64)) - .foregroundStyle(.white.opacity(0.9)) - } + ) + .frame(height: 200) + .overlay { + Circle() + .fill(.white.opacity(0.08)) + .frame(width: 160, height: 160) + .blur(radius: 2) + } - VStack(spacing: 6) { - Text(primaryTitle(state: state)) - .font(.title2.weight(.semibold)) - .multilineTextAlignment(.center) - Text(secondaryLine(state: state)) - .font(.subheadline) - .foregroundStyle(.secondary) - .multilineTextAlignment(.center) - } + Image(systemName: state.tuner.band == .fm ? "radio.fill" : "antenna.radiowaves.left.and.right") + .font(.system(size: 56)) + .foregroundStyle(.white.opacity(0.95)) + .symbolEffect(.pulse, options: .repeating, value: state.tuner.locked) + } + + VStack(spacing: 6) { + Text(primaryTitle(state: state)) + .font(.title2.weight(.bold)) + .multilineTextAlignment(.center) + .contentTransition(.interpolate) + Text(secondaryLine(state: state)) + .font(.subheadline) + .foregroundStyle(.secondary) + .multilineTextAlignment(.center) + .lineLimit(2) + } + } + .igiPremiumCard() + .animation(.snappy, value: state.tuner.fm?.frequencyKhz) + .animation(.snappy, value: state.tuner.fm?.stationName) + } + + private var quickLinks: some View { + HStack(spacing: IGITheme.spacingS) { + NavigationLink { + FMRadioView() + } label: { + quickLinkLabel("FM", icon: "dot.radiowaves.left.and.right") + } + NavigationLink { + DABRadioView() + } label: { + quickLinkLabel("DAB", icon: "antenna.radiowaves.left.and.right") + } + NavigationLink { + AudioView() + } label: { + quickLinkLabel("Audio", icon: "slider.vertical.3") } } } - private var transportControls: some View { - HStack(spacing: IGITheme.spacingL) { - Button { - Task { await viewModel?.seekFM("down") } - } label: { - Image(systemName: "backward.fill") - .font(.title2) - .frame(width: 56, height: 56) - } - .buttonStyle(.bordered) - - Button { - Task { await viewModel?.refreshAll() } - } label: { - Image(systemName: "play.fill") - .font(.largeTitle) - .frame(width: 72, height: 72) - } - .buttonStyle(.borderedProminent) - .clipShape(Circle()) - - Button { - Task { await viewModel?.seekFM("up") } - } label: { - Image(systemName: "forward.fill") - .font(.title2) - .frame(width: 56, height: 56) - } - .buttonStyle(.bordered) - } - .frame(maxWidth: .infinity) + @ViewBuilder + private func quickLinkLabel(_ title: String, icon: String) -> some View { + Label(title, systemImage: icon) + .font(.caption.weight(.semibold)) + .frame(maxWidth: .infinity) + .padding(.vertical, 12) + .background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 12, style: .continuous)) } private var volumeSection: some View { - IGICard { + VStack(alignment: .leading, spacing: IGITheme.spacingS) { + HStack { + Text("Volume") + .font(.headline) + Spacer() + Text("\(Int(volume))") + .font(.title3.weight(.bold).monospacedDigit()) + .foregroundStyle(IGITheme.accent) + .contentTransition(.numericText()) + } IGIVolumeSlider(value: $volume) { value in Task { await viewModel?.setVolume(value) } } } + .igiPremiumCard() } @ViewBuilder @@ -171,20 +204,12 @@ struct HomeView: View { ScrollView(.horizontal, showsIndicators: false) { HStack(spacing: IGITheme.spacingS) { ForEach(Array(state.stations.enumerated()), id: \.offset) { index, station in - Button { + IGIPresetChip( + name: station.name, + subtitle: station.band.rawValue.uppercased() + ) { Task { await viewModel?.tunePreset(at: index) } - } label: { - VStack(alignment: .leading) { - Text(station.name) - .font(.headline) - Text(station.band.rawValue.uppercased()) - .font(.caption) - .foregroundStyle(.secondary) - } - .padding() - .background(IGITheme.cardBackground, in: RoundedRectangle(cornerRadius: 14)) } - .buttonStyle(.plain) } } } @@ -208,7 +233,9 @@ struct HomeView: View { switch state.tuner.band { case .fm: if let khz = state.tuner.fm?.frequencyKhz { - return String(format: "%.2f MHz", Double(khz) / 1000.0) + let freq = String(format: "%.2f MHz", Double(khz) / 1000.0) + if let rt = state.tuner.fm?.radiotext, !rt.isEmpty { return "\(freq) · \(rt)" } + return freq } return state.tuner.fm?.radiotext ?? "FM Radio" case .dab: diff --git a/Software/APP/igiRadio/igiRadio/Views/RootView.swift b/Software/APP/igiRadio/igiRadio/Views/RootView.swift index d11c887..787a8d8 100644 --- a/Software/APP/igiRadio/igiRadio/Views/RootView.swift +++ b/Software/APP/igiRadio/igiRadio/Views/RootView.swift @@ -33,6 +33,9 @@ private struct IPhoneRootView: View { NavigationStack { PresetsView() } .tabItem { Label("Preset", systemImage: "star.fill") } + NavigationStack { AudioView() } + .tabItem { Label("Audio", systemImage: "waveform") } + NavigationStack { SettingsRootView() } .tabItem { Label("Impostazioni", systemImage: "gearshape.fill") } } diff --git a/Software/APP/igiRadio/igiRadio/Views/SettingsViews.swift b/Software/APP/igiRadio/igiRadio/Views/SettingsViews.swift index f841ed9..00de520 100644 --- a/Software/APP/igiRadio/igiRadio/Views/SettingsViews.swift +++ b/Software/APP/igiRadio/igiRadio/Views/SettingsViews.swift @@ -15,7 +15,11 @@ struct SettingsRootView: View { NavigationLink("Preset") { PresetsView() } } Section("Audio") { - NavigationLink("Profilo audio") { AudioView() } + NavigationLink { + AudioView() + } label: { + Label("Mixer & Equalizzatore", systemImage: "slider.vertical.3") + } NavigationLink("Bluetooth speaker") { BluetoothView() } } } From a1aaefd47a0097fc79b2ad531c7c33b4b2f592df Mon Sep 17 00:00:00 2001 From: Michele Bigi Date: Tue, 18 Aug 2026 16:15:21 +0200 Subject: [PATCH 03/12] Add multi-source streaming to igiRadio (web radio, URL, local files). Supports POST /api/streaming for HTTP MP3 stations and phone PCM push for on-device audio files, with a dedicated Stream tab and preset for Radio Monte Carlo. Co-authored-by: Cursor --- .../igiRadio/Mock/MockDigiRadioService.swift | 5 + .../Services/Audio/LocalAudioDecoder.swift | 90 +++++++ .../Audio/PhonePCMStreamService.swift | 152 +++++++++++ .../Services/Device/DigiRadioService.swift | 1 + .../Device/RealDigiRadioService.swift | 5 + .../HTTPDigiRadioClient.swift | 4 + .../ViewModels/StreamingViewModel.swift | 184 ++++++++++++++ .../igiRadio/igiRadio/Views/HomeView.swift | 44 ++++ .../igiRadio/igiRadio/Views/RootView.swift | 8 +- .../igiRadio/Views/SettingsViews.swift | 1 + .../igiRadio/Views/StreamingView.swift | 235 ++++++++++++++++++ 11 files changed, 728 insertions(+), 1 deletion(-) create mode 100644 Software/APP/igiRadio/igiRadio/Services/Audio/LocalAudioDecoder.swift create mode 100644 Software/APP/igiRadio/igiRadio/Services/Audio/PhonePCMStreamService.swift create mode 100644 Software/APP/igiRadio/igiRadio/ViewModels/StreamingViewModel.swift create mode 100644 Software/APP/igiRadio/igiRadio/Views/StreamingView.swift diff --git a/Software/APP/igiRadio/igiRadio/Mock/MockDigiRadioService.swift b/Software/APP/igiRadio/igiRadio/Mock/MockDigiRadioService.swift index e3ae4be..c172e3a 100644 --- a/Software/APP/igiRadio/igiRadio/Mock/MockDigiRadioService.swift +++ b/Software/APP/igiRadio/igiRadio/Mock/MockDigiRadioService.swift @@ -45,6 +45,11 @@ final class MockDigiRadioService: DigiRadioService { try await delay() } + func setStreaming(enabled: Bool, url: String) async throws { + try await delay() + state.streaming = StreamingState(enabled: enabled, url: url) + } + func tuneFM(frequencyKhz: Int) async throws { try await delay() state.tuner.band = .fm diff --git a/Software/APP/igiRadio/igiRadio/Services/Audio/LocalAudioDecoder.swift b/Software/APP/igiRadio/igiRadio/Services/Audio/LocalAudioDecoder.swift new file mode 100644 index 0000000..0da750c --- /dev/null +++ b/Software/APP/igiRadio/igiRadio/Services/Audio/LocalAudioDecoder.swift @@ -0,0 +1,90 @@ +import AVFoundation +import Foundation + +/// Decodes local audio files to interleaved PCM s16le stereo @ 48 kHz for PUT /api/stream/phone. +enum LocalAudioDecoder { + static let outputSampleRate: Double = 48_000 + static let chunkFrames: AVAudioFrameCount = 2048 + + static func pcmChunks(from fileURL: URL) -> AsyncThrowingStream { + AsyncThrowingStream { continuation in + Task { + do { + let accessed = fileURL.startAccessingSecurityScopedResource() + defer { if accessed { fileURL.stopAccessingSecurityScopedResource() } } + + let inputFile = try AVAudioFile(forReading: fileURL) + guard let outputFormat = AVAudioFormat( + commonFormat: .pcmFormatInt16, + sampleRate: outputSampleRate, + channels: 2, + interleaved: true + ) else { + throw PhoneStreamError.formatUnsupported + } + + guard let converter = AVAudioConverter(from: inputFile.processingFormat, to: outputFormat) else { + throw PhoneStreamError.formatUnsupported + } + + let inputBuffer = AVAudioPCMBuffer( + pcmFormat: inputFile.processingFormat, + frameCapacity: chunkFrames + )! + let outputBuffer = AVAudioPCMBuffer( + pcmFormat: outputFormat, + frameCapacity: chunkFrames + )! + + while inputFile.framePosition < inputFile.length { + try Task.checkCancellation() + try inputFile.read(into: inputBuffer, frameCount: chunkFrames) + if inputBuffer.frameLength == 0 { break } + + var inputProvided = false + var convertError: NSError? + let status = converter.convert(to: outputBuffer, error: &convertError) { _, outStatus in + if inputProvided { + outStatus.pointee = .noDataNow + return nil + } + inputProvided = true + outStatus.pointee = .haveData + return inputBuffer + } + + if status == .error { + throw convertError ?? PhoneStreamError.decodeFailed + } + if outputBuffer.frameLength == 0 { continue } + + let byteCount = Int(outputBuffer.frameLength) * Int(outputFormat.streamDescription.pointee.mBytesPerFrame) + guard let channelData = outputBuffer.int16ChannelData else { continue } + continuation.yield(Data(bytes: channelData[0], count: byteCount)) + } + continuation.finish() + } catch { + continuation.finish(throwing: error) + } + } + } + } +} + +enum PhoneStreamError: LocalizedError { + case notConnected + case formatUnsupported + case decodeFailed + case deviceRejected(Int, String?) + case transport(Error) + + var errorDescription: String? { + switch self { + case .notConnected: return "DigiRadio non connesso." + case .formatUnsupported: return "Formato audio non supportato." + case .decodeFailed: return "Impossibile decodificare il file." + case let .deviceRejected(code, body): return "Device HTTP \(code): \(body ?? "")" + case let .transport(error): return error.localizedDescription + } + } +} diff --git a/Software/APP/igiRadio/igiRadio/Services/Audio/PhonePCMStreamService.swift b/Software/APP/igiRadio/igiRadio/Services/Audio/PhonePCMStreamService.swift new file mode 100644 index 0000000..3e83bcb --- /dev/null +++ b/Software/APP/igiRadio/igiRadio/Services/Audio/PhonePCMStreamService.swift @@ -0,0 +1,152 @@ +import Foundation +import Network +import OSLog + +/// Streams PCM to DigiRadio via documented PUT /api/stream/phone (chunked HTTP/1.1). +actor PhonePCMStreamService { + static let shared = PhonePCMStreamService() + + private(set) var isStreaming = false + private(set) var statusMessage = "" + private var streamTask: Task? + private let logger = Logger(subsystem: "com.digiradio.igiRadio", category: "PhoneStream") + + func start(fileURL: URL, connectionHost: String) { + stop() + let host = Self.parseHost(connectionHost) + guard !host.isEmpty else { + statusMessage = "Host non valido" + return + } + + isStreaming = true + statusMessage = "Avvio stream…" + streamTask = Task { + do { + try await stream(fileURL: fileURL, host: host) + statusMessage = "Stream completato" + isStreaming = false + } catch is CancellationError { + statusMessage = "Stream interrotto" + isStreaming = false + } catch { + statusMessage = error.localizedDescription + isStreaming = false + logger.error("Phone stream failed: \(error.localizedDescription, privacy: .public)") + } + } + } + + func stop() { + streamTask?.cancel() + streamTask = nil + isStreaming = false + } + + private func stream(fileURL: URL, host: String) async throws { + let chunks = LocalAudioDecoder.pcmChunks(from: fileURL) + try await sendChunkedPUT(host: host, chunks: chunks) + } + + private func sendChunkedPUT(host: String, chunks: AsyncThrowingStream) async throws { + let connection = NWConnection(host: NWEndpoint.Host(host), port: 80, using: .tcp) + connection.start(queue: .global(qos: .userInitiated)) + try await waitUntilReady(connection) + + let header = """ + PUT /api/stream/phone HTTP/1.1\r + Host: \(host)\r + Transfer-Encoding: chunked\r + Connection: close\r + Content-Type: application/octet-stream\r + \r + """ + try await send(connection, Data(header.utf8)) + + for try await chunk in chunks { + try Task.checkCancellation() + try await sendChunk(connection, chunk) + statusMessage = "Streaming…" + } + + try await send(connection, Data("0\r\n\r\n".utf8)) + _ = try await readResponse(connection) + connection.cancel() + } + + private func waitUntilReady(_ connection: NWConnection) async throws { + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + var finished = false + connection.stateUpdateHandler = { state in + guard !finished else { return } + switch state { + case .ready: + finished = true + continuation.resume() + case let .failed(error): + finished = true + continuation.resume(throwing: PhoneStreamError.transport(error)) + default: + break + } + } + } + } + + private func send(_ connection: NWConnection, _ data: Data) async throws { + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + connection.send(content: data, completion: .contentProcessed { error in + if let error { + continuation.resume(throwing: PhoneStreamError.transport(error)) + } else { + continuation.resume() + } + }) + } + } + + private func sendChunk(_ connection: NWConnection, _ chunk: Data) async throws { + var payload = Data() + payload.append(contentsOf: "\(String(chunk.count, radix: 16, uppercase: false))\r\n".utf8) + payload.append(chunk) + payload.append(contentsOf: "\r\n".utf8) + try await send(connection, payload) + } + + private func readResponse(_ connection: NWConnection) async throws -> Int { + try await withCheckedThrowingContinuation { continuation in + connection.receive(minimumIncompleteLength: 1, maximumLength: 8192) { data, _, _, error in + if let error { + continuation.resume(throwing: PhoneStreamError.transport(error)) + return + } + guard let data, let text = String(data: data, encoding: .utf8) else { + continuation.resume(returning: 200) + return + } + let statusLine = text.split(separator: "\r\n").first.map(String.init) ?? "" + if statusLine.contains("409") { + continuation.resume(throwing: PhoneStreamError.deviceRejected(409, "Stream occupato (web radio attiva?)")) + } else if statusLine.contains("503") { + continuation.resume(throwing: PhoneStreamError.deviceRejected(503, "Sink I2S non disponibile")) + } else if statusLine.contains("200") { + continuation.resume(returning: 200) + } else if let code = Int(statusLine.split(separator: " ").dropFirst().first ?? "") { + continuation.resume(throwing: PhoneStreamError.deviceRejected(code, statusLine)) + } else { + continuation.resume(returning: 200) + } + } + } + } + + private static func parseHost(_ raw: String) -> String { + var h = raw.trimmingCharacters(in: .whitespacesAndNewlines) + h = h.replacingOccurrences(of: "http://", with: "") + h = h.replacingOccurrences(of: "https://", with: "") + if let slash = h.firstIndex(of: "/") { + h = String(h[.. StreamingState { + try await post("/api/streaming", body: config) + } + // MARK: - HTTP helpers private func url(for path: String) throws -> URL { diff --git a/Software/APP/igiRadio/igiRadio/ViewModels/StreamingViewModel.swift b/Software/APP/igiRadio/igiRadio/ViewModels/StreamingViewModel.swift new file mode 100644 index 0000000..65d3b2a --- /dev/null +++ b/Software/APP/igiRadio/igiRadio/ViewModels/StreamingViewModel.swift @@ -0,0 +1,184 @@ +import Foundation +import Observation +import UniformTypeIdentifiers + +enum StreamSource: String, CaseIterable, Identifiable { + case webRadio = "Radio web" + case urlContent = "URL contenuto" + case localFile = "File iPhone" + + var id: String { rawValue } + + var subtitle: String { + switch self { + case .webRadio: "MP3 HTTP sul DigiRadio (decoder ESP32)" + case .urlContent: "Podcast, playlist o stream personalizzato" + case .localFile: "File audio decodificato e inviato dal telefono" + } + } + + var icon: String { + switch self { + case .webRadio: "antenna.radiowaves.left.and.right" + case .urlContent: "link" + case .localFile: "music.note.list" + } + } +} + +struct StreamPreset: Identifiable { + var id: String { name } + var name: String + var url: String +} + +enum StreamPresets { + /// Default from firmware WebRadioConfig.hpp (documented example). + static let catalog: [StreamPreset] = [ + StreamPreset(name: "Radio Monte Carlo", url: "http://edge.radiomontecarlo.net/RMC.mp3"), + StreamPreset(name: "Esempio MP3", url: "http://stream.example.com/radio.mp3") + ] +} + +@Observable +final class StreamingViewModel { + private let service: any DigiRadioService + + var source: StreamSource = .webRadio + var enabled = false + var url = StreamPresets.catalog[0].url + var isLoading = false + var isSaving = false + var errorMessage: String? + + var selectedFileName = "" + var selectedFileURL: URL? + var phoneStreamActive = false + var phoneStreamStatus = "" + + private var statusPollTask: Task? + + init(service: any DigiRadioService) { + self.service = service + } + + deinit { + statusPollTask?.cancel() + } + + var connectionHost: String { + service.state.connection.host + } + + var isConnected: Bool { + service.state.connection.isConnected + } + + func load() async { + isLoading = true + errorMessage = nil + defer { isLoading = false } + do { + try await service.refreshStreaming() + syncFromDevice() + } catch { + errorMessage = error.localizedDescription + } + startStatusPolling() + } + + func selectPreset(_ preset: StreamPreset) { + url = preset.url + source = .webRadio + } + + func normalizedHTTPURL() -> String? { + var trimmed = url.trimmingCharacters(in: .whitespacesAndNewlines) + if trimmed.isEmpty { return nil } + if !trimmed.hasPrefix("http://") { + if trimmed.hasPrefix("https://") { + errorMessage = "Il firmware accetta solo URL http:// (non https)." + return nil + } + trimmed = "http://\(trimmed)" + } + return trimmed + } + + func playWebStream() async { + guard let normalized = normalizedHTTPURL() else { return } + url = normalized + await apply(enabled: true) + } + + func stopWebStream() async { + await apply(enabled: false) + } + + func apply(enabled: Bool? = nil) async { + guard let normalized = normalizedHTTPURL() else { return } + isSaving = true + errorMessage = nil + defer { isSaving = false } + let targetEnabled = enabled ?? self.enabled + do { + try await service.setStreaming(enabled: targetEnabled, url: normalized) + syncFromDevice() + } catch { + errorMessage = error.localizedDescription + } + } + + func pickFile(_ url: URL) { + selectedFileURL = url + selectedFileName = url.lastPathComponent + source = .localFile + errorMessage = nil + } + + func playLocalFile() async { + guard isConnected, let fileURL = selectedFileURL else { + errorMessage = "Seleziona un file e connetti DigiRadio." + return + } + errorMessage = nil + await stopWebStreamSilently() + await PhonePCMStreamService.shared.start(fileURL: fileURL, connectionHost: connectionHost) + } + + func stopLocalFile() async { + await PhonePCMStreamService.shared.stop() + phoneStreamActive = false + phoneStreamStatus = "Fermo" + } + + func stopPolling() { + statusPollTask?.cancel() + statusPollTask = nil + } + + private func stopWebStreamSilently() async { + try? await service.setStreaming(enabled: false, url: url) + syncFromDevice() + } + + private func syncFromDevice() { + enabled = service.state.streaming.enabled + if !service.state.streaming.url.isEmpty { + url = service.state.streaming.url + } + } + + private func startStatusPolling() { + statusPollTask?.cancel() + statusPollTask = Task { @MainActor in + while !Task.isCancelled { + let streamer = PhonePCMStreamService.shared + phoneStreamActive = await streamer.isStreaming + let status = await streamer.statusMessage + if !status.isEmpty { phoneStreamStatus = status } + try? await Task.sleep(for: .milliseconds(500)) + } + } + } +} diff --git a/Software/APP/igiRadio/igiRadio/Views/HomeView.swift b/Software/APP/igiRadio/igiRadio/Views/HomeView.swift index 8e6b71c..ff11ec1 100644 --- a/Software/APP/igiRadio/igiRadio/Views/HomeView.swift +++ b/Software/APP/igiRadio/igiRadio/Views/HomeView.swift @@ -11,6 +11,7 @@ struct HomeView: View { VStack(alignment: .leading, spacing: IGITheme.spacingL) { header(state: state) nowPlayingCard(state: state) + streamCard(state: state) quickLinks IGITransportCluster( onPrevious: { Task { await viewModel?.seekFM("down") } }, @@ -45,6 +46,7 @@ struct HomeView: View { } viewModel?.onAppear() volume = Double(state.tuner.volume) + Task { try? await environment.digiRadio.refreshStreaming() } } .onDisappear { viewModel?.onDisappear() } .onChange(of: environment.state.tuner.volume) { _, newValue in @@ -144,6 +146,43 @@ struct HomeView: View { .animation(.snappy, value: state.tuner.fm?.stationName) } + @ViewBuilder + private func streamCard(state: DigiRadioState) -> some View { + NavigationLink { + StreamingView() + } label: { + HStack(spacing: IGITheme.spacingM) { + ZStack { + RoundedRectangle(cornerRadius: 14, style: .continuous) + .fill(state.streaming.enabled ? IGITheme.accent.opacity(0.2) : Color.secondary.opacity(0.12)) + .frame(width: 52, height: 52) + Image(systemName: state.streaming.enabled ? "dot.radiowaves.forward" : "dot.radiowaves.forward.slash") + .font(.title2) + .foregroundStyle(state.streaming.enabled ? IGITheme.accent : .secondary) + } + VStack(alignment: .leading, spacing: 4) { + Text("Web radio stream") + .font(.headline) + if state.streaming.enabled, !state.streaming.url.isEmpty { + Text(state.streaming.url) + .font(.caption) + .foregroundStyle(.secondary) + .lineLimit(1) + } else { + Text("Nessuno stream attivo") + .font(.caption) + .foregroundStyle(.secondary) + } + } + Spacer() + Image(systemName: "chevron.right") + .foregroundStyle(.tertiary) + } + .igiPremiumCard() + } + .buttonStyle(.plain) + } + private var quickLinks: some View { HStack(spacing: IGITheme.spacingS) { NavigationLink { @@ -161,6 +200,11 @@ struct HomeView: View { } label: { quickLinkLabel("Audio", icon: "slider.vertical.3") } + NavigationLink { + StreamingView() + } label: { + quickLinkLabel("Stream", icon: "dot.radiowaves.forward") + } } } diff --git a/Software/APP/igiRadio/igiRadio/Views/RootView.swift b/Software/APP/igiRadio/igiRadio/Views/RootView.swift index 787a8d8..f1d3a08 100644 --- a/Software/APP/igiRadio/igiRadio/Views/RootView.swift +++ b/Software/APP/igiRadio/igiRadio/Views/RootView.swift @@ -30,6 +30,9 @@ private struct IPhoneRootView: View { NavigationStack { DABRadioView() } .tabItem { Label("DAB", systemImage: "antenna.radiowaves.left.and.right") } + NavigationStack { StreamingView() } + .tabItem { Label("Stream", systemImage: "dot.radiowaves.forward") } + NavigationStack { PresetsView() } .tabItem { Label("Preset", systemImage: "star.fill") } @@ -58,6 +61,7 @@ private struct IPadRootView: View { case .home: HomeView() case .fm: FMRadioView() case .dab: DABRadioView() + case .stream: StreamingView() case .bluetooth: BluetoothView() case .audio: AudioView() case .presets: PresetsView() @@ -68,7 +72,7 @@ private struct IPadRootView: View { } enum SidebarItem: String, CaseIterable, Identifiable { - case home, fm, dab, bluetooth, audio, presets, settings + case home, fm, dab, stream, bluetooth, audio, presets, settings var id: String { rawValue } @@ -77,6 +81,7 @@ enum SidebarItem: String, CaseIterable, Identifiable { case .home: "Home" case .fm: "FM" case .dab: "DAB" + case .stream: "Stream" case .bluetooth: "Bluetooth" case .audio: "Audio" case .presets: "Preset" @@ -89,6 +94,7 @@ enum SidebarItem: String, CaseIterable, Identifiable { case .home: "house.fill" case .fm: "dot.radiowaves.left.and.right" case .dab: "antenna.radiowaves.left.and.right" + case .stream: "dot.radiowaves.forward" case .bluetooth: "dot.radiowaves.left.and.right" case .audio: "waveform" case .presets: "star.fill" diff --git a/Software/APP/igiRadio/igiRadio/Views/SettingsViews.swift b/Software/APP/igiRadio/igiRadio/Views/SettingsViews.swift index 00de520..69b853c 100644 --- a/Software/APP/igiRadio/igiRadio/Views/SettingsViews.swift +++ b/Software/APP/igiRadio/igiRadio/Views/SettingsViews.swift @@ -12,6 +12,7 @@ struct SettingsRootView: View { Section("Radio") { NavigationLink("FM") { FMRadioView() } NavigationLink("DAB") { DABRadioView() } + NavigationLink("Web radio stream") { StreamingView() } NavigationLink("Preset") { PresetsView() } } Section("Audio") { diff --git a/Software/APP/igiRadio/igiRadio/Views/StreamingView.swift b/Software/APP/igiRadio/igiRadio/Views/StreamingView.swift new file mode 100644 index 0000000..3721b59 --- /dev/null +++ b/Software/APP/igiRadio/igiRadio/Views/StreamingView.swift @@ -0,0 +1,235 @@ +import SwiftUI +import UniformTypeIdentifiers + +struct StreamingView: View { + @Environment(AppEnvironment.self) private var environment + @State private var viewModel: StreamingViewModel? + @State private var showFilePicker = false + + var body: some View { + let vm = viewModel ?? StreamingViewModel(service: environment.digiRadio) + + ScrollView { + VStack(alignment: .leading, spacing: IGITheme.spacingL) { + sourcePicker(vm) + statusHero(vm) + + switch vm.source { + case .webRadio: + webRadioSection(vm) + case .urlContent: + urlContentSection(vm) + case .localFile: + localFileSection(vm) + } + + if let error = vm.errorMessage { + Text(error) + .font(.caption) + .foregroundStyle(.red) + .igiPremiumCard() + } + } + .padding(IGITheme.spacingM) + } + .background(IGIHeroBackground()) + .navigationTitle("Stream") + .navigationBarTitleDisplayMode(.large) + .fileImporter( + isPresented: $showFilePicker, + allowedContentTypes: [.audio, .mp3, .mpeg4Audio, .wav, .aiff], + allowsMultipleSelection: false + ) { result in + if case let .success(urls) = result, let url = urls.first { + vm.pickFile(url) + } + } + .onAppear { + if viewModel == nil { viewModel = vm } + Task { await vm.load() } + } + .onDisappear { + viewModel?.stopPolling() + } + } + + @ViewBuilder + private func sourcePicker(_ vm: StreamingViewModel) -> some View { + VStack(alignment: .leading, spacing: IGITheme.spacingS) { + Text("Sorgente") + .font(.headline) + ForEach(StreamSource.allCases) { item in + Button { + withAnimation(.snappy) { vm.source = item } + } label: { + HStack(spacing: IGITheme.spacingM) { + Image(systemName: item.icon) + .frame(width: 28) + .foregroundStyle(vm.source == item ? .white : IGITheme.accent) + VStack(alignment: .leading, spacing: 2) { + Text(item.rawValue).font(.subheadline.weight(.semibold)) + Text(item.subtitle).font(.caption2).foregroundStyle(vm.source == item ? .white.opacity(0.85) : .secondary) + } + Spacer() + if vm.source == item { + Image(systemName: "checkmark.circle.fill") + } + } + .padding(IGITheme.spacingM) + .background( + vm.source == item + ? AnyShapeStyle(IGITheme.accent.gradient) + : AnyShapeStyle(Color.primary.opacity(0.05)), + in: RoundedRectangle(cornerRadius: 14, style: .continuous) + ) + .foregroundStyle(vm.source == item ? .white : .primary) + } + .buttonStyle(.plain) + } + } + .igiPremiumCard() + } + + @ViewBuilder + private func statusHero(_ vm: StreamingViewModel) -> some View { + let active = vm.source == .localFile ? vm.phoneStreamActive : vm.enabled + VStack(spacing: IGITheme.spacingM) { + Image(systemName: active ? "dot.radiowaves.forward" : "pause.circle") + .font(.system(size: 48)) + .foregroundStyle(active ? IGITheme.accent : .secondary) + .symbolEffect(.pulse, options: .repeating, value: active) + + Text(active ? "In riproduzione" : "Fermo") + .font(.title3.weight(.bold)) + Text(statusSubtitle(vm)) + .font(.caption) + .foregroundStyle(.secondary) + .multilineTextAlignment(.center) + } + .frame(maxWidth: .infinity) + .igiPremiumCard() + } + + private func statusSubtitle(_ vm: StreamingViewModel) -> String { + switch vm.source { + case .webRadio, .urlContent: + return vm.enabled ? vm.url : "Nessuno stream web attivo" + case .localFile: + return vm.phoneStreamStatus.isEmpty ? "Nessun file in invio" : vm.phoneStreamStatus + } + } + + @ViewBuilder + private func webRadioSection(_ vm: StreamingViewModel) -> some View { + VStack(alignment: .leading, spacing: IGITheme.spacingM) { + Text("Stazioni") + .font(.headline) + ForEach(StreamPresets.catalog) { preset in + Button { + vm.selectPreset(preset) + } label: { + HStack { + VStack(alignment: .leading) { + Text(preset.name).font(.headline) + Text(preset.url).font(.caption2).foregroundStyle(.secondary).lineLimit(1) + } + Spacer() + Image(systemName: "play.circle.fill").font(.title2) + } + } + .buttonStyle(.plain) + if preset.id != StreamPresets.catalog.last?.id { + Divider() + } + } + } + .igiPremiumCard() + + streamURLControls(vm, playLabel: "Play radio") + } + + @ViewBuilder + private func urlContentSection(_ vm: StreamingViewModel) -> some View { + VStack(alignment: .leading, spacing: IGITheme.spacingS) { + Text("Contenuto via URL") + .font(.headline) + Text("Il DigiRadio scarica e decodifica MP3 HTTP sul device. L'URL deve iniziare con http:// (max 200 caratteri).") + .font(.caption) + .foregroundStyle(.secondary) + } + .igiPremiumCard() + + streamURLControls(vm, playLabel: "Play contenuto") + } + + @ViewBuilder + private func streamURLControls(_ vm: StreamingViewModel, playLabel: String) -> some View { + VStack(alignment: .leading, spacing: IGITheme.spacingM) { + TextField("http://…", text: Binding(get: { vm.url }, set: { vm.url = $0 })) + .textInputAutocapitalization(.never) + .autocorrectionDisabled() + .keyboardType(.URL) + .padding() + .background(Color.primary.opacity(0.05), in: RoundedRectangle(cornerRadius: 12)) + + HStack(spacing: IGITheme.spacingM) { + Button { Task { await vm.playWebStream() } } label: { + Label(playLabel, systemImage: "play.fill") + .frame(maxWidth: .infinity) + .padding(.vertical, 14) + } + .buttonStyle(.borderedProminent) + .disabled(vm.isSaving || !vm.isConnected) + + Button { Task { await vm.stopWebStream() } } label: { + Label("Stop", systemImage: "stop.fill") + .frame(maxWidth: .infinity) + .padding(.vertical, 14) + } + .buttonStyle(.bordered) + .disabled(vm.isSaving) + } + } + .igiPremiumCard() + } + + @ViewBuilder + private func localFileSection(_ vm: StreamingViewModel) -> some View { + VStack(alignment: .leading, spacing: IGITheme.spacingM) { + Text("File sul telefono") + .font(.headline) + Text("L'iPhone decodifica il file e invia PCM stereo 48 kHz a PUT /api/stream/phone. Ferma prima eventuali stream web sul device.") + .font(.caption) + .foregroundStyle(.secondary) + + Button { + showFilePicker = true + } label: { + Label(vm.selectedFileName.isEmpty ? "Scegli file audio" : vm.selectedFileName, systemImage: "folder") + .frame(maxWidth: .infinity, alignment: .leading) + .padding() + .background(Color.primary.opacity(0.05), in: RoundedRectangle(cornerRadius: 12)) + } + .buttonStyle(.plain) + + HStack(spacing: IGITheme.spacingM) { + Button { Task { await vm.playLocalFile() } } label: { + Label("Invia a DigiRadio", systemImage: "arrow.up.circle.fill") + .frame(maxWidth: .infinity) + .padding(.vertical, 14) + } + .buttonStyle(.borderedProminent) + .disabled(!vm.isConnected || vm.selectedFileURL == nil || vm.phoneStreamActive) + + Button { Task { await vm.stopLocalFile() } } label: { + Label("Stop", systemImage: "stop.fill") + .frame(maxWidth: .infinity) + .padding(.vertical, 14) + } + .buttonStyle(.bordered) + .disabled(!vm.phoneStreamActive) + } + } + .igiPremiumCard() + } +} From 9ad2e42fe2b24ad23ad2470557206350bbc45910 Mon Sep 17 00:00:00 2001 From: Michele Bigi Date: Tue, 18 Aug 2026 17:06:59 +0200 Subject: [PATCH 04/12] Add audio profile presets tab with user-saved profiles and EQ editor. Replace separate Audio/EQ tabs with a unified profile picker (built-in + local presets), keeping mixer access and applying full profiles to the device. Co-authored-by: Cursor --- .../igiRadio/Components/AudioComponents.swift | 2 - .../Models/AudioProfileTemplate.swift | 75 +++++++ .../igiRadio/Models/DigiRadioState.swift | 4 +- .../Services/Audio/AudioProfileStore.swift | 20 ++ .../AudioProfileEditorViewModel.swift | 51 +++++ .../ViewModels/AudioProfilesViewModel.swift | 94 +++++++++ .../Views/AudioProfileEditorView.swift | 118 +++++++++++ .../igiRadio/Views/AudioProfilesView.swift | 190 ++++++++++++++++++ .../igiRadio/igiRadio/Views/AudioView.swift | 26 --- .../igiRadio/igiRadio/Views/HomeView.swift | 4 +- .../igiRadio/igiRadio/Views/RootView.swift | 12 +- .../igiRadio/Views/SettingsViews.swift | 7 +- 12 files changed, 564 insertions(+), 39 deletions(-) create mode 100644 Software/APP/igiRadio/igiRadio/Models/AudioProfileTemplate.swift create mode 100644 Software/APP/igiRadio/igiRadio/Services/Audio/AudioProfileStore.swift create mode 100644 Software/APP/igiRadio/igiRadio/ViewModels/AudioProfileEditorViewModel.swift create mode 100644 Software/APP/igiRadio/igiRadio/ViewModels/AudioProfilesViewModel.swift create mode 100644 Software/APP/igiRadio/igiRadio/Views/AudioProfileEditorView.swift create mode 100644 Software/APP/igiRadio/igiRadio/Views/AudioProfilesView.swift diff --git a/Software/APP/igiRadio/igiRadio/Components/AudioComponents.swift b/Software/APP/igiRadio/igiRadio/Components/AudioComponents.swift index b0954ea..1f3641d 100644 --- a/Software/APP/igiRadio/igiRadio/Components/AudioComponents.swift +++ b/Software/APP/igiRadio/igiRadio/Components/AudioComponents.swift @@ -420,7 +420,6 @@ extension View { enum AudioPanel: String, CaseIterable, Identifiable { case mixer = "Mixer" - case equalizer = "EQ" case enhance = "FX" var id: String { rawValue } @@ -428,7 +427,6 @@ enum AudioPanel: String, CaseIterable, Identifiable { var icon: String { switch self { case .mixer: "slider.vertical.3" - case .equalizer: "waveform.path.ecg" case .enhance: "sparkles" } } diff --git a/Software/APP/igiRadio/igiRadio/Models/AudioProfileTemplate.swift b/Software/APP/igiRadio/igiRadio/Models/AudioProfileTemplate.swift new file mode 100644 index 0000000..2725f74 --- /dev/null +++ b/Software/APP/igiRadio/igiRadio/Models/AudioProfileTemplate.swift @@ -0,0 +1,75 @@ +import Foundation + +/// Audio profile: EQ + enhancements (stored on device via PUT /api/audio/profile). +struct AudioProfileTemplate: Identifiable, Codable, Equatable, Hashable { + var id: String + var name: String + var subtitle: String + var systemImage: String + var isBuiltIn: Bool + var eq: [EQBandState] + var enhancements: EnhancementsState + + static let bandCenters: [Double] = [40, 120, 400, 1000, 3000, 10_000] + + static func eqBands(gains: [Double]) -> [EQBandState] { + zip(bandCenters, gains).enumerated().map { index, pair in + EQBandState(index: index, gainDb: pair.1, centerHz: pair.0, q: 1.414) + } + } + + func profileDTO(mixer: MixerState, master: MasterVolumeState) -> AudioProfileDTO { + AudioProfileDTO(mixer: mixer, master: master, eq: eq, enhancements: enhancements) + } + + func duplicatedAsUser(named name: String) -> AudioProfileTemplate { + AudioProfileTemplate( + id: UUID().uuidString, + name: name, + subtitle: "Profilo personalizzato", + systemImage: "slider.horizontal.3", + isBuiltIn: false, + eq: eq, + enhancements: enhancements + ) + } +} + +enum AudioProfileLibrary { + static let builtIn: [AudioProfileTemplate] = [ + template(id: "builtin.flat", name: "Piatto", subtitle: "Risposta neutra", icon: "minus", gains: [0, 0, 0, 0, 0, 0], stereo: 0, bass: 0), + template(id: "builtin.vocal", name: "Voci", subtitle: "Parlato e podcast", icon: "person.wave.2", gains: [-2, -1, 3, 4, 2, 0], stereo: 15, bass: 5), + template(id: "builtin.bass", name: "Bassi", subtitle: "Più corpo e profondità", icon: "waveform.path", gains: [6, 4, 1, 0, -1, -2], stereo: 10, bass: 35), + template(id: "builtin.treble", name: "Alti", subtitle: "Dettaglio e aria", icon: "sparkles", gains: [-2, -1, 0, 2, 4, 5], stereo: 25, bass: 0), + template(id: "builtin.rock", name: "Rock", subtitle: "Energia V-shape", icon: "guitars", gains: [5, 3, -1, 1, 4, 5], stereo: 30, bass: 25), + template(id: "builtin.jazz", name: "Jazz", subtitle: "Caldo e naturale", icon: "music.quarternote.3", gains: [3, 2, 1, 2, 1, 0], stereo: 20, bass: 15), + template(id: "builtin.classical", name: "Classica", subtitle: "Equilibrio dinamico", icon: "hifispeaker.2", gains: [2, 1, 0, 0, 2, 3], stereo: 15, bass: 5), + template(id: "builtin.lounge", name: "Lounge", subtitle: "Ascolto rilassato", icon: "moon.stars", gains: [2, 1, 0, -1, -2, -3], stereo: 10, bass: 20), + template(id: "builtin.fm", name: "Radio FM", subtitle: "Voce in evidenza", icon: "radio", gains: [-1, 0, 2, 3, 2, 1], stereo: 20, bass: 10) + ] + + private static func template( + id: String, name: String, subtitle: String, icon: String, + gains: [Double], stereo: Int, bass: Int + ) -> AudioProfileTemplate { + AudioProfileTemplate( + id: id, + name: name, + subtitle: subtitle, + systemImage: icon, + isBuiltIn: true, + eq: AudioProfileTemplate.eqBands(gains: gains), + enhancements: EnhancementsState(stereoLevel: stereo, bassLevel: bass) + ) + } + + static func matchActive(_ profile: AudioProfileTemplate, eq: [EQBandState], enhancements: EnhancementsState) -> Bool { + guard eq.count == profile.eq.count else { return false } + let sortedEq = eq.sorted { $0.centerHz < $1.centerHz } + let sortedProfile = profile.eq.sorted { $0.centerHz < $1.centerHz } + let eqMatch = zip(sortedEq, sortedProfile).allSatisfy { abs($0.gainDb - $1.gainDb) < 0.6 } + return eqMatch + && profile.enhancements.stereoLevel == enhancements.stereoLevel + && profile.enhancements.bassLevel == enhancements.bassLevel + } +} diff --git a/Software/APP/igiRadio/igiRadio/Models/DigiRadioState.swift b/Software/APP/igiRadio/igiRadio/Models/DigiRadioState.swift index 06a9ea0..dbe2fe0 100644 --- a/Software/APP/igiRadio/igiRadio/Models/DigiRadioState.swift +++ b/Software/APP/igiRadio/igiRadio/Models/DigiRadioState.swift @@ -116,7 +116,7 @@ struct MasterVolumeState: Equatable, Sendable, Codable { } } -struct EQBandState: Equatable, Sendable, Codable, Identifiable { +struct EQBandState: Equatable, Hashable, Sendable, Codable, Identifiable { var id: Int { index } var index: Int var gainDb: Double @@ -152,7 +152,7 @@ struct EQBandState: Equatable, Sendable, Codable, Identifiable { } } -struct EnhancementsState: Equatable, Sendable, Codable { +struct EnhancementsState: Equatable, Hashable, Sendable, Codable { var stereoLevel: Int = 0 var bassLevel: Int = 0 diff --git a/Software/APP/igiRadio/igiRadio/Services/Audio/AudioProfileStore.swift b/Software/APP/igiRadio/igiRadio/Services/Audio/AudioProfileStore.swift new file mode 100644 index 0000000..0d341d5 --- /dev/null +++ b/Software/APP/igiRadio/igiRadio/Services/Audio/AudioProfileStore.swift @@ -0,0 +1,20 @@ +import Foundation + +/// Persists user-created audio profiles on the iPhone (firmware stores one active profile only). +enum AudioProfileStore { + private static let key = "igiRadio.userAudioProfiles" + + static func loadUserProfiles() -> [AudioProfileTemplate] { + guard let data = UserDefaults.standard.data(forKey: key) else { return [] } + return (try? JSONDecoder().decode([AudioProfileTemplate].self, from: data)) ?? [] + } + + static func saveUserProfiles(_ profiles: [AudioProfileTemplate]) { + guard let data = try? JSONEncoder().encode(profiles) else { return } + UserDefaults.standard.set(data, forKey: key) + } + + static func allProfiles() -> [AudioProfileTemplate] { + AudioProfileLibrary.builtIn + loadUserProfiles() + } +} diff --git a/Software/APP/igiRadio/igiRadio/ViewModels/AudioProfileEditorViewModel.swift b/Software/APP/igiRadio/igiRadio/ViewModels/AudioProfileEditorViewModel.swift new file mode 100644 index 0000000..b6ef50b --- /dev/null +++ b/Software/APP/igiRadio/igiRadio/ViewModels/AudioProfileEditorViewModel.swift @@ -0,0 +1,51 @@ +import Foundation +import Observation + +@Observable +final class AudioProfileEditorViewModel { + var profile: AudioProfileTemplate + private let service: any DigiRadioService + private let onSave: (AudioProfileTemplate) -> Void + private var applyTask: Task? + + var isSaving = false + var isNewProfile: Bool + + init( + profile: AudioProfileTemplate, + service: any DigiRadioService, + isNewProfile: Bool, + onSave: @escaping (AudioProfileTemplate) -> Void + ) { + self.profile = profile + self.service = service + self.isNewProfile = isNewProfile + self.onSave = onSave + } + + func schedulePreviewApply() { + guard !profile.isBuiltIn else { return } + applyTask?.cancel() + applyTask = Task { + try? await Task.sleep(for: .milliseconds(400)) + guard !Task.isCancelled else { return } + await applyToDevice(persistUserCopy: false) + } + } + + func applyToDevice(persistUserCopy: Bool) async { + isSaving = true + defer { isSaving = false } + let audio = service.state.audio + let dto = profile.profileDTO(mixer: audio.mixer, master: audio.master) + try? await service.applyAudioProfile(dto) + if persistUserCopy, !profile.isBuiltIn { + onSave(profile) + } + } + + func saveUserProfile() { + guard !profile.isBuiltIn else { return } + onSave(profile) + } +} diff --git a/Software/APP/igiRadio/igiRadio/ViewModels/AudioProfilesViewModel.swift b/Software/APP/igiRadio/igiRadio/ViewModels/AudioProfilesViewModel.swift new file mode 100644 index 0000000..44b2af8 --- /dev/null +++ b/Software/APP/igiRadio/igiRadio/ViewModels/AudioProfilesViewModel.swift @@ -0,0 +1,94 @@ +import Foundation +import Observation + +@Observable +final class AudioProfilesViewModel { + private let service: any DigiRadioService + + var builtIn: [AudioProfileTemplate] = AudioProfileLibrary.builtIn + var userProfiles: [AudioProfileTemplate] = [] + var activeProfileID: String? + var isLoading = false + var isApplying = false + var errorMessage: String? + + init(service: any DigiRadioService) { + self.service = service + reloadUserProfiles() + } + + func reloadUserProfiles() { + userProfiles = AudioProfileStore.loadUserProfiles() + } + + func load() async { + isLoading = true + defer { isLoading = false } + try? await service.refreshAudioProfile() + detectActiveProfile() + } + + func detectActiveProfile() { + let audio = service.state.audio + activeProfileID = AudioProfileStore.allProfiles().first { + AudioProfileLibrary.matchActive($0, eq: audio.eq, enhancements: audio.enhancements) + }?.id + } + + func apply(_ profile: AudioProfileTemplate) async { + isApplying = true + errorMessage = nil + defer { isApplying = false } + do { + let audio = service.state.audio + let dto = profile.profileDTO(mixer: audio.mixer, master: audio.master) + try await service.applyAudioProfile(dto) + activeProfileID = profile.id + } catch { + errorMessage = error.localizedDescription + } + } + + func deleteUserProfile(_ profile: AudioProfileTemplate) { + guard !profile.isBuiltIn else { return } + userProfiles.removeAll { $0.id == profile.id } + AudioProfileStore.saveUserProfiles(userProfiles) + if activeProfileID == profile.id { activeProfileID = nil } + } + + func saveUserProfile(_ profile: AudioProfileTemplate) { + var copy = profile + copy.isBuiltIn = false + if let index = userProfiles.firstIndex(where: { $0.id == copy.id }) { + userProfiles[index] = copy + } else { + userProfiles.append(copy) + } + AudioProfileStore.saveUserProfiles(userProfiles) + } + + func duplicateAsUser(from profile: AudioProfileTemplate, name: String) -> AudioProfileTemplate { + AudioProfileTemplate( + id: UUID().uuidString, + name: name, + subtitle: "Profilo personalizzato", + systemImage: "slider.horizontal.3", + isBuiltIn: false, + eq: profile.eq, + enhancements: profile.enhancements + ) + } + + func profileFromDevice(name: String) -> AudioProfileTemplate { + let audio = service.state.audio + return AudioProfileTemplate( + id: UUID().uuidString, + name: name, + subtitle: "Creato dal dispositivo", + systemImage: "waveform.path.ecg", + isBuiltIn: false, + eq: audio.eq.isEmpty ? AudioProfileLibrary.builtIn[0].eq : audio.eq, + enhancements: audio.enhancements + ) + } +} diff --git a/Software/APP/igiRadio/igiRadio/Views/AudioProfileEditorView.swift b/Software/APP/igiRadio/igiRadio/Views/AudioProfileEditorView.swift new file mode 100644 index 0000000..e99ca60 --- /dev/null +++ b/Software/APP/igiRadio/igiRadio/Views/AudioProfileEditorView.swift @@ -0,0 +1,118 @@ +import SwiftUI + +struct AudioProfileEditorView: View { + @Environment(AppEnvironment.self) private var environment + @Environment(\.dismiss) private var dismiss + + @State private var viewModel: AudioProfileEditorViewModel? + let profile: AudioProfileTemplate + let isNewProfile: Bool + let onSave: (AudioProfileTemplate) -> Void + + var body: some View { + let vm = viewModel ?? AudioProfileEditorViewModel( + profile: profile, + service: environment.digiRadio, + isNewProfile: isNewProfile, + onSave: onSave + ) + + ScrollView { + VStack(alignment: .leading, spacing: IGITheme.spacingL) { + if !vm.profile.isBuiltIn { + VStack(alignment: .leading, spacing: IGITheme.spacingS) { + Text("Nome profilo") + .font(.headline) + TextField("Nome", text: Binding( + get: { vm.profile.name }, + set: { vm.profile.name = $0 } + )) + .padding() + .background(Color.primary.opacity(0.05), in: RoundedRectangle(cornerRadius: 12)) + } + .igiPremiumCard() + } + + VStack(alignment: .leading, spacing: IGITheme.spacingM) { + Text("Equalizzatore") + .font(.title3.weight(.bold)) + IGIGraphicEqualizer( + bands: Binding( + get: { vm.profile.eq }, + set: { vm.profile.eq = $0 } + ), + onCommit: { vm.schedulePreviewApply() } + ) + } + + VStack(alignment: .leading, spacing: IGITheme.spacingM) { + Text("Enhancements") + .font(.headline) + HStack(spacing: IGITheme.spacingL) { + IGIEnhancementDial( + title: "Stereo", + systemImage: "circle.lefthalf.filled", + level: Binding( + get: { Double(vm.profile.enhancements.stereoLevel) }, + set: { vm.profile.enhancements.stereoLevel = Int($0) } + ), + onCommit: { vm.schedulePreviewApply() } + ) + IGIEnhancementDial( + title: "Bass", + systemImage: "waveform.path", + level: Binding( + get: { Double(vm.profile.enhancements.bassLevel) }, + set: { vm.profile.enhancements.bassLevel = Int($0) } + ), + onCommit: { vm.schedulePreviewApply() } + ) + } + } + .igiPremiumCard() + + if !vm.profile.isBuiltIn { + Button { + vm.saveUserProfile() + Task { await vm.applyToDevice(persistUserCopy: true) } + dismiss() + } label: { + Label("Salva profilo", systemImage: "square.and.arrow.down") + .frame(maxWidth: .infinity) + .padding(.vertical, 14) + } + .buttonStyle(.borderedProminent) + } + + Button { + Task { await vm.applyToDevice(persistUserCopy: false) } + } label: { + Label("Applica a DigiRadio", systemImage: "play.fill") + .frame(maxWidth: .infinity) + .padding(.vertical, 14) + } + .buttonStyle(.bordered) + + if vm.profile.isBuiltIn { + Button { + let duplicate = vm.profile.duplicatedAsUser(named: "\(vm.profile.name) custom") + onSave(duplicate) + dismiss() + } label: { + Label("Salva come profilo personale", systemImage: "plus.square.on.square") + .frame(maxWidth: .infinity) + .padding(.vertical, 14) + } + .buttonStyle(.bordered) + } + } + .padding(IGITheme.spacingM) + } + .background(IGIHeroBackground()) + .navigationTitle(vm.profile.isBuiltIn ? vm.profile.name : "Modifica profilo") + .navigationBarTitleDisplayMode(.inline) + .onAppear { + if viewModel == nil { viewModel = vm } + } + } +} diff --git a/Software/APP/igiRadio/igiRadio/Views/AudioProfilesView.swift b/Software/APP/igiRadio/igiRadio/Views/AudioProfilesView.swift new file mode 100644 index 0000000..40a7430 --- /dev/null +++ b/Software/APP/igiRadio/igiRadio/Views/AudioProfilesView.swift @@ -0,0 +1,190 @@ +import SwiftUI + +struct AudioProfilesView: View { + @Environment(AppEnvironment.self) private var environment + @State private var viewModel: AudioProfilesViewModel? + @State private var editorProfile: AudioProfileTemplate? + @State private var editorIsNew = false + @State private var showNewProfileSheet = false + @State private var newProfileName = "" + + var body: some View { + let vm = viewModel ?? AudioProfilesViewModel(service: environment.digiRadio) + + ScrollView { + VStack(alignment: .leading, spacing: IGITheme.spacingL) { + activeBanner(vm) + profileSection(title: "Predefiniti", profiles: vm.builtIn, vm: vm, allowDelete: false, onDelete: nil) + profileSection(title: "I tuoi profili", profiles: vm.userProfiles, vm: vm, allowDelete: true, onDelete: { vm.deleteUserProfile($0) }) + + if vm.userProfiles.isEmpty { + Text("Crea un profilo personalizzato partendo da un preset o dal suono attuale del device.") + .font(.caption) + .foregroundStyle(.secondary) + .igiPremiumCard() + } + + HStack(spacing: IGITheme.spacingM) { + Button { + newProfileName = "Il mio profilo" + showNewProfileSheet = true + } label: { + Label("Nuovo profilo", systemImage: "plus.circle.fill") + .frame(maxWidth: .infinity) + .padding(.vertical, 14) + } + .buttonStyle(.borderedProminent) + + NavigationLink { + AudioView() + } label: { + Label("Mixer", systemImage: "slider.vertical.3") + .frame(maxWidth: .infinity) + .padding(.vertical, 14) + } + .buttonStyle(.bordered) + } + } + .padding(IGITheme.spacingM) + } + .background(IGIHeroBackground()) + .navigationTitle("Profilo audio") + .navigationBarTitleDisplayMode(.large) + .navigationDestination(item: $editorProfile) { profile in + AudioProfileEditorView( + profile: profile, + isNewProfile: editorIsNew, + onSave: { saved in + vm.saveUserProfile(saved) + vm.reloadUserProfiles() + vm.activeProfileID = saved.id + } + ) + } + .alert("Nuovo profilo", isPresented: $showNewProfileSheet) { + TextField("Nome", text: $newProfileName) + Button("Annulla", role: .cancel) {} + Button("Crea") { + let profile = vm.profileFromDevice(name: newProfileName.isEmpty ? "Il mio profilo" : newProfileName) + editorIsNew = true + editorProfile = profile + } + } message: { + Text("Parte dal profilo attualmente sul DigiRadio.") + } + .onAppear { + if viewModel == nil { viewModel = vm } + Task { await vm.load() } + } + } + + @ViewBuilder + private func activeBanner(_ vm: AudioProfilesViewModel) -> some View { + HStack(spacing: IGITheme.spacingM) { + Image(systemName: "checkmark.seal.fill") + .font(.title2) + .foregroundStyle(IGITheme.accent) + VStack(alignment: .leading, spacing: 2) { + Text("Profilo attivo") + .font(.caption) + .foregroundStyle(.secondary) + Text(vm.builtIn.first(where: { $0.id == vm.activeProfileID })?.name + ?? vm.userProfiles.first(where: { $0.id == vm.activeProfileID })?.name + ?? "Personalizzato / device") + .font(.headline) + } + Spacer() + if vm.isApplying { ProgressView() } + } + .igiPremiumCard() + } + + @ViewBuilder + private func profileSection( + title: String, + profiles: [AudioProfileTemplate], + vm: AudioProfilesViewModel, + allowDelete: Bool, + onDelete: ((AudioProfileTemplate) -> Void)? + ) -> some View { + VStack(alignment: .leading, spacing: IGITheme.spacingM) { + Text(title) + .font(.title3.weight(.bold)) + + LazyVGrid(columns: [GridItem(.adaptive(minimum: 150), spacing: IGITheme.spacingS)], spacing: IGITheme.spacingS) { + ForEach(profiles) { profile in + AudioProfileCard( + profile: profile, + isActive: vm.activeProfileID == profile.id, + allowDelete: allowDelete + ) { + Task { await vm.apply(profile) } + } onEdit: { + editorIsNew = false + editorProfile = profile + } onDelete: { + onDelete?(profile) + } + } + } + } + .igiPremiumCard() + } +} + +private struct AudioProfileCard: View { + var profile: AudioProfileTemplate + var isActive: Bool + var allowDelete: Bool + var onApply: () -> Void + var onEdit: () -> Void + var onDelete: () -> Void + + var body: some View { + VStack(alignment: .leading, spacing: IGITheme.spacingS) { + HStack { + Image(systemName: profile.systemImage) + .foregroundStyle(isActive ? .white : IGITheme.accent) + Spacer() + if isActive { + Image(systemName: "checkmark.circle.fill") + .foregroundStyle(.white) + } + } + Text(profile.name) + .font(.headline) + .foregroundStyle(isActive ? .white : .primary) + Text(profile.subtitle) + .font(.caption2) + .foregroundStyle(isActive ? .white.opacity(0.85) : .secondary) + .lineLimit(2) + + HStack(spacing: IGITheme.spacingS) { + Button("Applica", action: onApply) + .font(.caption.weight(.semibold)) + .buttonStyle(.borderedProminent) + .tint(isActive ? .white : IGITheme.accent) + + Button("Modifica", action: onEdit) + .font(.caption.weight(.semibold)) + .buttonStyle(.bordered) + .tint(isActive ? .white : .primary) + } + .padding(.top, 4) + } + .padding(IGITheme.spacingM) + .background( + isActive + ? AnyShapeStyle(IGITheme.accent.gradient) + : AnyShapeStyle(Color.primary.opacity(0.05)), + in: RoundedRectangle(cornerRadius: 16, style: .continuous) + ) + .contextMenu { + if allowDelete { + Button(role: .destructive, action: onDelete) { + Label("Elimina", systemImage: "trash") + } + } + } + } +} diff --git a/Software/APP/igiRadio/igiRadio/Views/AudioView.swift b/Software/APP/igiRadio/igiRadio/Views/AudioView.swift index 8518bd0..43019ac 100644 --- a/Software/APP/igiRadio/igiRadio/Views/AudioView.swift +++ b/Software/APP/igiRadio/igiRadio/Views/AudioView.swift @@ -33,8 +33,6 @@ struct AudioView: View { switch vm.panel { case .mixer: mixerPanel(vm) - case .equalizer: - equalizerPanel(vm) case .enhance: enhancePanel(vm) } @@ -161,30 +159,6 @@ struct AudioView: View { } } - @ViewBuilder - private func equalizerPanel(_ vm: AudioViewModel) -> some View { - VStack(alignment: .leading, spacing: IGITheme.spacingM) { - Text("Equalizzatore 6 bande") - .font(.headline) - Text("Trascina le barre per regolare il guadagno. Rilascia per applicare al DSP.") - .font(.caption) - .foregroundStyle(.secondary) - - if vm.eqBands.isEmpty { - ProgressView() - .frame(maxWidth: .infinity) - } else { - IGIGraphicEqualizer( - bands: Binding( - get: { vm.eqBands }, - set: { vm.eqBands = $0 } - ), - onCommit: { vm.scheduleApply() } - ) - } - } - } - @ViewBuilder private func enhancePanel(_ vm: AudioViewModel) -> some View { HStack(spacing: IGITheme.spacingL) { diff --git a/Software/APP/igiRadio/igiRadio/Views/HomeView.swift b/Software/APP/igiRadio/igiRadio/Views/HomeView.swift index ff11ec1..d4cb1cd 100644 --- a/Software/APP/igiRadio/igiRadio/Views/HomeView.swift +++ b/Software/APP/igiRadio/igiRadio/Views/HomeView.swift @@ -196,9 +196,9 @@ struct HomeView: View { quickLinkLabel("DAB", icon: "antenna.radiowaves.left.and.right") } NavigationLink { - AudioView() + AudioProfilesView() } label: { - quickLinkLabel("Audio", icon: "slider.vertical.3") + quickLinkLabel("Profilo", icon: "waveform.path.ecg") } NavigationLink { StreamingView() diff --git a/Software/APP/igiRadio/igiRadio/Views/RootView.swift b/Software/APP/igiRadio/igiRadio/Views/RootView.swift index f1d3a08..84e43a9 100644 --- a/Software/APP/igiRadio/igiRadio/Views/RootView.swift +++ b/Software/APP/igiRadio/igiRadio/Views/RootView.swift @@ -36,8 +36,8 @@ private struct IPhoneRootView: View { NavigationStack { PresetsView() } .tabItem { Label("Preset", systemImage: "star.fill") } - NavigationStack { AudioView() } - .tabItem { Label("Audio", systemImage: "waveform") } + NavigationStack { AudioProfilesView() } + .tabItem { Label("Profilo", systemImage: "waveform.path.ecg") } NavigationStack { SettingsRootView() } .tabItem { Label("Impostazioni", systemImage: "gearshape.fill") } @@ -63,7 +63,7 @@ private struct IPadRootView: View { case .dab: DABRadioView() case .stream: StreamingView() case .bluetooth: BluetoothView() - case .audio: AudioView() + case .audioProfiles: AudioProfilesView() case .presets: PresetsView() case .settings: SettingsRootView() } @@ -72,7 +72,7 @@ private struct IPadRootView: View { } enum SidebarItem: String, CaseIterable, Identifiable { - case home, fm, dab, stream, bluetooth, audio, presets, settings + case home, fm, dab, stream, bluetooth, audioProfiles, presets, settings var id: String { rawValue } @@ -83,7 +83,7 @@ enum SidebarItem: String, CaseIterable, Identifiable { case .dab: "DAB" case .stream: "Stream" case .bluetooth: "Bluetooth" - case .audio: "Audio" + case .audioProfiles: "Profilo audio" case .presets: "Preset" case .settings: "Impostazioni" } @@ -96,7 +96,7 @@ enum SidebarItem: String, CaseIterable, Identifiable { case .dab: "antenna.radiowaves.left.and.right" case .stream: "dot.radiowaves.forward" case .bluetooth: "dot.radiowaves.left.and.right" - case .audio: "waveform" + case .audioProfiles: "waveform.path.ecg" case .presets: "star.fill" case .settings: "gearshape.fill" } diff --git a/Software/APP/igiRadio/igiRadio/Views/SettingsViews.swift b/Software/APP/igiRadio/igiRadio/Views/SettingsViews.swift index 69b853c..be7bdde 100644 --- a/Software/APP/igiRadio/igiRadio/Views/SettingsViews.swift +++ b/Software/APP/igiRadio/igiRadio/Views/SettingsViews.swift @@ -16,10 +16,15 @@ struct SettingsRootView: View { NavigationLink("Preset") { PresetsView() } } Section("Audio") { + NavigationLink { + AudioProfilesView() + } label: { + Label("Profili audio", systemImage: "waveform.path.ecg") + } NavigationLink { AudioView() } label: { - Label("Mixer & Equalizzatore", systemImage: "slider.vertical.3") + Label("Mixer & volume", systemImage: "slider.vertical.3") } NavigationLink("Bluetooth speaker") { BluetoothView() } } From 3a58d33aadbe26a81feda70955b2668c9cff481f Mon Sep 17 00:00:00 2001 From: Michele Bigi Date: Fri, 21 Aug 2026 08:12:08 +0200 Subject: [PATCH 05/12] Add DAB ANTCAP calibration and fix BT1035 boot banner timing Extend the FM-only ANTCAP antenna-varactor override to DAB, mirroring the existing mechanism end to end (driver, tuner, service, EEPROM storage, HTTP API). Live sweep on real hardware found no ANTCAP value beating auto-tune on the ensembles tested, so DAB stays on auto-tune by default. Also fix BT1035 boot: the module's real boot banner doesn't appear until ~18-24s after RESET# releases, not the 3.5s previously waited; add a 2-attempt retry and a baud-rate probe fallback for diagnostics. Co-Authored-By: Claude Sonnet 5 --- .../components/core/include/core/ITuner.hpp | 16 +- .../core/include/core/TunerJson.hpp | 37 +++ Software/components/core/src/TunerJson.cpp | 28 +++ Software/components/core/test/CMakeLists.txt | 4 + .../core/test/bluetooth_json_test.cpp | 220 ++++++++++++++++++ .../core/test/integration_service_test.cpp | 4 +- .../core/test/station_service_test.cpp | 4 +- .../bt1035/include/bt1035/Bt1035Driver.hpp | 1 + .../drivers/bt1035/src/Bt1035Driver.cpp | 137 +++++++++-- .../drivers/eeprom24aa/CMakeLists.txt | 2 +- .../include/eeprom24aa/Eeprom24aa.hpp | 86 ++++++- .../drivers/eeprom24aa/src/Eeprom24aa.cpp | 152 ++++++++++++ .../si4684/include/si4684/Si4684Driver.hpp | 7 +- .../si4684/include/si4684/Si4684Tuner.hpp | 6 +- .../drivers/si4684/src/Si4684Driver.cpp | 55 +++-- .../drivers/si4684/src/Si4684Tuner.cpp | 8 +- .../net/include/net/BleProvisioning.hpp | 10 +- .../net/include/net/NetBootstrap.hpp | 3 + .../net/include/net/SetupWebServer.hpp | 26 +++ .../components/net/src/BleProvisioning.cpp | 12 +- Software/components/net/src/NetBootstrap.cpp | 16 +- .../components/net/src/SetupWebServer.cpp | 86 ++++++- .../tuner/include/tuner/TunerService.hpp | 44 +++- .../services/tuner/src/TunerService.cpp | 23 +- Software/docs/TODO.md | 57 ++++- Software/docs/manual/ch-api.tex | 39 +++- Software/docs/manual/ch-classes.tex | 2 +- Software/docs/manual/ch-intro.tex | 2 +- Software/docs/manual/manual.tex | 2 +- .../docs/si4684-rf-investigation-report.md | 186 +++++++++++++-- Software/instructions.md | 62 ++++- Software/main/CMakeLists.txt | 1 + Software/main/antenna_calibration.cpp | 26 +++ Software/main/antenna_calibration.hpp | 28 +++ Software/main/hardware_bootstrap.cpp | 83 ++++++- Software/main/hardware_bootstrap.hpp | 60 +++++ Software/main/main.cpp | 10 + Software/partitions.csv | 14 +- 38 files changed, 1444 insertions(+), 115 deletions(-) create mode 100644 Software/components/core/test/bluetooth_json_test.cpp create mode 100644 Software/main/antenna_calibration.cpp create mode 100644 Software/main/antenna_calibration.hpp diff --git a/Software/components/core/include/core/ITuner.hpp b/Software/components/core/include/core/ITuner.hpp index d093323..829b467 100644 --- a/Software/components/core/include/core/ITuner.hpp +++ b/Software/components/core/include/core/ITuner.hpp @@ -86,6 +86,10 @@ public: * * @dname tuneDab * @param freqIndex Ensemble index 0–37. + * @param antCap Front-end antenna varactor override (0-128). + * 0 = automatic; other values force a specific + * varactor setting, for antenna calibration + * sweeps. See tuneFm's antCap doc for details. * @return Ok on success, or WrongBand / TuneFailed / NotBooted. * @pubstate writes last tune target in the adapter. * @@ -93,13 +97,21 @@ public: * @date 2026-07-06 */ [[nodiscard]] virtual std::expected tuneDab( - std::uint8_t freqIndex) = 0; + std::uint8_t freqIndex, std::uint8_t antCap = 0U) = 0; /** * @brief tuneFm — tune to an FM centre frequency. * * @dname tuneFm * @param frequency Validated FM centre frequency. + * @param antCap Front-end antenna varactor override (0-128). + * 0 = automatic (chip's own FE_VARM/VARB-derived + * tuning); other values force a specific varactor + * setting, for antenna calibration sweeps. Chip- + * specific concept (AN851 Appendix A on the + * Si4684), exposed here only because ANTCAP has no + * other reasonable home without duplicating the + * whole tune path per driver. * @return Ok on success, or WrongBand / TuneFailed / NotBooted. * @pubstate writes last tune target in the adapter. * @@ -107,7 +119,7 @@ public: * @date 2026-07-06 */ [[nodiscard]] virtual std::expected tuneFm( - FrequencyKHz frequency) = 0; + FrequencyKHz frequency, std::uint8_t antCap = 0U) = 0; /** * @brief seekFm — seek to the next valid FM station. diff --git a/Software/components/core/include/core/TunerJson.hpp b/Software/components/core/include/core/TunerJson.hpp index 53fae2d..edad96f 100644 --- a/Software/components/core/include/core/TunerJson.hpp +++ b/Software/components/core/include/core/TunerJson.hpp @@ -41,6 +41,9 @@ struct TunerTuneRequest { TunerBand band; ///< Target band (Dab or Fm). std::uint8_t dabFreqIndex; ///< Band III ensemble index (0–37) when band is Dab. std::optional fmFrequency; ///< FM centre frequency when band is Fm. + std::optional antCap; ///< Antenna varactor override (0–128), + ///< for calibration sweeps; applies to + ///< whichever band is being tuned. }; /** @@ -239,4 +242,38 @@ struct TunerFmScannedStation { [[nodiscard]] std::string serializeTunerFmBandScanJson( const std::vector& stations); +/** + * @brief AntennaCalibrationRequest — parsed POST + * /api/tuner/calibrate-antenna body. + * + * @dname AntennaCalibrationRequest + * @return n/a (type) + * @pubstate Plain DTO filled by parseAntennaCalibrationJson at the HTTP + * boundary. + * + * @author Michele Bigi + * @date 2026-08-20 + */ +struct AntennaCalibrationRequest { + TunerBand band; ///< Which band's ANTCAP default this saves. + std::uint8_t antCap; ///< Value to persist (0-128). +}; + +/** + * @brief parseAntennaCalibrationJson — validate POST + * /api/tuner/calibrate-antenna body. + * + * @dname parseAntennaCalibrationJson + * @param json Untrusted request body from the HTTP handler. + * @return Band + ANTCAP value (0-128) on success, or a ParseError. + * `band` defaults to Fm when the field is omitted, preserving + * the original FM-only request shape. + * @pubstate none + * + * @author Michele Bigi + * @date 2026-08-19 + */ +[[nodiscard]] std::expected +parseAntennaCalibrationJson(std::string_view json); + } // namespace core diff --git a/Software/components/core/src/TunerJson.cpp b/Software/components/core/src/TunerJson.cpp index e2fef81..3df7952 100644 --- a/Software/components/core/src/TunerJson.cpp +++ b/Software/components/core/src/TunerJson.cpp @@ -203,6 +203,10 @@ std::expected parseTunerTuneJson( } else { return std::unexpected(ParseError::InvalidJson); } + unsigned long antCap = 0U; + if (extractJsonUint(json, "antcap", antCap) && antCap <= 128U) { + req.antCap = static_cast(antCap); + } return req; } @@ -336,4 +340,28 @@ std::string serializeTunerFmBandScanJson( return out.str(); } +std::expected +parseAntennaCalibrationJson(std::string_view json) +{ + if (json.find('{') == std::string_view::npos) { + return std::unexpected(ParseError::InvalidJson); + } + unsigned long antCap = 0U; + if (!extractJsonUint(json, "antcap", antCap) || antCap > 128U) { + return std::unexpected(ParseError::MissingField); + } + + AntennaCalibrationRequest req = {}; + req.antCap = static_cast(antCap); + const std::string_view band = extractJsonString(json, "band"); + if (band == "dab") { + req.band = TunerBand::Dab; + } else if (band.empty() || band == "fm") { + req.band = TunerBand::Fm; + } else { + return std::unexpected(ParseError::InvalidJson); + } + return req; +} + } // namespace core diff --git a/Software/components/core/test/CMakeLists.txt b/Software/components/core/test/CMakeLists.txt index 32a576c..784f300 100644 --- a/Software/components/core/test/CMakeLists.txt +++ b/Software/components/core/test/CMakeLists.txt @@ -94,6 +94,10 @@ add_executable(dsp_param_json_test dsp_param_json_test.cpp) target_link_libraries(dsp_param_json_test PRIVATE digiradio_core) add_test(NAME dsp_param_json_test COMMAND dsp_param_json_test) +add_executable(bluetooth_json_test bluetooth_json_test.cpp) +target_link_libraries(bluetooth_json_test PRIVATE digiradio_core) +add_test(NAME bluetooth_json_test COMMAND bluetooth_json_test) + add_executable(frequency_khz_test frequency_khz_test.cpp) target_link_libraries(frequency_khz_test PRIVATE digiradio_core) add_test(NAME frequency_khz_test COMMAND frequency_khz_test) diff --git a/Software/components/core/test/bluetooth_json_test.cpp b/Software/components/core/test/bluetooth_json_test.cpp new file mode 100644 index 0000000..29f0a6e --- /dev/null +++ b/Software/components/core/test/bluetooth_json_test.cpp @@ -0,0 +1,220 @@ +/** + * @file bluetooth_json_test.cpp + * @brief Host tests for Bluetooth JSON parse/serialise. + * + * DigiRadio firmware — https://github.com/manvalan/DigiRadio + * + * Copyright 2026 Michele Bigi + * SPDX-License-Identifier: Apache-2.0 + * + * @author Michele Bigi + * @date 2026-08-19 + */ + +#include "core/BluetoothJson.hpp" +#include "core/ParseError.hpp" + +#include +#include +#include + +namespace { + +[[nodiscard]] bool expectEqual(const std::string& actual, + const std::string& expected) +{ + if (actual == expected) { + return true; + } + std::cerr << "expected: " << expected << "\nactual: " << actual << '\n'; + return false; +} + +[[nodiscard]] int runStatusSerialiseTest() +{ + const core::BluetoothStatus status{ + .booted = true, + .pairing = false, + .a2dpState = core::Bt1035A2dpState::Streaming, + .deviceName = "DigiRadio-CC4DB4", + .autoReconnect = 3U, + }; + const std::string json = core::serializeBluetoothStatusJson(status); + if (!expectEqual(json, + R"({"booted":true,"pairing":false,"a2dp":"streaming",)" + R"("device_name":"DigiRadio-CC4DB4","auto_reconnect":3})")) { + return EXIT_FAILURE; + } + return EXIT_SUCCESS; +} + +[[nodiscard]] int runScanSerialiseTest() +{ + const std::vector devices{ + core::Bt1035ScannedDevice{ + .index = 1U, + .addressType = 2U, + .mac = "001122334455", + .rssiDbm = -58, + .name = "Bose SoundLink", + .deviceClass = "240404", + }, + }; + const std::string json = core::serializeBluetoothScanJson(devices); + if (!expectEqual(json, + R"({"devices":[{"index":1,"mac":"001122334455",)" + R"("name":"Bose SoundLink","rssi_dbm":-58}]})")) { + return EXIT_FAILURE; + } + return EXIT_SUCCESS; +} + +[[nodiscard]] int runPairedSerialiseTest() +{ + const std::vector devices{ + core::Bt1035PairedDevice{.index = 1U, .mac = "AABBCCDDEEFF", .name = "Phone"}, + }; + const std::string json = core::serializeBluetoothPairedJson(devices); + if (!expectEqual(json, + R"({"devices":[{"index":1,"mac":"AABBCCDDEEFF","name":"Phone"}]})")) { + return EXIT_FAILURE; + } + return EXIT_SUCCESS; +} + +[[nodiscard]] int runAutoReconnectParseTest() +{ + const auto ok = core::parseBluetoothAutoReconnectJson(R"({"times":5})"); + if (!ok || *ok != 5U) { + std::cerr << "auto-reconnect valid parse failed\n"; + return EXIT_FAILURE; + } + const auto tooHigh = core::parseBluetoothAutoReconnectJson(R"({"times":16})"); + if (tooHigh) { + std::cerr << "auto-reconnect out-of-range accepted\n"; + return EXIT_FAILURE; + } + const auto missing = core::parseBluetoothAutoReconnectJson(R"({})"); + if (missing || missing.error() != core::ParseError::MissingField) { + std::cerr << "auto-reconnect missing field mis-reported\n"; + return EXIT_FAILURE; + } + return EXIT_SUCCESS; +} + +[[nodiscard]] int runConnectJsonParseTest() +{ + const auto ok = + core::parseBluetoothConnectJson(R"({"mac":"001122334455"})"); + if (!ok || *ok != "001122334455") { + std::cerr << "connect mac parse failed\n"; + return EXIT_FAILURE; + } + // Normalises to uppercase. + const auto lower = + core::parseBluetoothConnectJson(R"({"mac":"aabbccddeeff"})"); + if (!lower || *lower != "AABBCCDDEEFF") { + std::cerr << "connect mac uppercasing failed\n"; + return EXIT_FAILURE; + } + const auto invalid = core::parseBluetoothConnectJson(R"({"mac":"not-a-mac"})"); + if (invalid) { + std::cerr << "connect invalid mac accepted\n"; + return EXIT_FAILURE; + } + return EXIT_SUCCESS; +} + +[[nodiscard]] int runConnectRequestParseTest() +{ + const auto request = core::parseBluetoothConnectRequest( + R"({"mac":"001122334455","name":"Bose SoundLink","save":true})"); + if (request.mac != "001122334455" || request.name != "Bose SoundLink" + || !request.save) { + std::cerr << "connect request full parse failed\n"; + return EXIT_FAILURE; + } + const auto minimal = + core::parseBluetoothConnectRequest(R"({"mac":"001122334455"})"); + if (minimal.mac != "001122334455" || !minimal.name.empty() + || minimal.save) { + std::cerr << "connect request minimal parse failed\n"; + return EXIT_FAILURE; + } + const auto badMac = core::parseBluetoothConnectRequest(R"({})"); + if (!badMac.mac.empty()) { + std::cerr << "connect request missing mac should stay empty\n"; + return EXIT_FAILURE; + } + return EXIT_SUCCESS; +} + +[[nodiscard]] int runSpeakerRoundTripTest() +{ + const core::BtSpeakerTarget target{.mac = "001122334455", + .name = "Bose SoundLink"}; + const std::string json = core::serializeBluetoothSpeakerJson(&target); + if (!expectEqual(json, + R"({"configured":true,"mac":"001122334455",)" + R"("name":"Bose SoundLink"})")) { + return EXIT_FAILURE; + } + const std::string unset = core::serializeBluetoothSpeakerJson(nullptr); + if (!expectEqual(unset, R"({"configured":false})")) { + return EXIT_FAILURE; + } + + const auto parsed = core::parseBluetoothSpeakerJson( + R"({"mac":"001122334455","name":"Bose SoundLink"})"); + if (!parsed || parsed->mac != "001122334455" + || parsed->name != "Bose SoundLink") { + std::cerr << "speaker parse round-trip failed\n"; + return EXIT_FAILURE; + } + const auto invalid = core::parseBluetoothSpeakerJson(R"({"mac":"bad"})"); + if (invalid) { + std::cerr << "speaker parse invalid mac accepted\n"; + return EXIT_FAILURE; + } + return EXIT_SUCCESS; +} + +[[nodiscard]] int runErrorSerialiseTest() +{ + const std::string json = core::serializeBluetoothErrorJson("scan_failed"); + if (!expectEqual(json, R"({"status":"error","reason":"scan_failed"})")) { + return EXIT_FAILURE; + } + return EXIT_SUCCESS; +} + +} // namespace + +int main() +{ + if (runStatusSerialiseTest() != EXIT_SUCCESS) { + return EXIT_FAILURE; + } + if (runScanSerialiseTest() != EXIT_SUCCESS) { + return EXIT_FAILURE; + } + if (runPairedSerialiseTest() != EXIT_SUCCESS) { + return EXIT_FAILURE; + } + if (runAutoReconnectParseTest() != EXIT_SUCCESS) { + return EXIT_FAILURE; + } + if (runConnectJsonParseTest() != EXIT_SUCCESS) { + return EXIT_FAILURE; + } + if (runConnectRequestParseTest() != EXIT_SUCCESS) { + return EXIT_FAILURE; + } + if (runSpeakerRoundTripTest() != EXIT_SUCCESS) { + return EXIT_FAILURE; + } + if (runErrorSerialiseTest() != EXIT_SUCCESS) { + return EXIT_FAILURE; + } + return EXIT_SUCCESS; +} diff --git a/Software/components/core/test/integration_service_test.cpp b/Software/components/core/test/integration_service_test.cpp index d98445b..41451d1 100644 --- a/Software/components/core/test/integration_service_test.cpp +++ b/Software/components/core/test/integration_service_test.cpp @@ -111,13 +111,13 @@ public: } [[nodiscard]] std::expected tuneDab( - std::uint8_t) override + std::uint8_t, std::uint8_t) override { return {}; } [[nodiscard]] std::expected tuneFm( - core::FrequencyKHz) override + core::FrequencyKHz, std::uint8_t) override { tunedFm = true; return {}; diff --git a/Software/components/core/test/station_service_test.cpp b/Software/components/core/test/station_service_test.cpp index d82a267..494e0f7 100644 --- a/Software/components/core/test/station_service_test.cpp +++ b/Software/components/core/test/station_service_test.cpp @@ -53,13 +53,13 @@ public: } [[nodiscard]] std::expected tuneDab( - std::uint8_t) override + std::uint8_t, std::uint8_t) override { return {}; } [[nodiscard]] std::expected tuneFm( - core::FrequencyKHz) override + core::FrequencyKHz, std::uint8_t) override { return {}; } diff --git a/Software/components/drivers/bt1035/include/bt1035/Bt1035Driver.hpp b/Software/components/drivers/bt1035/include/bt1035/Bt1035Driver.hpp index 5e02254..3d65d06 100644 --- a/Software/components/drivers/bt1035/include/bt1035/Bt1035Driver.hpp +++ b/Software/components/drivers/bt1035/include/bt1035/Bt1035Driver.hpp @@ -343,6 +343,7 @@ public: private: [[nodiscard]] std::expected ensureBooted() const; [[nodiscard]] std::expected runInitSequence(); + [[nodiscard]] std::expected resetAndInitOnce(); [[nodiscard]] std::expected transmitAndCollect( std::string_view commandLine, int timeoutMs = kResponseTimeoutMs); [[nodiscard]] std::expected transmitAndCollectUntil( diff --git a/Software/components/drivers/bt1035/src/Bt1035Driver.cpp b/Software/components/drivers/bt1035/src/Bt1035Driver.cpp index 208cf90..1b5e7de 100644 --- a/Software/components/drivers/bt1035/src/Bt1035Driver.cpp +++ b/Software/components/drivers/bt1035/src/Bt1035Driver.cpp @@ -38,6 +38,23 @@ constexpr int kUartTxBuffer = 256; constexpr int kResponseTimeoutMs = 2000; constexpr int kPostResetMs = 500; constexpr int kPostUartMs = 100; +/** Feasycom BT1035 programming user guide §2.2 (pin 34 SYS_CTRL): "Delay + * 100ms, pull high". */ +constexpr int kSysCtlLeadInMs = 100; +/** Margin beyond the datasheet's own >20ms SYS_CTRL-assertion-to-power-up + * minimum (§4.7), for regulator/crystal settling before RESET releases. */ +constexpr int kSysCtlSettleMs = 50; +/** Measured live (2026-08-20, power/wiring confirmed sound with a + * multimeter — VBAT_IN/SYS_CTRL/1.8V_OUT/VDD_IO all correct, TX/RX pins + * verified via continuity): the module's spontaneous boot banner + * (+VER=FSC-BT1035,..., +DEVSTAT=1) doesn't appear until ~18.5s after + * RESET# releases — full BT stack init, not just the internal regulator. + * The previous 3500ms wait here was never enough for the module to say + * anything, so every prior boot attempt cut power and restarted before + * the module could finish booting even once. See kBootAttempts below. */ +constexpr int kBootBannerWaitMs = 25000; +constexpr int kBootAttempts = 2; +constexpr int kBootRetryDelayMs = 300; void flushUartRx(int uartPort) noexcept { @@ -49,15 +66,53 @@ void flushUartRx(int uartPort) noexcept } } -// Diagnostic only: some BT1035 firmware prints an unsolicited boot banner on -// UART right after the hardware RESET# pulse. Capturing it (or its absence) -// tells us whether the UART link is electrically alive independent of the -// AT command layer. +// Diagnostic only, run once if all kBootAttempts fail at 115200 (the +// datasheet's own default). AT+BAUD persists across RESET#/SYS_CTRL power +// cycles (programming guide §5.1.3), so a stray manual AT+BAUD or +// AT+RESTORE sent during earlier interactive testing could have left the +// module listening at a different rate than our fixed assumption — this +// sweep tells us if that's what's happening instead of guessing. +constexpr std::array kBaudProbeCandidates = { + 9600, 19200, 38400, 57600, 115200, 230400, 460800, 921600}; + +void probeBaudRates(int uartPort) noexcept +{ + ESP_LOGW(kTag, "115200 unresponsive after %d attempts — sweeping baud rates", + kBootAttempts); + for (const int baud : kBaudProbeCandidates) { + if (uart_set_baudrate(static_cast(uartPort), baud) + != ESP_OK) { + continue; + } + flushUartRx(uartPort); + uart_write_bytes(static_cast(uartPort), "AT\r\n", 4); + + std::array buf{}; + const int n = uart_read_bytes(static_cast(uartPort), + buf.data(), buf.size(), + pdMS_TO_TICKS(500)); + if (n > 0) { + ESP_LOGW(kTag, "baud probe: module responded at %d baud (%d bytes)", + baud, n); + ESP_LOG_BUFFER_HEX(kTag, buf.data(), static_cast(n)); + } else { + ESP_LOGI(kTag, "baud probe: silent at %d baud", baud); + } + } + uart_set_baudrate(static_cast(uartPort), kBaudRate); + flushUartRx(uartPort); +} + +// The BT1035 prints an unsolicited boot banner (+VER=..., +DEVSTAT=1, ...) +// once its full Bluetooth stack finishes initialising — this blocks for up +// to kBootBannerWaitMs waiting for it, since that's the real boot-complete +// signal (the module is otherwise silent and won't answer AT commands +// until this appears). void logRawUartBoot(int uartPort) noexcept { std::array buf{}; const int n = uart_read_bytes(static_cast(uartPort), buf.data(), - buf.size(), pdMS_TO_TICKS(3500)); + buf.size(), pdMS_TO_TICKS(kBootBannerWaitMs)); if (n <= 0) { ESP_LOGW("Bt1035", "no spontaneous UART bytes after hardware reset"); return; @@ -904,32 +959,68 @@ std::expected Bt1035Driver::runInitSequence() return {}; } +std::expected Bt1035Driver::resetAndInitOnce() +{ + // Feasycom BT1035 programming user guide §2.2, pin 34 SYS_CTRL: + // "Delay 100ms, pull high" — the datasheet's own OFF-state timing spec + // (§4.7) says SYS_CTRL must be asserted >20ms before the internal + // regulators start powering up at all, so pulling it high with no + // lead-in delay (the previous sequence here) races the chip's own + // power-on requirement. Held low with RESET already asserted, then a + // 100ms lead-in exactly matching the guide, then SYS_CTRL high, then + // extra settle time before releasing RESET into a chip that's had a + // chance to actually power up first. + const auto sysCtlPin = static_cast(pins_.sysCtlGpio); + const auto resetPin = static_cast(pins_.resetGpio); + + gpio_set_level(sysCtlPin, 0); + gpio_set_level(resetPin, 0); + vTaskDelay(pdMS_TO_TICKS(kSysCtlLeadInMs)); + ESP_LOGI(kTag, "pre-power: SYS_CTRL=%d RESET=%d (want 0,0)", + gpio_get_level(sysCtlPin), gpio_get_level(resetPin)); + + gpio_set_level(sysCtlPin, 1); + vTaskDelay(pdMS_TO_TICKS(kSysCtlSettleMs)); + ESP_LOGI(kTag, "post-syscl: SYS_CTRL=%d RESET=%d (want 1,0)", + gpio_get_level(sysCtlPin), gpio_get_level(resetPin)); + + gpio_set_level(resetPin, 1); + vTaskDelay(pdMS_TO_TICKS(kPostResetMs)); + ESP_LOGI(kTag, "post-reset: SYS_CTRL=%d RESET=%d (want 1,1)", + gpio_get_level(sysCtlPin), gpio_get_level(resetPin)); + + logRawUartBoot(uartPort_); + uart_flush_input(static_cast(uartPort_)); + vTaskDelay(pdMS_TO_TICKS(kPostUartMs)); + + return runInitSequence(); +} + std::expected Bt1035Driver::boot() { if (booted_) { return {}; } + // GPIO_MODE_INPUT_OUTPUT (not plain GPIO_MODE_OUTPUT): gpio_config() + // only enables the pad's input buffer when the INPUT bit is set, so a + // pure-output config leaves gpio_get_level() reading a stale/always-0 + // register instead of the real driven level — needed for the readback + // diagnostic below to be meaningful. gpio_config_t resetCfg = {}; resetCfg.pin_bit_mask = 1ULL << pins_.resetGpio; - resetCfg.mode = GPIO_MODE_OUTPUT; + resetCfg.mode = GPIO_MODE_INPUT_OUTPUT; if (gpio_config(&resetCfg) != ESP_OK) { return std::unexpected(Bt1035Error::ResetFailed); } gpio_config_t sysCfg = {}; sysCfg.pin_bit_mask = 1ULL << pins_.sysCtlGpio; - sysCfg.mode = GPIO_MODE_OUTPUT; + sysCfg.mode = GPIO_MODE_INPUT_OUTPUT; if (gpio_config(&sysCfg) != ESP_OK) { return std::unexpected(Bt1035Error::ResetFailed); } - gpio_set_level(static_cast(pins_.sysCtlGpio), 1); - gpio_set_level(static_cast(pins_.resetGpio), 0); - vTaskDelay(pdMS_TO_TICKS(100)); - gpio_set_level(static_cast(pins_.resetGpio), 1); - vTaskDelay(pdMS_TO_TICKS(kPostResetMs)); - if (!uartInstalled_) { const uart_config_t uartCfg = { .baud_rate = kBaudRate, @@ -961,12 +1052,20 @@ std::expected Bt1035Driver::boot() uartInstalled_ = true; } - logRawUartBoot(uartPort_); - uart_flush_input(static_cast(uartPort_)); - vTaskDelay(pdMS_TO_TICKS(kPostUartMs)); - - if (auto init = runInitSequence(); !init) { - ESP_LOGE(kTag, "AT init failed"); + std::expected init = std::unexpected(Bt1035Error::UnexpectedResponse); + for (int attempt = 1; attempt <= kBootAttempts; ++attempt) { + init = resetAndInitOnce(); + if (init) { + break; + } + ESP_LOGW(kTag, "boot attempt %d/%d failed", attempt, kBootAttempts); + if (attempt < kBootAttempts) { + vTaskDelay(pdMS_TO_TICKS(kBootRetryDelayMs)); + } + } + if (!init) { + ESP_LOGE(kTag, "AT init failed after %d attempts", kBootAttempts); + probeBaudRates(uartPort_); return init; } diff --git a/Software/components/drivers/eeprom24aa/CMakeLists.txt b/Software/components/drivers/eeprom24aa/CMakeLists.txt index 9357eb2..3c2f946 100644 --- a/Software/components/drivers/eeprom24aa/CMakeLists.txt +++ b/Software/components/drivers/eeprom24aa/CMakeLists.txt @@ -1,7 +1,7 @@ idf_component_register( SRCS "src/Eeprom24aa.cpp" INCLUDE_DIRS "include" - REQUIRES core driver esp_driver_i2c + REQUIRES core driver esp_driver_i2c freertos ) target_compile_features(${COMPONENT_LIB} PUBLIC cxx_std_23) diff --git a/Software/components/drivers/eeprom24aa/include/eeprom24aa/Eeprom24aa.hpp b/Software/components/drivers/eeprom24aa/include/eeprom24aa/Eeprom24aa.hpp index a6bf946..5db36d9 100644 --- a/Software/components/drivers/eeprom24aa/include/eeprom24aa/Eeprom24aa.hpp +++ b/Software/components/drivers/eeprom24aa/include/eeprom24aa/Eeprom24aa.hpp @@ -13,27 +13,44 @@ #pragma once #include "core/IDeviceIdentitySource.hpp" +#include "core/IdentityError.hpp" #include "driver/i2c_master.h" #include +#include +#include namespace eeprom24aa { /** - * @brief Eeprom24aa — reads the factory EUI-48 from Microchip 24AA025E48. + * @brief Eeprom24aa — reads the factory EUI-48 from Microchip 24AA025E48; + * also stores one board-specific calibration byte in the chip's + * user-writable region. * * @dname Eeprom24aa * @return n/a (type) * @pubstate Borrows an existing I2C master bus (shared with ADAU1701). The - * EUI-48 lives at word address 0xFA..0xFF per the 24AA025E48 - * datasheet (DS20001191). + * EUI-48 lives at word address 0xFA..0xFF (read-only, factory + * programmed) per the 24AA025E48 datasheet (DS20001191); the FM + * ANTCAP calibration byte lives at word address 0x00, and the + * DAB ANTCAP calibration byte at word address 0x01, both in the + * remaining user-writable 250 bytes. * * @author Michele Bigi * @date 2026-07-07 */ class Eeprom24aa : public core::IDeviceIdentitySource { public: + /** Valid ANTCAP calibration values (FM or DAB) are 0-128 (AN649/AN851); + * any stored byte above this, including the EEPROM's blank/erased + * 0xFF, reads back as "never calibrated" — no separate sentinel write + * needed for a fresh chip. */ + static constexpr std::uint8_t kFmAntCapMax = 128U; + /** Same range as kFmAntCapMax; kept as a separate name for the DAB + * calibration byte's own doc comments below. */ + static constexpr std::uint8_t kDabAntCapMax = 128U; + /** * @brief Eeprom24aa — bind to a running I2C master bus and 7-bit addr. * @@ -60,6 +77,69 @@ public: [[nodiscard]] std::expected readDeviceIdentity() override; + /** + * @brief readFmAntCap — read the stored FM antenna calibration byte. + * + * @dname readFmAntCap + * @return Calibrated ANTCAP (0-kFmAntCapMax) if one was ever saved via + * writeFmAntCap(), nullopt if the byte is blank/out of range, + * or IdentityError on an I2C failure. + * @pubstate performs one I2C read of one byte at word address 0x00. + * + * @author Michele Bigi + * @date 2026-08-19 + */ + [[nodiscard]] std::expected, core::IdentityError> + readFmAntCap(); + + /** + * @brief writeFmAntCap — persist an FM antenna calibration value. + * + * @dname writeFmAntCap + * @param value ANTCAP to store, 0-kFmAntCapMax (AN851 Appendix A + * calibration procedure; found via a sweep, not + * computed). + * @return Ok on success, or IdentityError::I2cFailed. + * @pubstate performs one I2C byte write at word address 0x00, then + * blocks for the chip's write-cycle time before returning. + * + * @author Michele Bigi + * @date 2026-08-19 + */ + [[nodiscard]] std::expected + writeFmAntCap(std::uint8_t value); + + /** + * @brief readDabAntCap — read the stored DAB antenna calibration byte. + * + * @dname readDabAntCap + * @return Calibrated ANTCAP (0-kDabAntCapMax) if one was ever saved via + * writeDabAntCap(), nullopt if the byte is blank/out of range, + * or IdentityError on an I2C failure. + * @pubstate performs one I2C read of one byte at word address 0x01. + * + * @author Michele Bigi + * @date 2026-08-20 + */ + [[nodiscard]] std::expected, core::IdentityError> + readDabAntCap(); + + /** + * @brief writeDabAntCap — persist a DAB antenna calibration value. + * + * @dname writeDabAntCap + * @param value ANTCAP to store, 0-kDabAntCapMax (AN649 Command + * 0xB0 ARG4; found via a sweep, not computed). + * @return Ok on success, or IdentityError::I2cFailed. + * @pubstate performs one I2C byte write at word address 0x01, then + * blocks for the chip's write-cycle time before returning. + * + * @author Michele Bigi + * @date 2026-08-20 + */ + [[nodiscard]] std::expected + writeDabAntCap(std::uint8_t value); + private: i2c_master_bus_handle_t bus_; std::uint8_t addr7_; diff --git a/Software/components/drivers/eeprom24aa/src/Eeprom24aa.cpp b/Software/components/drivers/eeprom24aa/src/Eeprom24aa.cpp index 019c455..600e011 100644 --- a/Software/components/drivers/eeprom24aa/src/Eeprom24aa.cpp +++ b/Software/components/drivers/eeprom24aa/src/Eeprom24aa.cpp @@ -15,6 +15,8 @@ #include "driver/i2c_master.h" #include "esp_log.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" #include @@ -25,7 +27,15 @@ namespace { constexpr char kTag[] = "Eeprom24aa"; /** EUI-48 word address per Microchip 24AA025E48 datasheet (DS20001191). */ constexpr std::uint8_t kEui48WordAddress = 0xFAU; +/** FM ANTCAP calibration byte, in the chip's user-writable region (anywhere + * below the factory-locked 0xFA-0xFF EUI-48 block). */ +constexpr std::uint8_t kFmAntCapWordAddress = 0x00U; +/** DAB ANTCAP calibration byte, next word address after the FM byte. */ +constexpr std::uint8_t kDabAntCapWordAddress = 0x01U; constexpr int kI2cTimeoutMs = 100; +/** DS20001191 §"Page Write"/"Byte Write": max write cycle time after STOP + * before the chip acknowledges further I2C traffic. */ +constexpr int kI2cWriteCycleMs = 5; } // namespace @@ -69,4 +79,146 @@ Eeprom24aa::readDeviceIdentity() return core::DeviceIdentity::fromEui48(core::Eui48::fromBytes(payload)); } +std::expected, core::IdentityError> +Eeprom24aa::readFmAntCap() +{ + if (bus_ == nullptr) { + return std::unexpected(core::IdentityError::I2cFailed); + } + + i2c_device_config_t devCfg = {}; + devCfg.dev_addr_length = I2C_ADDR_BIT_LEN_7; + devCfg.device_address = addr7_; + devCfg.scl_speed_hz = 100000; + + i2c_master_dev_handle_t dev = nullptr; + if (i2c_master_bus_add_device(bus_, &devCfg, &dev) != ESP_OK) { + ESP_LOGW(kTag, "i2c_master_bus_add_device failed"); + return std::unexpected(core::IdentityError::I2cFailed); + } + + const std::uint8_t wordAddress = kFmAntCapWordAddress; + std::uint8_t value = 0xFFU; + const esp_err_t err = i2c_master_transmit_receive( + dev, &wordAddress, 1U, &value, 1U, kI2cTimeoutMs); + + i2c_master_bus_rm_device(dev); + + if (err != ESP_OK) { + ESP_LOGW(kTag, "FM ANTCAP read failed (err=0x%x)", + static_cast(err)); + return std::unexpected(core::IdentityError::ReadFailed); + } + + if (value > kFmAntCapMax) { + return std::optional{}; + } + return std::optional{value}; +} + +std::expected +Eeprom24aa::writeFmAntCap(std::uint8_t value) +{ + if (bus_ == nullptr) { + return std::unexpected(core::IdentityError::I2cFailed); + } + + i2c_device_config_t devCfg = {}; + devCfg.dev_addr_length = I2C_ADDR_BIT_LEN_7; + devCfg.device_address = addr7_; + devCfg.scl_speed_hz = 100000; + + i2c_master_dev_handle_t dev = nullptr; + if (i2c_master_bus_add_device(bus_, &devCfg, &dev) != ESP_OK) { + ESP_LOGW(kTag, "i2c_master_bus_add_device failed"); + return std::unexpected(core::IdentityError::I2cFailed); + } + + const std::array payload = {kFmAntCapWordAddress, value}; + const esp_err_t err = + i2c_master_transmit(dev, payload.data(), payload.size(), kI2cTimeoutMs); + + i2c_master_bus_rm_device(dev); + + if (err != ESP_OK) { + ESP_LOGW(kTag, "FM ANTCAP write failed (err=0x%x)", + static_cast(err)); + return std::unexpected(core::IdentityError::I2cFailed); + } + + vTaskDelay(pdMS_TO_TICKS(kI2cWriteCycleMs)); + return {}; +} + +std::expected, core::IdentityError> +Eeprom24aa::readDabAntCap() +{ + if (bus_ == nullptr) { + return std::unexpected(core::IdentityError::I2cFailed); + } + + i2c_device_config_t devCfg = {}; + devCfg.dev_addr_length = I2C_ADDR_BIT_LEN_7; + devCfg.device_address = addr7_; + devCfg.scl_speed_hz = 100000; + + i2c_master_dev_handle_t dev = nullptr; + if (i2c_master_bus_add_device(bus_, &devCfg, &dev) != ESP_OK) { + ESP_LOGW(kTag, "i2c_master_bus_add_device failed"); + return std::unexpected(core::IdentityError::I2cFailed); + } + + const std::uint8_t wordAddress = kDabAntCapWordAddress; + std::uint8_t value = 0xFFU; + const esp_err_t err = i2c_master_transmit_receive( + dev, &wordAddress, 1U, &value, 1U, kI2cTimeoutMs); + + i2c_master_bus_rm_device(dev); + + if (err != ESP_OK) { + ESP_LOGW(kTag, "DAB ANTCAP read failed (err=0x%x)", + static_cast(err)); + return std::unexpected(core::IdentityError::ReadFailed); + } + + if (value > kDabAntCapMax) { + return std::optional{}; + } + return std::optional{value}; +} + +std::expected +Eeprom24aa::writeDabAntCap(std::uint8_t value) +{ + if (bus_ == nullptr) { + return std::unexpected(core::IdentityError::I2cFailed); + } + + i2c_device_config_t devCfg = {}; + devCfg.dev_addr_length = I2C_ADDR_BIT_LEN_7; + devCfg.device_address = addr7_; + devCfg.scl_speed_hz = 100000; + + i2c_master_dev_handle_t dev = nullptr; + if (i2c_master_bus_add_device(bus_, &devCfg, &dev) != ESP_OK) { + ESP_LOGW(kTag, "i2c_master_bus_add_device failed"); + return std::unexpected(core::IdentityError::I2cFailed); + } + + const std::array payload = {kDabAntCapWordAddress, value}; + const esp_err_t err = + i2c_master_transmit(dev, payload.data(), payload.size(), kI2cTimeoutMs); + + i2c_master_bus_rm_device(dev); + + if (err != ESP_OK) { + ESP_LOGW(kTag, "DAB ANTCAP write failed (err=0x%x)", + static_cast(err)); + return std::unexpected(core::IdentityError::I2cFailed); + } + + vTaskDelay(pdMS_TO_TICKS(kI2cWriteCycleMs)); + return {}; +} + } // namespace eeprom24aa diff --git a/Software/components/drivers/si4684/include/si4684/Si4684Driver.hpp b/Software/components/drivers/si4684/include/si4684/Si4684Driver.hpp index 7966702..91e00f2 100644 --- a/Software/components/drivers/si4684/include/si4684/Si4684Driver.hpp +++ b/Software/components/drivers/si4684/include/si4684/Si4684Driver.hpp @@ -288,13 +288,18 @@ public: * * @dname tuneDab * @param freqIndex Ensemble index 0–37. + * @param antCap ANTCAP[7:0] override (0-128, AN649 Command 0xB0 + * ARG4). 0 = automatic front-end tuning; other + * values force a specific varactor setting, for + * antenna calibration sweeps (mirrors tuneFm). * @return Ok on success, or Si4684Error. * @pubstate sends DAB_TUNE_FREQ and waits for STC. * * @author Michele Bigi * @date 2026-07-06 */ - [[nodiscard]] std::expected tuneDab(std::uint8_t freqIndex); + [[nodiscard]] std::expected tuneDab( + std::uint8_t freqIndex, std::uint8_t antCap = 0U); /** * @brief readDabDigRadStatus — read ensemble lock metrics. diff --git a/Software/components/drivers/si4684/include/si4684/Si4684Tuner.hpp b/Software/components/drivers/si4684/include/si4684/Si4684Tuner.hpp index 2342037..a11be0c 100644 --- a/Software/components/drivers/si4684/include/si4684/Si4684Tuner.hpp +++ b/Software/components/drivers/si4684/include/si4684/Si4684Tuner.hpp @@ -96,6 +96,7 @@ public: * * @dname tuneDab * @param freqIndex Ensemble index 0–37. + * @param antCap Forwarded to Si4684Driver::tuneDab (0 = auto). * @return Ok on success, or a mapped TunerError. * @pubstate writes dabIndex_ on success. * @@ -103,13 +104,14 @@ public: * @date 2026-07-06 */ [[nodiscard]] std::expected tuneDab( - std::uint8_t freqIndex) override; + std::uint8_t freqIndex, std::uint8_t antCap = 0U) override; /** * @brief tuneFm — tune to an FM centre frequency in kHz. * * @dname tuneFm * @param frequency Validated FM centre frequency. + * @param antCap Forwarded to Si4684Driver::tuneFm (0 = auto). * @return Ok on success, or a mapped TunerError. * @pubstate writes fmFrequency_ on success. * @@ -117,7 +119,7 @@ public: * @date 2026-07-06 */ [[nodiscard]] std::expected tuneFm( - core::FrequencyKHz frequency) override; + core::FrequencyKHz frequency, std::uint8_t antCap = 0U) override; /** * @brief seekFm — seek FM with band wrap. diff --git a/Software/components/drivers/si4684/src/Si4684Driver.cpp b/Software/components/drivers/si4684/src/Si4684Driver.cpp index ebfd622..27f41e1 100644 --- a/Software/components/drivers/si4684/src/Si4684Driver.cpp +++ b/Software/components/drivers/si4684/src/Si4684Driver.cpp @@ -85,6 +85,7 @@ constexpr std::uint16_t kPropDabTuneFeCfg = 0x1712U; constexpr std::uint16_t kPropFmTuneFeCfg = 0x1712U; constexpr std::uint16_t kPropDabXpadEnable = 0xB400U; constexpr std::uint16_t kPropDigitalServiceIntSource = 0x8100U; +constexpr std::uint16_t kPropDabEventIntSource = 0xB300U; /** AN649 INT_CTL_ENABLE / INT_CTL_REPEAT — route STC to INTB until STCACK. */ constexpr std::uint16_t kPropIntCtlEnable = 0x0000U; constexpr std::uint16_t kPropIntCtlRepeat = 0x0001U; @@ -504,6 +505,15 @@ std::expected Si4684Driver::configureAfterBoot( ESP_LOGW(kTag, "DIGITAL_SERVICE_INT_SOURCE (0x8100) failed"); return dsrv; } + // AN649 Property 0xB300 DAB_EVENT_INTERRUPT_SOURCE, bit0=SRVLIST_INTEN + // (default 0x0000 = disabled at power-on). Without this, nothing in + // this driver ever enables the service-list-ready event, so + // fetchDabServiceList() can stay gated behind an eternally-false + // serviceListReady even on a clean, well-locked ensemble. + if (auto evt = setProperty(kPropDabEventIntSource, 0x0001U); !evt) { + ESP_LOGW(kTag, "DAB_EVENT_INTERRUPT_SOURCE (0xB300) failed"); + return evt; + } } else { // FM varactor cal per hitech95/uGreen DTS (not DAB PE5PVB values). static constexpr std::uint16_t kFmFeProps[][2] = { @@ -1027,7 +1037,8 @@ std::expected Si4684Driver::installDefaultDabFrequencyPlan() return sendCommand(cmd); } -std::expected Si4684Driver::tuneDab(std::uint8_t freqIndex) +std::expected Si4684Driver::tuneDab( + std::uint8_t freqIndex, std::uint8_t antCap) { if (auto band = ensureBand(Si4684Band::Dab); !band) { return band; @@ -1037,8 +1048,9 @@ std::expected Si4684Driver::tuneDab(std::uint8_t freqIndex) } // writeCommand() always prepends a fixed ARG1=0x00 (INJECTION=0), so // this array starts at ARG2 (AN649 Command 0xB0 table: ARG2=FREQ_INDEX, - // ARG3=0x00 fixed, ARG4=ANTCAP[7:0], ARG5=ANTCAP[15:8]). - const std::uint8_t args[] = {freqIndex, 0x00U, 0x00U, 0x00U}; + // ARG3=0x00 fixed, ARG4=ANTCAP[7:0], ARG5=ANTCAP[15:8] -- high byte + // always 0, range is 0-128, same as tuneFm's ANTCAP). + const std::uint8_t args[] = {freqIndex, 0x00U, antCap, 0x00U}; if (auto cmd = writeCommand(Command::DabTuneFreq, args, sizeof(args)); !cmd) { return std::unexpected(Si4684Error::TuneFailed); @@ -1121,26 +1133,37 @@ Si4684Driver::fetchDabServiceList() } // raw[5]=RESP4=SIZE[7:0], raw[6]=RESP5=SIZE[15:8] (see readFmRds()). + // AN649 only documents SIZE/DATA_0/DATA_N generically for this command + // and defers the DAB payload layout to a supplemental "Digital + // Services User's Guide" we don't have; the exact field layout below + // is cross-checked against hitech95/si468x_dab_receiver's + // si468x_core_cmd_dab_get_service_list() (drivers/mfd/si468x-cmd.c), + // a real working Linux driver for the same command. That driver also + // establishes that the payload actually carried after SIZE is + // SIZE-2 bytes, not SIZE bytes. const std::uint16_t payloadSize = readLe16(header.data() + 5); - if (payloadSize == 0U || payloadSize + 7U > kSpiBufferSize) { + if (payloadSize <= 2U || payloadSize + 5U > kSpiBufferSize) { return std::unexpected(Si4684Error::ReplyTooShort); } - // DATA_0 (first byte of the AN649 Table 14 "DAB/DMB Digital Service - // List" structure) is RESP6 = body[7]: lead-in(1) + STATUS0-3(4) + - // SIZE(2) = 7 header bytes before it. - std::vector body(payloadSize + 7U, 0U); + // DATA_0 (first byte of the payload) is RESP6 = body[7]: lead-in(1) + + // STATUS0-3(4) + SIZE(2) = 7 header bytes before it. Total frame is + // lead-in(1) + STATUS0-3(4) + SIZE(2) + payload(SIZE-2) = SIZE+5. + std::vector body(payloadSize + 5U, 0U); if (auto rd = readRaw(body); !rd) { return std::unexpected(rd.error()); } - // Table 14: List Size(2) + Version(2) + NumServices(1) + AlignPad(3) = - // 8 bytes, then Service 1 begins. - const std::uint8_t serviceCount = body[11]; + // From DATA_0 (body[7]): Version(2) + NumServices/flags(1) + + // AlignPad(3) = 6 bytes, then Service 1 begins at body[13]. (The + // previous version of this code double-counted the already-consumed + // SIZE field here, offsetting every read by 2 bytes — that is why the + // service list always came back empty.) + const std::uint8_t serviceCount = body[9] & 0x1FU; // max 32 services std::vector services; services.reserve(serviceCount); - std::size_t offset = 15U; + std::size_t offset = 13U; for (std::uint8_t i = 0; i < serviceCount; ++i) { // Fixed per-service part: ServiceID(4) + ServiceInfo1-3(3) + // AlignPad(1) + Label(16) = 24 bytes. @@ -1155,11 +1178,13 @@ Si4684Driver::fetchDabServiceList() entry.label[16] = '\0'; offset += 24U; - // Component ID is 2 bytes (AN649 Table 14); only the first + // Component ID is 2 bytes (hitech95's si468x-cmd.c packs tm_id/ + // sub_ch_id/fidc_id/sc_id into this same field; only the raw + // 16-bit value is exposed on this DTO). Only the first // component's ID is exposed on this DTO. Every component (M = // componentCount) must still be skipped to keep the next service - // entry aligned, each one ComponentID(2) + ComponentInfo(1) + - // ValidFlags(1) = 4 bytes. + // entry aligned, each one 2 bytes packed field + ServiceType/ + // flags(1) + ValidFlags(1) = 4 bytes. if (componentCount > 0U && offset + 2U <= body.size()) { entry.componentId = readLe16(body.data() + offset); } diff --git a/Software/components/drivers/si4684/src/Si4684Tuner.cpp b/Software/components/drivers/si4684/src/Si4684Tuner.cpp index ed2189e..3330599 100644 --- a/Software/components/drivers/si4684/src/Si4684Tuner.cpp +++ b/Software/components/drivers/si4684/src/Si4684Tuner.cpp @@ -209,12 +209,12 @@ std::expected Si4684Tuner::readStatus() } std::expected Si4684Tuner::tuneDab( - std::uint8_t freqIndex) + std::uint8_t freqIndex, std::uint8_t antCap) { if (auto ready = ensureBandLoaded(core::TunerBand::Dab); !ready) { return ready; } - if (auto result = driver_.tuneDab(freqIndex); !result) { + if (auto result = driver_.tuneDab(freqIndex, antCap); !result) { return std::unexpected(mapError(result.error())); } dabIndex_ = freqIndex; @@ -225,12 +225,12 @@ std::expected Si4684Tuner::tuneDab( } std::expected Si4684Tuner::tuneFm( - core::FrequencyKHz frequency) + core::FrequencyKHz frequency, std::uint8_t antCap) { if (auto ready = ensureBandLoaded(core::TunerBand::Fm); !ready) { return ready; } - if (auto result = driver_.tuneFm(frequency); !result) { + if (auto result = driver_.tuneFm(frequency, antCap); !result) { return std::unexpected(mapError(result.error())); } fmFrequency_ = frequency; diff --git a/Software/components/net/include/net/BleProvisioning.hpp b/Software/components/net/include/net/BleProvisioning.hpp index 84a7807..0a3595c 100644 --- a/Software/components/net/include/net/BleProvisioning.hpp +++ b/Software/components/net/include/net/BleProvisioning.hpp @@ -31,15 +31,19 @@ namespace net::ble_provisioning { * @dname start * @param store Secure store the received credentials are saved * to (same store POST /api/wifi writes to). - * @param deviceIdentity Supplies the BLE advertising name (softApSsid) - * and the proof-of-possession string (serialNumber). + * @param deviceIdentity Supplies the BLE advertising name (softApSsid). * @return Ok once provisioning is advertising, or NetError::BleProvisioningFailed. * @pubstate Starts the onboard ESP32-S3 BLE radio (independent of the BT1035 * UART module) and a process-lifetime wifi_provisioning manager * singleton. On a successful join, saves credentials to store and * reboots, mirroring wifiPostHandler's POST /api/wifi behaviour. * Runs alongside the existing SoftAP + HTTP provisioning route, - * not instead of it — either path can complete setup. + * not instead of it — either path can complete setup. Uses + * protocomm Security0 (no proof-of-possession, no encryption): + * a PoP derived from the BLE advertising name would be visible to + * anyone scanning anyway, so it added app/firmware coupling + * without adding real secrecy — same trust level as the open + * SoftAP setup path this runs alongside. * * @author Michele Bigi * @date 2026-08-18 diff --git a/Software/components/net/include/net/NetBootstrap.hpp b/Software/components/net/include/net/NetBootstrap.hpp index b16ad21..2dde756 100644 --- a/Software/components/net/include/net/NetBootstrap.hpp +++ b/Software/components/net/include/net/NetBootstrap.hpp @@ -87,6 +87,8 @@ public: * @param ota Firmware OTA service for POST /api/system/ota. * @param webRadio Streaming config for GET/POST /api/streaming. * @param phoneStream I2S write-through for PUT /api/stream/phone. + * @param antennaCalibration EEPROM write-through for + * POST /api/tuner/calibrate-antenna. * @param companionChips Boot flags exposed on GET /api/health. * @param deviceIdentity EEPROM-derived SSID, hostname, and serial. * @return NetBootstrap on success, or a NetError. @@ -103,6 +105,7 @@ public: ota::OtaService& ota, webradio::WebRadioService& webRadio, PhoneStreamSink& phoneStream, + AntennaCalibration& antennaCalibration, core::CompanionChipStatus companionChips, const core::DeviceIdentity& deviceIdentity); diff --git a/Software/components/net/include/net/SetupWebServer.hpp b/Software/components/net/include/net/SetupWebServer.hpp index ba6de91..7953dc7 100644 --- a/Software/components/net/include/net/SetupWebServer.hpp +++ b/Software/components/net/include/net/SetupWebServer.hpp @@ -85,6 +85,28 @@ struct PhoneStreamSink { std::size_t frameCount); }; +/** + * @brief AntennaCalibration — plain function pointers over EEPROM-backed + * FM and DAB ANTCAP storage, so net/ never includes eeprom24aa + * headers directly; main/ supplies it (HardwareBootstrap owns the + * I2C bus and EEPROM handle). + * + * @dname AntennaCalibration + * @return n/a (type) + * @pubstate Free functions with process lifetime; no per-instance state. + * + * @author Michele Bigi + * @date 2026-08-19 + */ +struct AntennaCalibration { + /** Persist a new FM ANTCAP calibration value to EEPROM. + * @return false on an I2C failure. */ + bool (*save)(std::uint8_t antCap); + /** Persist a new DAB ANTCAP calibration value to EEPROM. + * @return false on an I2C failure. */ + bool (*saveDab)(std::uint8_t antCap); +}; + /** * @brief HttpRouteContext — dependencies injected into HTTP handlers. * @@ -106,6 +128,7 @@ struct HttpRouteContext { ota::OtaService* ota; ///< Firmware OTA streaming. webradio::WebRadioService* webRadio; ///< Streaming config REST routes. PhoneStreamSink* phoneStream; ///< PUT /api/stream/phone I2S write-through. + AntennaCalibration* antennaCalibration; ///< POST /api/tuner/calibrate-antenna. core::CompanionChipStatus companionChips; ///< Boot flags for /api/health. core::DeviceIdentity deviceIdentity; ///< EEPROM-derived unit identity. }; @@ -187,6 +210,8 @@ public: * @param ota Firmware OTA service for POST /api/system/ota. * @param webRadio Streaming config for GET/POST /api/streaming. * @param phoneStream I2S write-through for PUT /api/stream/phone. + * @param antennaCalibration EEPROM write-through for + * POST /api/tuner/calibrate-antenna. * @param companionChips Boot flags for GET /api/health. * @param deviceIdentity Unit identity for /api/health serialNumber. * @return Ok on success, or NetError::HttpServerStartFailed. @@ -204,6 +229,7 @@ public: ota::OtaService& ota, webradio::WebRadioService& webRadio, PhoneStreamSink& phoneStream, + AntennaCalibration& antennaCalibration, core::CompanionChipStatus companionChips, const core::DeviceIdentity& deviceIdentity); diff --git a/Software/components/net/src/BleProvisioning.cpp b/Software/components/net/src/BleProvisioning.cpp index 9552c12..23ae3d9 100644 --- a/Software/components/net/src/BleProvisioning.cpp +++ b/Software/components/net/src/BleProvisioning.cpp @@ -125,10 +125,9 @@ start(core::ISecureStore& store, const core::DeviceIdentity& deviceIdentity) gStore = &store; gPendingCreds.reset(); - // Copies kept for the lifetime of provisioning: wifi_prov_mgr_start_ - // provisioning only borrows these pointers, it does not take ownership. + // Copy kept for the lifetime of provisioning: wifi_prov_mgr_start_ + // provisioning only borrows this pointer, it does not take ownership. static const std::string serviceName(deviceIdentity.softApSsid()); - static const std::string pop(deviceIdentity.serialNumber()); const wifi_prov_mgr_config_t config{ .scheme = wifi_prov_scheme_ble, @@ -141,7 +140,7 @@ start(core::ISecureStore& store, const core::DeviceIdentity& deviceIdentity) return std::unexpected(NetError::BleProvisioningFailed); } - if (wifi_prov_mgr_start_provisioning(WIFI_PROV_SECURITY_1, pop.c_str(), + if (wifi_prov_mgr_start_provisioning(WIFI_PROV_SECURITY_0, nullptr, serviceName.c_str(), nullptr) != ESP_OK) { ESP_LOGE(kTag, "wifi_prov_mgr_start_provisioning failed"); @@ -149,9 +148,8 @@ start(core::ISecureStore& store, const core::DeviceIdentity& deviceIdentity) return std::unexpected(NetError::BleProvisioningFailed); } - ESP_LOGI(kTag, - "BLE provisioning advertising as %s (proof-of-possession: " - "device serial number)", + ESP_LOGI(kTag, "BLE provisioning advertising as %s (no PoP, same trust " + "level as the open SoftAP)", serviceName.c_str()); return {}; } diff --git a/Software/components/net/src/NetBootstrap.cpp b/Software/components/net/src/NetBootstrap.cpp index 8bc6f55..7cdcda1 100644 --- a/Software/components/net/src/NetBootstrap.cpp +++ b/Software/components/net/src/NetBootstrap.cpp @@ -106,6 +106,7 @@ startSetupMode(core::ISecureStore& store, tuner::TunerService& tuner, ota::OtaService& ota, webradio::WebRadioService& webRadio, PhoneStreamSink& phoneStream, + AntennaCalibration& antennaCalibration, core::CompanionChipStatus companionChips, const core::DeviceIdentity& deviceIdentity) { @@ -121,7 +122,8 @@ startSetupMode(core::ISecureStore& store, tuner::TunerService& tuner, if (auto webResult = webServer.start(store, NetState::SoftApSetup, tuner, audio, bluetooth, stations, integration, ota, webRadio, - phoneStream, companionChips, deviceIdentity); + phoneStream, antennaCalibration, companionChips, + deviceIdentity); !webResult) { return std::unexpected(webResult.error()); } @@ -168,6 +170,7 @@ startStaMode(core::ISecureStore& store, tuner::TunerService& tuner, ota::OtaService& ota, webradio::WebRadioService& webRadio, PhoneStreamSink& phoneStream, + AntennaCalibration& antennaCalibration, core::CompanionChipStatus companionChips, const core::DeviceIdentity& deviceIdentity) { @@ -198,7 +201,8 @@ startStaMode(core::ISecureStore& store, tuner::TunerService& tuner, if (auto webResult = webServer.start(store, NetState::StaConnected, tuner, audio, bluetooth, stations, integration, ota, webRadio, - phoneStream, companionChips, deviceIdentity); + phoneStream, antennaCalibration, companionChips, + deviceIdentity); !webResult) { return std::unexpected(webResult.error()); } @@ -227,6 +231,7 @@ NetBootstrap::start(core::ISecureStore& store, tuner::TunerService& tuner, ota::OtaService& ota, webradio::WebRadioService& webRadio, PhoneStreamSink& phoneStream, + AntennaCalibration& antennaCalibration, core::CompanionChipStatus companionChips, const core::DeviceIdentity& deviceIdentity) { @@ -241,7 +246,8 @@ NetBootstrap::start(core::ISecureStore& store, tuner::TunerService& tuner, if (store.hasWifiCredentials()) { auto staResult = startStaMode(store, tuner, audio, bluetooth, stations, integration, ota, webRadio, phoneStream, - companionChips, deviceIdentity); + antennaCalibration, companionChips, + deviceIdentity); if (staResult) { return staResult; } @@ -253,8 +259,8 @@ NetBootstrap::start(core::ISecureStore& store, tuner::TunerService& tuner, } return startSetupMode(store, tuner, audio, bluetooth, stations, integration, - ota, webRadio, phoneStream, companionChips, - deviceIdentity); + ota, webRadio, phoneStream, antennaCalibration, + companionChips, deviceIdentity); } NetBootstrap::NetBootstrap(std::optional softAp, diff --git a/Software/components/net/src/SetupWebServer.cpp b/Software/components/net/src/SetupWebServer.cpp index 6e94902..e7c1547 100644 --- a/Software/components/net/src/SetupWebServer.cpp +++ b/Software/components/net/src/SetupWebServer.cpp @@ -68,7 +68,7 @@ namespace net { namespace { constexpr char kTag[] = "SetupWebServer"; -constexpr char kFirmwareVersion[] = "0.8.5"; +constexpr char kFirmwareVersion[] = "0.9.0"; constexpr unsigned kRebootDelaySec = 3; extern const uint8_t index_html_gz_start[] asm( @@ -99,6 +99,7 @@ extern const uint8_t index_html_gz_end[] asm( .ota = nullptr, .webRadio = nullptr, .phoneStream = nullptr, + .antennaCalibration = nullptr, .companionChips = {}, .deviceIdentity = core::DeviceIdentity::unknown(), }; @@ -429,10 +430,13 @@ esp_err_t tunerTunePostHandler(httpd_req_t* req) std::expected result = std::unexpected( core::TunerError::InvalidInput); + // Omitting antcap uses the board's saved calibration for that band (or + // hardware auto-tune if never calibrated) — only an explicit value in + // the request overrides it, e.g. for a calibration sweep. if (parsed->band == core::TunerBand::Dab) { - result = ctx->tuner->tuneDab(parsed->dabFreqIndex); + result = ctx->tuner->tuneDab(parsed->dabFreqIndex, parsed->antCap); } else if (parsed->fmFrequency) { - result = ctx->tuner->tuneFm(*parsed->fmFrequency); + result = ctx->tuner->tuneFm(*parsed->fmFrequency, parsed->antCap); } if (!result) { @@ -635,6 +639,66 @@ esp_err_t tunerFullScanPostHandler(httpd_req_t* req) return httpd_resp_send(req, json.c_str(), json.size()); } +/** + * @brief tunerCalibrateAntennaPostHandler — save the FM ANTCAP found by + * a calibration sweep as the board's permanent default. + * + * @dname tunerCalibrateAntennaPostHandler + * @param req HTTP request handle from esp_http_server. + * @return ESP_OK on success, or an esp_err_t error code. + * @pubstate writes the 24AA025E48 via route context antenna calibration + * bridge, then updates the live tuner default immediately. + * + * @author Michele Bigi + * @date 2026-08-19 + */ +esp_err_t tunerCalibrateAntennaPostHandler(httpd_req_t* req) +{ + auto* ctx = routeContextFrom(req); + if (ctx == nullptr || ctx->tuner == nullptr + || ctx->antennaCalibration == nullptr) { + httpd_resp_set_status(req, "503 Service Unavailable"); + return httpd_resp_send(req, nullptr, 0); + } + + std::array body{}; + if (!readRequestBody(req, body)) { + httpd_resp_set_status(req, "400 Bad Request"); + return httpd_resp_send(req, nullptr, 0); + } + + const auto parsed = + core::parseAntennaCalibrationJson(std::string_view(body.data())); + if (!parsed) { + const std::string json = core::serializeTunerErrorJson("invalid_json"); + httpd_resp_set_status(req, "400 Bad Request"); + httpd_resp_set_type(req, "application/json"); + return httpd_resp_send(req, json.c_str(), json.size()); + } + + const bool isDab = parsed->band == core::TunerBand::Dab; + const bool saved = isDab ? ctx->antennaCalibration->saveDab(parsed->antCap) + : ctx->antennaCalibration->save(parsed->antCap); + if (!saved) { + const std::string json = core::serializeTunerErrorJson("store_failed"); + httpd_resp_set_status(req, "500 Internal Server Error"); + httpd_resp_set_type(req, "application/json"); + return httpd_resp_send(req, json.c_str(), json.size()); + } + if (isDab) { + ctx->tuner->setDefaultDabAntCap(parsed->antCap); + } else { + ctx->tuner->setDefaultFmAntCap(parsed->antCap); + } + + const std::string json = + std::string("{\"status\":\"saved\",\"band\":\"") + + (isDab ? "dab" : "fm") + "\",\"antcap\":" + + std::to_string(parsed->antCap) + "}"; + httpd_resp_set_type(req, "application/json"); + return httpd_resp_send(req, json.c_str(), json.size()); +} + /** * @brief audioProfileGetHandler — serve GET /api/audio/profile JSON. * @@ -1865,6 +1929,7 @@ std::expected SetupWebServer::start( ota::OtaService& ota, webradio::WebRadioService& webRadio, PhoneStreamSink& phoneStream, + AntennaCalibration& antennaCalibration, core::CompanionChipStatus companionChips, const core::DeviceIdentity& deviceIdentity) { @@ -1889,13 +1954,18 @@ std::expected SetupWebServer::start( routeContext.ota = &ota; routeContext.webRadio = &webRadio; routeContext.phoneStream = &phoneStream; + routeContext.antennaCalibration = &antennaCalibration; routeContext.companionChips = companionChips; routeContext.deviceIdentity = deviceIdentity; httpd_config_t config = HTTPD_DEFAULT_CONFIG(); config.stack_size = 12288; config.max_open_sockets = 3; - config.max_uri_handlers = 40; + config.max_uri_handlers = 56; // 41 routes registered below as of 2026-08-19; + // keep headroom so a silent + // httpd_register_uri_handler failure + // ("no slots left") doesn't quietly drop + // the last-registered route again. config.server_port = 80; config.lru_purge_enable = true; config.recv_wait_timeout = 60; @@ -1997,6 +2067,14 @@ std::expected SetupWebServer::start( }; httpd_register_uri_handler(server_, &tunerFullScanUri); + const httpd_uri_t tunerCalibrateAntennaUri = { + .uri = "/api/tuner/calibrate-antenna", + .method = HTTP_POST, + .handler = tunerCalibrateAntennaPostHandler, + .user_ctx = routeCtx, + }; + httpd_register_uri_handler(server_, &tunerCalibrateAntennaUri); + const httpd_uri_t audioProfileGetUri = { .uri = "/api/audio/profile", .method = HTTP_GET, diff --git a/Software/components/services/tuner/include/tuner/TunerService.hpp b/Software/components/services/tuner/include/tuner/TunerService.hpp index 46e67c6..d716f85 100644 --- a/Software/components/services/tuner/include/tuner/TunerService.hpp +++ b/Software/components/services/tuner/include/tuner/TunerService.hpp @@ -73,6 +73,10 @@ public: * * @dname tuneDab * @param freqIndex Ensemble index 0–37. + * @param antCap Front-end antenna varactor override for this one + * tune (e.g. for a calibration sweep). Omit to use + * defaultDabAntCap_ (the board's saved calibration, + * or hardware auto-tune if never calibrated). * @return Ok on success, or a TunerError from ITuner. * @pubstate writes lastDabIndex_ on success; clears last-played DAB ids. * @@ -80,13 +84,32 @@ public: * @date 2026-07-06 */ [[nodiscard]] std::expected tuneDab( - std::uint8_t freqIndex); + std::uint8_t freqIndex, + std::optional antCap = std::nullopt); + + /** + * @brief setDefaultDabAntCap — set the board's calibrated DAB ANTCAP. + * + * @dname setDefaultDabAntCap + * @param antCap Value applied to every DAB tune that doesn't pass an + * explicit override (0 = chip auto-tune, the factory + * default before any calibration is saved). + * @pubstate writes defaultDabAntCap_. Does not itself re-tune. + * + * @author Michele Bigi + * @date 2026-08-20 + */ + void setDefaultDabAntCap(std::uint8_t antCap) noexcept; /** * @brief tuneFm — tune to an FM centre frequency. * * @dname tuneFm * @param frequency Validated FM centre frequency. + * @param antCap Front-end antenna varactor override for this one + * tune (e.g. for a calibration sweep). Omit to use + * defaultFmAntCap_ (the board's saved calibration, + * or hardware auto-tune if never calibrated). * @return Ok on success, or a TunerError from ITuner. * @pubstate writes lastFmFrequency_ on success. * @@ -94,7 +117,22 @@ public: * @date 2026-07-06 */ [[nodiscard]] std::expected tuneFm( - core::FrequencyKHz frequency); + core::FrequencyKHz frequency, + std::optional antCap = std::nullopt); + + /** + * @brief setDefaultFmAntCap — set the board's calibrated ANTCAP. + * + * @dname setDefaultFmAntCap + * @param antCap Value applied to every FM tune that doesn't pass an + * explicit override (0 = chip auto-tune, the factory + * default before any calibration is saved). + * @pubstate writes defaultFmAntCap_. Does not itself re-tune. + * + * @author Michele Bigi + * @date 2026-08-19 + */ + void setDefaultFmAntCap(std::uint8_t antCap) noexcept; /** * @brief seekFm — seek FM in the given direction. @@ -200,6 +238,8 @@ private: std::uint8_t lastDabIndex_; core::FrequencyKHz lastFmFrequency_; std::uint8_t volume_; + std::uint8_t defaultFmAntCap_; + std::uint8_t defaultDabAntCap_; std::optional lastPlayedServiceId_; std::optional lastPlayedComponentId_; }; diff --git a/Software/components/services/tuner/src/TunerService.cpp b/Software/components/services/tuner/src/TunerService.cpp index b9d0878..f674207 100644 --- a/Software/components/services/tuner/src/TunerService.cpp +++ b/Software/components/services/tuner/src/TunerService.cpp @@ -138,9 +138,21 @@ TunerService::TunerService(core::ITuner& tuner) , lastDabIndex_(0U) , lastFmFrequency_(defaultFmFrequency()) , volume_(40U) + , defaultFmAntCap_(0U) + , defaultDabAntCap_(0U) { } +void TunerService::setDefaultFmAntCap(std::uint8_t antCap) noexcept +{ + defaultFmAntCap_ = antCap; +} + +void TunerService::setDefaultDabAntCap(std::uint8_t antCap) noexcept +{ + defaultDabAntCap_ = antCap; +} + std::expected TunerService::refreshStatus() { auto status = tuner_.readStatus(); @@ -155,9 +167,11 @@ std::expected TunerService::refreshStatus() } std::expected TunerService::tuneDab( - std::uint8_t freqIndex) + std::uint8_t freqIndex, std::optional antCap) { - if (auto result = tuner_.tuneDab(freqIndex); !result) { + if (auto result = + tuner_.tuneDab(freqIndex, antCap.value_or(defaultDabAntCap_)); + !result) { return result; } lastDabIndex_ = freqIndex; @@ -167,9 +181,10 @@ std::expected TunerService::tuneDab( } std::expected TunerService::tuneFm( - core::FrequencyKHz frequency) + core::FrequencyKHz frequency, std::optional antCap) { - if (auto result = tuner_.tuneFm(frequency); !result) { + if (auto result = tuner_.tuneFm(frequency, antCap.value_or(defaultFmAntCap_)); + !result) { return result; } lastFmFrequency_ = frequency; diff --git a/Software/docs/TODO.md b/Software/docs/TODO.md index 8e61cf5..7a30580 100644 --- a/Software/docs/TODO.md +++ b/Software/docs/TODO.md @@ -3,9 +3,13 @@ Agent task list and hardware-in-the-loop backlog. Working directory for all commands is `Software/`. -**Current firmware:** `0.8.5` — BT1035 I2S slave boot init, dual OTA + DSP blob updates, EEPROM identity, -NVS + flash encryption (dev mode), tabbed Web UI with System uploads, CI gate -(4 jobs). +**Current firmware:** `0.9.0` — everything in 0.8.5, plus: Si4684 RF +blackout root-caused and fixed (real FM/DAB lock and audio on real +hardware), DAB service list fixed (two rounds), FM ANTCAP antenna +calibration persisted to EEPROM, generic ADAU1701 parameter API, phone PCM +streaming, BLE Wi-Fi provisioning, full FM band scan, BT1035 boot retry. +See "Post-0.8.5 hardware-in-the-loop findings" below and +`docs/si4684-rf-investigation-report.md` for the full story. **Before writing code, read `AGENTS.md`, `.cursor/rules/`, and `instructions.md`.** Definition of Done: Apache header, doc blocks, @@ -62,10 +66,49 @@ verify ADAU replay after reboot. After H1 passes, trial build with `sdkconfig.defaults.production` overlay on a sacrificial unit; confirm RELEASE mode policy before shipping. -### H5. Si4684 FM/DAB no-lock — blob integrity checked, verdict: hardware -**Verdict (2026-08-13): blob OK → suspect U6 RF ground (re-open PCBWay)**, not -a firmware/blob defect. Full investigation, evidence, and the two byte-offset -bugs found/fixed while verifying this: [`docs/si4684-rf-investigation-report.md`](si4684-rf-investigation-report.md). +### H5. Si4684 FM/DAB no-lock — RESOLVED, was firmware after all +**Superseded verdict (2026-08-13): blob OK → suspected U6 RF ground, PCBWay +dispute opened.** That verdict was wrong. The actual cause was +`writeCommand()`'s ARG1 byte being mis-offset across FM/DAB tune, seek, and +several status/ack commands — the chip always answered correctly, so every +signal pointed at hardware, but it never actually tuned. Fixed; real FM +lock, real DAB ensemble lock, real audio confirmed live on the same board. +No PCB rework was needed. Full investigation, the wrong initial verdict, +and the eventual root cause: [`docs/si4684-rf-investigation-report.md`](si4684-rf-investigation-report.md). + +--- + +## Post-0.8.5 hardware-in-the-loop findings + +The board arrived and testing against it (not just host tests) found real +bugs the host-testable core couldn't catch, since they live in +ESP-IDF-only drivers. Full detail and evidence in +[`docs/si4684-rf-investigation-report.md`](si4684-rf-investigation-report.md). +Short version: + +- Si4684 total RF blackout (H5 above) — firmware bug, fixed. +- Si4684→ADAU1701 digital audio silence — `PIN_CONFIG_ENABLE` mutual + exclusion + `SerialInputRegister` polarity, fixed. +- DAB service list empty/garbled — response-parsing offset bugs (two + rounds) plus `DAB_EVENT_INTERRUPT_SOURCE` (0xB300) never configured, + fixed. +- FM front-end auto-tune measurably suboptimal on this board's actual + matching network — ANTCAP calibration swept and persisted to EEPROM, + `POST /api/tuner/calibrate-antenna`. +- BT1035 total boot silence — root cause found and fixed (2026-08-20): + the module's spontaneous boot banner (`+VER=...`, `+DEVSTAT=1`) doesn't + appear until ~18-24s after RESET# releases (full BT stack init, not + just the internal regulator), but the boot code only waited 3.5s before + cutting power and restarting — so every attempt, in every prior session, + cut power before the module could ever finish booting even once. Power + rails (VBAT_IN/SYS_CTRL/VDD_IO/1.8V_OUT) and TX/RX wiring were all + independently verified correct with a multimeter first — the module and + PCB were never at fault. Fixed by waiting up to 25s for the banner + (`kBootBannerWaitMs`); boot now succeeds on the first attempt. +- **Still open**: intermittent multi-second HTTP unresponsiveness under + load; DAB signal quality still antenna-limited; 24 KB `nvs` partition + may be undersized (`saveProfile()` `store_failed` seen intermittently, + error code never captured). --- diff --git a/Software/docs/manual/ch-api.tex b/Software/docs/manual/ch-api.tex index 1231f26..0a92232 100644 --- a/Software/docs/manual/ch-api.tex +++ b/Software/docs/manual/ch-api.tex @@ -6,7 +6,7 @@ implemented in \texttt{SetupWebServer}. Request bodies are parsed into domain types in the pure core (\texttt{components/core}) before any persistence or driver call. Exact C++ signatures live in the generated Doxygen output under \texttt{docs/api/}; this chapter documents the -wire protocol and behaviour as shipped in firmware~0.8.5. +wire protocol and behaviour as shipped in firmware~0.9.0. \section{Transport and reachability} @@ -36,7 +36,7 @@ Returns a health-check DTO serialised by \begin{drnote}[Response schema] \begin{drcode}[JSON] -{"status":"ok","fw":"0.8.5","serialNumber":"0004A3123456", +{"status":"ok","fw":"0.9.0","serialNumber":"0004A3123456", "chips":{"si4684":true,"adau1701":true,"bt1035":true}} \end{drcode} \begin{itemize} @@ -172,7 +172,13 @@ or {"band":"fm","frequency_khz":101500} \end{drcode} DAB \texttt{freq\_index} is 0--37; FM \texttt{frequency\_khz} is -64\,000--108\,000. +64\,000--108\,000. FM accepts an optional \texttt{antcap} (0--128): +overrides the front-end antenna varactor for this one tune, e.g. while +running a calibration sweep. Omit it (the normal case) to use the board's +saved calibration --- see +\texttt{POST /api/tuner/calibrate-antenna} +(Section~\ref{sec:api-tuner-calibrate-antenna}) --- or hardware auto-tune +if never calibrated. Ignored for \texttt{"dab"}. \end{drnote} On success returns the updated status JSON (same shape as @@ -267,6 +273,33 @@ per-station poll budget ran out. HTTP status: \textbf{200 OK}; \textbf{409} on a hardware/driver failure mid-sweep; \textbf{503} when the tuner service is unavailable. +\apiendpoint{POST}{/api/tuner/calibrate-antenna} +\label{sec:api-tuner-calibrate-antenna} + +Commits an FM ANTCAP value (AN851 Appendix A front-end calibration) as the +board's permanent default, found by sweeping \texttt{antcap} on +\texttt{POST /api/tuner/tune} (Section~\ref{sec:api-tuner-tune}) across +0--128 and comparing \texttt{rssi\_dbuv}/\texttt{snr\_db}. Persists to the +24AA025E48 EEPROM's user-writable region (separate from the factory-locked +EUI-48) and takes effect immediately on the live tuner --- no reboot +required, though it also survives one since \texttt{HardwareBootstrap::boot()} +reloads it at every boot. Once saved, every ordinary FM tune (seek, scan, +station recall, live UI) uses this value automatically instead of the +chip's own front-end auto-tune, unless a request explicitly overrides +\texttt{antcap} for that one call. + +\begin{drnote}[Request schema] +\begin{drcode}[JSON] +{"antcap":102} +\end{drcode} +\texttt{antcap} is required, 0--128. +\end{drnote} + +Success response: \texttt{\{"status":"saved","antcap":102\}}. HTTP status: +\textbf{200 OK}; \textbf{400} for invalid/missing \texttt{antcap}; +\textbf{500} on an EEPROM write failure; \textbf{503} when the tuner +service is unavailable. + \apiendpoint{GET}{/api/audio/profile} \label{sec:api-audio-profile-get} diff --git a/Software/docs/manual/ch-classes.tex b/Software/docs/manual/ch-classes.tex index 524b1dc..afcaf20 100644 --- a/Software/docs/manual/ch-classes.tex +++ b/Software/docs/manual/ch-classes.tex @@ -16,7 +16,7 @@ added in the same change that introduces the class. A tooling check keeps this chapter in step with the code, so it is always current. \end{drnote} -The class reference tracks firmware~0.8.5 on \texttt{main}. Public classes +The class reference tracks firmware~0.9.0 on \texttt{main}. Public classes are grouped by layer: domain core, application services, and hardware drivers. % ------------------------------------------------------------------ diff --git a/Software/docs/manual/ch-intro.tex b/Software/docs/manual/ch-intro.tex index 3274d99..f5a1bf1 100644 --- a/Software/docs/manual/ch-intro.tex +++ b/Software/docs/manual/ch-intro.tex @@ -4,7 +4,7 @@ DigiRadio is an open-source, high-fidelity digital radio receiver. It receives DAB+ and FM broadcasts, processes the audio through a dedicated signal processor, and streams the result over Bluetooth using a -high-resolution codec. Firmware~0.8.5 on \texttt{main} provides encrypted +high-resolution codec. Firmware~0.9.0 on \texttt{main} provides encrypted storage, a tabbed configuration web UI, and the full REST API documented in Chapter~\ref{ch:api}. The whole project --- hardware and firmware --- is released as open source for the maker and audio community to study, diff --git a/Software/docs/manual/manual.tex b/Software/docs/manual/manual.tex index 45bd431..c7157c5 100644 --- a/Software/docs/manual/manual.tex +++ b/Software/docs/manual/manual.tex @@ -30,7 +30,7 @@ \vfill {\color{drInk}\large Michele Bigi\par} \vspace{2mm} -{\color{drGray}Firmware 0.8.5 \quad\textbullet\quad 2026\par} +{\color{drGray}Firmware 0.9.0 \quad\textbullet\quad 2026\par} \vspace{2mm} {\color{drGray}Hardware: CERN-OHL-S v2 \quad\textbullet\quad Firmware: Apache-2.0\par} \vspace{2mm} diff --git a/Software/docs/si4684-rf-investigation-report.md b/Software/docs/si4684-rf-investigation-report.md index 4818d57..997cb90 100644 --- a/Software/docs/si4684-rf-investigation-report.md +++ b/Software/docs/si4684-rf-investigation-report.md @@ -616,19 +616,179 @@ treat BT1035 boot failure as non-fatal rather than halting the whole device, so the rest of the system (Si4684 tuning, web UI, Wi-Fi) remains usable while this is investigated separately. +## 2026-08-19 update: fetchDabServiceList() entry parsing fixed; DAB audio +confirmed, quality traced to signal strength + +Live retest on real hardware found `fetchDabServiceList()`'s *body* parsing +(the third bug flagged as "not yet investigated" above) double-counted the +already-consumed SIZE field: it treated the payload as starting 2 bytes +later than it actually does (`serviceCount` read from `body[11]` instead of +`body[9]`, service entries starting at `body[15]` instead of `body[13]`). +AN649 doesn't actually document the DAB service-list entry layout itself — +it defers to a supplemental "Digital Services User's Guide" this project +doesn't have a copy of — so the exact field layout was re-derived by +cross-checking `hitech95/si468x_dab_receiver`'s +`si468x_core_cmd_dab_get_service_list()` (a working Linux driver for the +same command over the same command set), which also confirmed the payload +carried after SIZE is `SIZE-2` bytes, not `SIZE` bytes (fixed the read +sizing to match). + +Confirmed live immediately after reflashing: `GET /api/tuner/services` on a +locked DAB ensemble (freq_index 5) now returns 22 real, correctly-decoded +Italian DAB station labels (R.M.T., Radio Cuore, GR News, Radio Sportiva, +Lifegate, ...) instead of an empty list. `POST /api/tuner/play` against one +of these real service/component IDs was confirmed audible — crackly/broken +but present, not silence — on a second try after the first selected +service (R.M.T., `cnr_db=7`) produced no audible sound at all. Switching to +GR News (`cnr_db=8`) did produce audible (if degraded) audio. This matches +DAB's two-tier robustness by design: the FIC channel (`fic_quality` 94-98 +throughout) is far more error-protected than the actual audio sub-channel, +so a receiver can report ensemble lock and a clean, complete service list +while individual programme audio is too weak (CNR ~7-8 dB here) to decode +cleanly or at all — the chip's own soft-mute is the most likely explanation +for the first service's total silence, not a firmware defect. This is +consistent with what FM already showed this session ("works, but +badly") and with the still-open antenna/front-end TODO below. + +Also found and fixed, unrelated to the Si4684: `SetupWebServer` was +registering 41 HTTP routes against `httpd_config_t::max_uri_handlers = 40` +— `httpd_register_uri_handler()` fails past the limit with only a generic +"no slots left" warning, no indication of which handler was dropped. The +41st and therefore last-registered route, `POST /api/stations/tune`, was +silently unroutable (404) on every boot since whichever commit first pushed +the route count past 40. Bumped to 56 for headroom. + +## 2026-08-19 update (2): DAB_EVENT_INTERRUPT_SOURCE never configured; +intermittent multi-second HTTP unresponsiveness noted, still open + +Retesting DAB service-list retrieval later the same night found it far less +reliable than the earlier confirmation: `locked:true, fic_quality:97-100` +sometimes took anywhere from ~15s to ~60s to appear after a fresh +`POST /api/tuner/tune` (full Si4684 reboot for the FM->DAB band switch), and +even once locked with excellent FIC quality, `GET /api/tuner/services` kept +returning `service_list_empty` for a further 30-65s. + +Root cause candidate found by re-reading AN649's DAB_GET_EVENT_STATUS +section (command 0xB3) carefully: the SVRLISTINT bit this driver polls via +`readDabEventStatus()` is explicitly documented as gated by **Property +0xB300 DAB_EVENT_INTERRUPT_SOURCE**, bit 0 = SRVLIST_INTEN, **default 0x0000 +(disabled) at power-on** — and this driver never wrote that property +anywhere. `configureAfterBoot()`'s DAB branch already wrote a +similarly-named `DIGITAL_SERVICE_INT_SOURCE` (property 0x8100), but AN649's +own text for 0x8100 is internally inconsistent between its prose ("configures +digital service interrupt sources") and its bit table (VHFCAPS/VHFSW, a +front-end switch config field) — almost certainly a `pdftotext -raw` +extraction artifact merging two adjacent property tables, the same failure +mode noted earlier this session for AN649/adau1701.pdf text extraction. +0x8100 and 0xB300 are two different properties; only 0xB300's own section +(page ~236) reads internally consistent, so it — not 0x8100 — is the one +that gates SVRLISTINT. Added `setProperty(kPropDabEventIntSource=0xB300, +0x0001)` right after the existing 0x8100 write. + +Verified live after reflashing: the service list did come back complete and +correct (all 22 real station labels) on the next test. Not proven +conclusively faster than before — DAB acquisition/list-assembly timing is +inherently variable and this was only tested once post-fix — but the +property write is unambiguously correct per its own AN649 section +regardless, so it stays. + +**Separately, and NOT explained by the above**: the HTTP server went fully +unresponsive (connection timeouts on `/api/health`, the simplest possible +route) for 5-10 second stretches, more than once, both before and after +this fix. The heartbeat log line kept appearing on schedule throughout +(`digiradio: heartbeat` every 5s, confirmed via serial), proving the whole +system did not crash or panic — only the HTTP server (or whatever it was +waiting on, most likely a blocking SPI/CTS wait inside the Si4684 driver +triggered from a DAB status/event read) stalled and then recovered on its +own. This was reproducible independent of the 0xB300 change (first +observed hours earlier, unrelated, during the ANTCAP sweep in the antenna +calibration work). Not investigated further tonight — candidate causes to +check next: whether any Si4684Driver SPI wait loop lacks a bound tight +enough for interactive HTTP use, and whether `httpd_config_t:: +max_open_sockets = 3` (components/net/src/SetupWebServer.cpp) is simply too +small once anything blocks even briefly. + ## TODO (next session) -- **Antenna/front-end calibration, now meaningful.** Before this session's - fixes, any ANTCAP sweep or front-end network experiment was untrustworthy - — a bad result could have been the software bug, not the antenna. Now - that the receiver chain is verified correct end to end (real FM lock, - real DAB lock, real audio), redo the ANTCAP sweep and compare the actual - front-end network (§ "Front-end network component mismatch" above) - against AN851 properly, with results that can actually be trusted. -- Fix `fetchDabServiceList()` entry parsing (garbled service_id/component_id/ - label) against AN649 §7 "Digital Services User's Guide" (~page 418). -- Confirm actual DAB audio playback end to end (blocked on the item above). -- Try a proper FM antenna to see if the residual noise under the music - clears up (suspected antenna quality, not yet confirmed). +- **Antenna/front-end calibration — now the real blocker for DAB/FM audio + quality, not firmware.** Both bands are confirmed working end to end + (real lock, real service list, real audio) but both are signal-limited: + FM "works, but badly" per live listening test, and DAB audio ranges from + crackly to fully soft-muted depending on the service's CNR (~7-8 dB + observed, on the low side). ~~Redo the ANTCAP sweep~~ — **done this + session for FM** (see the ANTCAP antenna calibration feature commit); + antcap=102 saved as the board's default, +6 to +11 dB RSSI/SNR across the + band. ~~DAB doesn't have an equivalent calibrated-default mechanism yet~~ + — **added and swept 2026-08-20, see below; no default saved (auto-tune + already best on the ensembles tested).** +- Try a proper FM/DAB antenna to see how much of the crackle/noise clears + up versus how much is inherent to the current antenna's gain/placement. +- Investigate the intermittent multi-second HTTP unresponsiveness noted + above — reproducible, not yet root-caused, not obviously related to any + single change this session. - BT1035 boot-failure root cause still open (see section above) — non-fatal - now, so it's no longer blocking, but still unexplained. + now, so it's no longer blocking, but still unexplained. **Recurred + 2026-08-20, see below — still open, confirmed not caused by physical + handling.** + +## 2026-08-20 update: DAB ANTCAP override added and swept live; BT1035 "total UART silence" recurred + +**DAB ANTCAP — implemented, built, flashed, swept live via the HTTP API.** +Extended the ANTCAP override (AN649 Command 0x30 ARG4/5 for FM, Command +0xB0 ARG4/5 for DAB) from FM-only to DAB, mirroring the existing FM +mechanism end to end: `ITuner::tuneDab`/`Si4684Driver::tuneDab` gained an +`antCap` parameter (was hardcoded `0x00`/auto); `TunerService` gained +`defaultDabAntCap_`/`setDefaultDabAntCap()`; `Eeprom24aa` gained +`readDabAntCap()`/`writeDabAntCap()` at word address 0x01 (FM stays at +0x00); `HardwareBootstrap` gained `dabAntCapCalibration()`/ +`saveDabAntCapCalibration()`, loaded at boot alongside the FM one; the +`net::AntennaCalibration` bridge gained `saveDab`; both `POST +/api/tuner/tune` (one-shot override, `{"band":"dab","freq_index":N, +"antcap":V}`) and `POST /api/tuner/calibrate-antenna` (persists to EEPROM, +`{"band":"dab","antcap":V}`, `band` defaults to `"fm"` so old clients are +unaffected) now accept DAB. Host build + 20/20 ctest + doxygen + +check-manual-sync all green before flashing. + +Swept live via the API (`freq_index` 0-128 step 8) against three real +ensembles: +- **freq_index 5** (weakest known ensemble, 7 dB CNR baseline from the + 2026-08-16 sweep): did not lock at all this session, at any ANTCAP + including auto — signal currently below threshold, not a code issue + (indices 22/23 locked normally in the same session). +- **freq_index 23** (strongest, 21-26 dB): CNR jittered ±3 dB across the + whole ANTCAP range with no discernible trend — already saturated, sweep + can't discriminate on a signal this strong. +- **freq_index 22** (medium, 16-20 dB): auto (0) and antcap=32 tied for + best (20 dB CNR); antcap=72 and 80 caused total loss of lock (a dead + zone to avoid); the rest of the range gave no systematic gain over auto, + unlike FM's clean +6 to +11 dB improvement. + +**Decision: left DAB on auto-tune, nothing saved to EEPROM.** Unlike FM, +no ANTCAP value tested beat the chip's own auto-tune by a margin worth +trusting. If DAB audio quality is still the limiting factor later, retest +specifically on a weak ensemble (index 5 or similar) once it's receivable +again — ANTCAP calibration matters most on weak signals, which is exactly +the case that wasn't testable this session. + +**BT1035 "total UART silence" recurred — same still-open issue as before, +confirmed (again) not physical.** During the DAB sweep, the board was +reset several times via opening a `pyserial` connection for log capture — +each open triggers a hardware EN/reset pulse on this ESP32-S3 (confirmed: +happens even with `dsrdtr=False, rtscts=False` and explicit +`setDTR(False)`/`setRTS(False)` — this is the USB-native auto-reset +circuit firing on port open, not a pyserial default that can be disabled +from the Mac side). One of these resets left BT1035 silent: `no +spontaneous UART bytes after hardware reset` on both boot attempts (2/2), +then silent across all 8 probed baud rates (9600-921600). This is *not* +the "banner arrives late" issue fixed 2026-08-20 earlier this same session +(`kBootBannerWaitMs = 25000` was already in effect and made no +difference) — it's the harder, total-silence failure mode already logged +above (the "Unrelated finding from the same session" note before the +2026-08-19 entry), recurring. Confirmed again this time that it is not +caused by physical handling: a full physical power-off for 60 s did not +recover it (Si4684/ADAU1701 both came back up fine on the same power +cycle, ruling out a board-wide power issue). Root cause still not +identified. `/api/bluetooth/status` and `/api/bluetooth/paired` correctly +report `{"status":"error","reason":"at_timeout"}` while in this state; the +rest of the device (tuner, web UI) stays usable per the existing +non-fatal-BT1035-boot design. diff --git a/Software/instructions.md b/Software/instructions.md index a3f0af2..ba0a953 100644 --- a/Software/instructions.md +++ b/Software/instructions.md @@ -4,8 +4,8 @@ Read this together with `AGENTS.md` and everything under `.cursor/rules/`. Those define *how* to write code; this file defines *what we are building* and the current state on `main`. -**Firmware on `main`:** **0.8.5** — agent tasks T1–T12 complete; device HIL -pending PCB arrival. +**Firmware on `main`:** **0.9.0** — agent tasks T1–T12 complete; device HIL +now largely done on the first real PCB (see below), not pending anymore. ## What DigiRadio is @@ -53,8 +53,62 @@ Repository: https://github.com/manvalan/DigiRadio | T8 NVS encryption | Done (0.8.3) | `initEncryptedStorage`; HIL when PCB ready | | T9–T12 Platform | Done (0.8.4) | Dual OTA, EEPROM identity, DSP + firmware OTA | -Next work: **hardware-in-the-loop** (`docs/TODO.md` § P4), not new features -unless the user requests them. +## Post-0.8.5 HIL work (first real PCB, not in the table above) + +The board arrived and most of Slice 3–8's HIL assumptions turned out to be +wrong in ways that needed real fixes, not just testing. Summary (full +detail in `docs/si4684-rf-investigation-report.md`): + +- **Si4684 total RF blackout, root-caused and fixed.** `writeCommand()`'s + ARG1 byte was mis-offset across FM/DAB tune, seek, DAB service commands, + and several ARG1-only status/ack commands — the chip answered every + command correctly but never actually tuned. Real FM lock, real DAB + ensemble lock (3+ ensembles), real audio confirmed live. +- **Si4684→ADAU1701 digital audio silence, fixed.** `PIN_CONFIG_ENABLE` + had both I2SOUTEN and DACOUTEN set (chip falls back to unused analog + out per AN649); `SerialInputRegister` IBP polarity was wrong (ADAU + sampling on the wrong BCLK edge). +- **DAB service list, two rounds of offset bugs fixed.** Response-parsing + offsets were wrong in a way that made `GET_DIGITAL_SERVICE_LIST` return + empty/garbled; confirmed live with 22 real, correctly-decoded station + labels. Also found `DAB_EVENT_INTERRUPT_SOURCE` (property 0xB300) was + never configured, so the service-list-ready event could never fire. +- **BT1035 total boot silence, root cause found and fixed (2026-08-20).** + Not a hardware fault: VBAT_IN/SYS_CTRL/VDD_IO/1.8V_OUT and TX/RX wiring + were all independently confirmed correct with a multimeter (SYS_CTRL + and 1.8V_OUT readings pinned down by probing the nearest decoupling cap + instead of the tiny 0.5mm-pitch castellated pad directly, which had + given a false "regulator dead" reading earlier). The actual bug: the + module's spontaneous boot banner (`+VER=...`, `+DEVSTAT=1`) doesn't + appear until ~18-24s after RESET# releases — full BT stack init, not + just the internal regulator powering up. The old code only waited 3.5s + before cutting power and restarting the whole sequence, so across every + prior session the module never once got the chance to finish booting. + Fixed by waiting up to 25s (`kBootBannerWaitMs`) for the banner before + giving up; boot now succeeds on the first attempt, no retries needed. +- **FM front-end calibration.** The board's actual matching network + differs from the AN851 reference the chip's auto-tune constants assume. + Swept ANTCAP (AN851 Appendix A) and found a fixed override that beats + auto-tune by 6–11 dB RSSI/SNR across the whole band; persisted to the + 24AA025E48 EEPROM (`POST /api/tuner/calibrate-antenna`) and applied by + default to every FM tune. +- **New features, not in the original Slice plan:** full FM band scan + (`POST /api/tuner/scan/full`), generic ADAU1701 parameter access + (`GET`/`PUT /api/dsp/param`, an escape hatch onto any SigmaStudio cell + beyond the curated mixer/EQ API), phone PCM streaming + (`PUT /api/stream/phone`), BLE Wi-Fi provisioning + (`net::ble_provisioning`, ESP-IDF's own `wifi_provisioning` over the + ESP32-S3's onboard BLE, additive alongside the SoftAP), web radio + streaming stutter fix (batched I2S writes). +- **Still open**: intermittent multi-second HTTP unresponsiveness under + load (candidate cause: a blocking Si4684 SPI wait colliding with + `max_open_sockets=3`); DAB signal quality still antenna-limited even + after calibration; NVS partition (24 KB) may be undersized given the + accumulated write traffic (`saveProfile()` `store_failed` seen + intermittently, never root-caused). + +Next work: keep chasing the open items above as the user prioritises them, +not new features unless requested. - **Blockers first** — state risks before solutions. - **One vertical slice at a time** — `main` always builds; host tests green. diff --git a/Software/main/CMakeLists.txt b/Software/main/CMakeLists.txt index 73d67d1..8893b97 100644 --- a/Software/main/CMakeLists.txt +++ b/Software/main/CMakeLists.txt @@ -5,6 +5,7 @@ idf_component_register( "web_radio_stream.cpp" "esp32_i2s_sink.cpp" "phone_stream.cpp" + "antenna_calibration.cpp" "$<$:test_firmware.cpp>" "$<$:i2s_sdata_probe.cpp>" INCLUDE_DIRS "." diff --git a/Software/main/antenna_calibration.cpp b/Software/main/antenna_calibration.cpp new file mode 100644 index 0000000..d727ec1 --- /dev/null +++ b/Software/main/antenna_calibration.cpp @@ -0,0 +1,26 @@ +/** + * @file antenna_calibration.cpp + * @brief net::AntennaCalibration implementation over HardwareBootstrap. + * + * DigiRadio firmware — https://github.com/manvalan/DigiRadio + * + * Copyright 2026 Michele Bigi + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "antenna_calibration.hpp" + +#include "hardware_bootstrap.hpp" + +namespace antenna_calibration { + +net::AntennaCalibration& bridge() noexcept +{ + static net::AntennaCalibration instance{ + .save = &hardware::HardwareBootstrap::saveFmAntCapCalibration, + .saveDab = &hardware::HardwareBootstrap::saveDabAntCapCalibration, + }; + return instance; +} + +} // namespace antenna_calibration diff --git a/Software/main/antenna_calibration.hpp b/Software/main/antenna_calibration.hpp new file mode 100644 index 0000000..bdb2b57 --- /dev/null +++ b/Software/main/antenna_calibration.hpp @@ -0,0 +1,28 @@ +/** + * @file antenna_calibration.hpp + * @brief net::AntennaCalibration implementation over HardwareBootstrap. + * + * DigiRadio firmware — https://github.com/manvalan/DigiRadio + * + * Copyright 2026 Michele Bigi + * SPDX-License-Identifier: Apache-2.0 + */ +#pragma once + +#include "net/SetupWebServer.hpp" + +namespace antenna_calibration { + +/** + * @brief bridge — the process-lifetime AntennaCalibration instance. + * + * @dname bridge + * @return Function-pointer table bound to + * HardwareBootstrap::saveFmAntCapCalibration, for + * net::HttpRouteContext::antennaCalibration / + * POST /api/tuner/calibrate-antenna. + * @pubstate none + */ +[[nodiscard]] net::AntennaCalibration& bridge() noexcept; + +} // namespace antenna_calibration diff --git a/Software/main/hardware_bootstrap.cpp b/Software/main/hardware_bootstrap.cpp index 2a94568..87eaa0b 100644 --- a/Software/main/hardware_bootstrap.cpp +++ b/Software/main/hardware_bootstrap.cpp @@ -80,7 +80,22 @@ bt1035::Bt1035Driver gBt1035( }); core::DeviceIdentity gDeviceIdentity = core::DeviceIdentity::unknown(); +std::optional gFmAntCapCalibration; +std::optional gDabAntCapCalibration; bool gReady = false; + +/** + * @brief makeEeprom — construct a transient EEPROM handle onto the + * shared I2C bus, matching the one built inline in boot(). + */ +[[nodiscard]] eeprom24aa::Eeprom24aa makeEeprom() +{ + auto* busHandle = + static_cast(gAdau1701.i2cBusHandle()); + return eeprom24aa::Eeprom24aa( + busHandle, + static_cast(board::pins::Eeprom24aaAddr)); +} } // namespace std::expected HardwareBootstrap::boot() @@ -102,11 +117,7 @@ std::expected HardwareBootstrap::boot() } } - auto* busHandle = - static_cast(gAdau1701.i2cBusHandle()); - eeprom24aa::Eeprom24aa eeprom(busHandle, - static_cast( - board::pins::Eeprom24aaAddr)); + eeprom24aa::Eeprom24aa eeprom = makeEeprom(); if (auto identity = eeprom.readDeviceIdentity(); identity) { gDeviceIdentity = std::move(*identity); ESP_LOGI(kTag, "unit serial %.*s", @@ -117,6 +128,32 @@ std::expected HardwareBootstrap::boot() ESP_LOGW(kTag, "EUI-48 read failed — using fallback identity"); } + if (auto antCap = eeprom.readFmAntCap(); antCap) { + gFmAntCapCalibration = *antCap; + if (gFmAntCapCalibration) { + ESP_LOGI(kTag, "FM ANTCAP calibration loaded: %u", + static_cast(*gFmAntCapCalibration)); + } else { + ESP_LOGI(kTag, "FM ANTCAP not calibrated — using chip auto-tune"); + } + } else { + ESP_LOGW(kTag, "FM ANTCAP calibration read failed — using chip " + "auto-tune"); + } + + if (auto antCap = eeprom.readDabAntCap(); antCap) { + gDabAntCapCalibration = *antCap; + if (gDabAntCapCalibration) { + ESP_LOGI(kTag, "DAB ANTCAP calibration loaded: %u", + static_cast(*gDabAntCapCalibration)); + } else { + ESP_LOGI(kTag, "DAB ANTCAP not calibrated — using chip auto-tune"); + } + } else { + ESP_LOGW(kTag, "DAB ANTCAP calibration read failed — using chip " + "auto-tune"); + } + if (auto audioResult = gAudioService.loadAndApply(); !audioResult) { ESP_LOGW(kTag, "ADAU1701 profile apply failed"); } @@ -175,4 +212,40 @@ const core::DeviceIdentity& HardwareBootstrap::deviceIdentity() noexcept return gDeviceIdentity; } +std::optional HardwareBootstrap::fmAntCapCalibration() noexcept +{ + return gFmAntCapCalibration; +} + +bool HardwareBootstrap::saveFmAntCapCalibration(std::uint8_t antCap) +{ + eeprom24aa::Eeprom24aa eeprom = makeEeprom(); + if (auto written = eeprom.writeFmAntCap(antCap); !written) { + ESP_LOGW(kTag, "FM ANTCAP calibration write failed"); + return false; + } + gFmAntCapCalibration = antCap; + ESP_LOGI(kTag, "FM ANTCAP calibration saved: %u", + static_cast(antCap)); + return true; +} + +std::optional HardwareBootstrap::dabAntCapCalibration() noexcept +{ + return gDabAntCapCalibration; +} + +bool HardwareBootstrap::saveDabAntCapCalibration(std::uint8_t antCap) +{ + eeprom24aa::Eeprom24aa eeprom = makeEeprom(); + if (auto written = eeprom.writeDabAntCap(antCap); !written) { + ESP_LOGW(kTag, "DAB ANTCAP calibration write failed"); + return false; + } + gDabAntCapCalibration = antCap; + ESP_LOGI(kTag, "DAB ANTCAP calibration saved: %u", + static_cast(antCap)); + return true; +} + } // namespace hardware diff --git a/Software/main/hardware_bootstrap.hpp b/Software/main/hardware_bootstrap.hpp index 9b49c09..2405a69 100644 --- a/Software/main/hardware_bootstrap.hpp +++ b/Software/main/hardware_bootstrap.hpp @@ -17,7 +17,9 @@ #include "bt1035/Bt1035Driver.hpp" +#include #include +#include namespace audio { class AudioService; @@ -137,6 +139,64 @@ public: * @date 2026-07-07 */ [[nodiscard]] static const core::DeviceIdentity& deviceIdentity() noexcept; + + /** + * @brief fmAntCapCalibration — saved FM antenna calibration, if any. + * + * @dname fmAntCapCalibration + * @return Calibrated ANTCAP (0-128) read from the 24AA025E48 during + * boot(), or nullopt if never calibrated / the read failed. + * @pubstate reads gFmAntCapCalibration; set once during boot(). + * + * @author Michele Bigi + * @date 2026-08-19 + */ + [[nodiscard]] static std::optional + fmAntCapCalibration() noexcept; + + /** + * @brief saveFmAntCapCalibration — persist a new FM ANTCAP to EEPROM. + * + * @dname saveFmAntCapCalibration + * @param antCap Value found via a calibration sweep (0-128). + * @return true on success, false on an I2C failure. + * @pubstate writes the 24AA025E48 user region and gFmAntCapCalibration. + * Does not itself change any live tuner state — callers must + * also call TunerService::setDefaultFmAntCap() to apply it. + * + * @author Michele Bigi + * @date 2026-08-19 + */ + [[nodiscard]] static bool saveFmAntCapCalibration(std::uint8_t antCap); + + /** + * @brief dabAntCapCalibration — saved DAB antenna calibration, if any. + * + * @dname dabAntCapCalibration + * @return Calibrated ANTCAP (0-128) read from the 24AA025E48 during + * boot(), or nullopt if never calibrated / the read failed. + * @pubstate reads gDabAntCapCalibration; set once during boot(). + * + * @author Michele Bigi + * @date 2026-08-20 + */ + [[nodiscard]] static std::optional + dabAntCapCalibration() noexcept; + + /** + * @brief saveDabAntCapCalibration — persist a new DAB ANTCAP to EEPROM. + * + * @dname saveDabAntCapCalibration + * @param antCap Value found via a calibration sweep (0-128). + * @return true on success, false on an I2C failure. + * @pubstate writes the 24AA025E48 user region and gDabAntCapCalibration. + * Does not itself change any live tuner state — callers must + * also call TunerService::setDefaultDabAntCap() to apply it. + * + * @author Michele Bigi + * @date 2026-08-20 + */ + [[nodiscard]] static bool saveDabAntCapCalibration(std::uint8_t antCap); }; } // namespace hardware diff --git a/Software/main/main.cpp b/Software/main/main.cpp index 3127db1..9cebc32 100644 --- a/Software/main/main.cpp +++ b/Software/main/main.cpp @@ -23,6 +23,7 @@ #include "si4684/Si4684Tuner.hpp" #include "station/StationService.hpp" #include "tuner/TunerService.hpp" +#include "antenna_calibration.hpp" #include "esp32_i2s_sink.hpp" #include "phone_stream.hpp" #include "web_radio_stream.hpp" @@ -179,6 +180,14 @@ extern "C" void app_main() static tuner::TunerService tunerService( hardware::HardwareBootstrap::si4684Tuner()); + if (auto antCap = hardware::HardwareBootstrap::fmAntCapCalibration(); + antCap) { + tunerService.setDefaultFmAntCap(*antCap); + } + if (auto antCap = hardware::HardwareBootstrap::dabAntCapCalibration(); + antCap) { + tunerService.setDefaultDabAntCap(*antCap); + } static station::StationService stationService(store, tunerService); @@ -214,6 +223,7 @@ extern "C" void app_main() otaService, webRadioService, phone_stream::sink(), + antenna_calibration::bridge(), hardware::HardwareBootstrap::companionChipStatus(), hardware::HardwareBootstrap::deviceIdentity()); if (!netResult) { diff --git a/Software/partitions.csv b/Software/partitions.csv index 622266a..295e45b 100644 --- a/Software/partitions.csv +++ b/Software/partitions.csv @@ -3,11 +3,17 @@ # dsp: updatable ADAU1701 program blob (task 3.1). # nvs_keys: NVS encryption keys (secure-store slice). # First flash after this layout: idf.py erase-flash flash (wired), not OTA. +# nvs: bumped 24 KiB -> 64 KiB (2026-08-19) — the small original size was a +# suspected contributor to intermittent NvsAudioProfileStore::saveProfile() +# store_failed under this project's accumulated write traffic (wifi creds, +# station_list, audio_profile_json, last_preset). otadata/nvs_keys/phy_init +# shift forward to make room; they still fit before ota_0's existing 64 KiB +# alignment boundary (0x20000), so ota_0/ota_1/dsp offsets are unchanged. # Name, Type, SubType, Offset, Size, Flags -nvs, data, nvs, 0x9000, 0x6000, -otadata, data, ota, 0xf000, 0x2000, -nvs_keys, data, nvs_keys, 0x11000, 0x1000, -phy_init, data, phy, 0x12000, 0x1000, +nvs, data, nvs, 0x9000, 0x10000, +otadata, data, ota, 0x19000, 0x2000, +nvs_keys, data, nvs_keys, 0x1b000, 0x1000, +phy_init, data, phy, 0x1c000, 0x1000, ota_0, app, ota_0, 0x20000, 0x400000, ota_1, app, ota_1, 0x420000, 0x400000, dsp, data, 0x40, 0x820000, 0x40000, From f6dc9916a48168de11ab415a4d89b2d2dda409d8 Mon Sep 17 00:00:00 2001 From: Michele Bigi Date: Fri, 21 Aug 2026 09:32:07 +0200 Subject: [PATCH 06/12] Add BT1035 background boot retry; document intermittent silence findings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BT1035 boot can fail with total UART silence even after the banner-timing fix, on otherwise-identical, correctly-powered hardware. Confirmed via multimeter (VBAT_IN, 1.8V_OUT, SYS_CTRL/RESET, TX all normal) and by observing the same physical module both succeed and fail across different boot attempts in one session, that this is intermittent, not a dead module — the crystal is sealed inside the module and not inspectable or fixable from our side. Since the fault self-clears on a later attempt, mitigate with an indefinite background retry task: if the initial boot() fails, keep retrying with no artificial delay (each attempt already takes ~25-60s) so a temporary failure becomes a bounded, self-recovering delay instead of requiring a manual power cycle. Documented the full diagnostic session, including the ruled-out theories and a possible future ESP32-S31 (native Bluetooth Classic) migration path, in the RF investigation report. Co-Authored-By: Claude Sonnet 5 --- Software/docs/TODO.md | 32 ++++- .../docs/si4684-rf-investigation-report.md | 116 ++++++++++++++++++ Software/main/hardware_bootstrap.cpp | 73 +++++++++-- 3 files changed, 209 insertions(+), 12 deletions(-) diff --git a/Software/docs/TODO.md b/Software/docs/TODO.md index 7a30580..5c82b0b 100644 --- a/Software/docs/TODO.md +++ b/Software/docs/TODO.md @@ -105,10 +105,40 @@ Short version: independently verified correct with a multimeter first — the module and PCB were never at fault. Fixed by waiting up to 25s for the banner (`kBootBannerWaitMs`); boot now succeeds on the first attempt. +- **BT1035 — a second, harder failure mode confirmed intermittent, not + hardware (2026-08-21).** Distinct from the banner-timing bug above: even + with the 25s wait already in place, boot sometimes still gets zero UART + bytes at all — no banner, no AT response, silent across all 8 probed + baud rates (9600-921600). Root-cause evidence this session: VBAT_IN + (3.3V), 1.8V_OUT (1.8V), SYS_CTRL/RESET (~3.27V, matching the firmware's + own GPIO readback), and BT1035 TX (idle-HIGH ~3.29V, no short/float) all + measured normal with a multimeter. The BT1035's 32 MHz crystal is + integrated inside the sealed Feasycom module (confirmed via the module's + own datasheet block diagram — no external crystal on our schematic), so + it can't be inspected or reworked from our side; a marginal + oscillator-startup margin inside the module is the leading suspect. + **Decisive evidence it's intermittent, not a dead unit**: the exact same + physical module booted cleanly (banner + all AT commands `OK`) on one + attempt and went totally silent on the very next attempt, no physical + changes in between. A replacement module is therefore not a guaranteed + fix — the same defect class could recur on a different unit. Mitigated + (not fixed) by an indefinite background retry task + (`hardware::bt1035RetryTask` in `main/hardware_bootstrap.cpp`): if the + initial `Bt1035Driver::boot()` fails, a background FreeRTOS task keeps + calling `boot()` again with no artificial delay between attempts (each + attempt already takes ~25-60s on its own) until it succeeds, while the + rest of the system (tuner, Wi-Fi, web UI) stays fully usable in the + meantime. Turns a permanent-until-manual-power-cycle failure into a + bounded, self-recovering delay. See + `docs/si4684-rf-investigation-report.md` (2026-08-21 entry) for the full + session narrative, including a UART TX/RX loopback test attempt that was + inconclusive (bridging the ESP32's own TX/RX pins from cold boot caused + an unrelated, reproducible, harmless early-boot hang, not yet explained). - **Still open**: intermittent multi-second HTTP unresponsiveness under load; DAB signal quality still antenna-limited; 24 KB `nvs` partition may be undersized (`saveProfile()` `store_failed` seen intermittently, - error code never captured). + error code never captured); BT1035 intermittent total-silence boot + failures (mitigated via background retry, not root-caused — see above). --- diff --git a/Software/docs/si4684-rf-investigation-report.md b/Software/docs/si4684-rf-investigation-report.md index 997cb90..59bc7ef 100644 --- a/Software/docs/si4684-rf-investigation-report.md +++ b/Software/docs/si4684-rf-investigation-report.md @@ -792,3 +792,119 @@ identified. `/api/bluetooth/status` and `/api/bluetooth/paired` correctly report `{"status":"error","reason":"at_timeout"}` while in this state; the rest of the device (tuner, web UI) stays usable per the existing non-fatal-BT1035-boot design. + +## 2026-08-21 update: BT1035 total silence confirmed intermittent (not +## hardware); background retry mitigation added + +Follow-up session dedicated entirely to the "total UART silence" BT1035 +failure mode above. Summary: **confirmed intermittent on genuinely +identical hardware, root cause narrowed to the module's internal crystal +(not our PCB, not fixable by us), and mitigated (not fixed) with an +indefinite background boot retry.** + +**Diagnostic instrumentation (temporary, added then reverted this +session)**: added `BT1035 AT TX: ` / `BT1035 UART RX RAW: >` / `BT1035 AT RESULT: OK|ERROR|TIMEOUT` logging around +`Bt1035Driver::transmitAndCollect()`, and temporarily dropped +`kBootAttempts` to 1 for single-attempt clarity. This confirmed the +failure signature precisely: `AT` is transmitted, zero bytes ever come +back (``), timeout. Reverted via `git checkout` once the manual +diagnosis was done — not kept in the codebase. + +**Multimeter checks, all normal** (scope-level checks — crystal +oscillation, power-on transient — remain out of reach without an +oscilloscope): +- VBAT_IN: 3.3V (datasheet range 3.0-4.2V) ✓ +- 1.8V_OUT (module's internal regulator): 1.8V ✓ — proves the module's + own power management *is* running, it isn't simply unpowered +- SYS_CTRL / RESET (post-boot): ~3.27V, matching the firmware's own GPIO + readback log (`post-reset: SYS_CTRL=1 RESET=1`) ✓ +- BT1035 TX pin (module side) to GND: 3.29V, idle-HIGH, no short/float/ + reversed polarity ✓ (though idle-HIGH alone doesn't prove the module's + firmware is executing — some pads default HIGH from reset state alone) +- A 10kΩ pull-down the user had added on SYS_CTRL (matching the + datasheet's own recommendation for an undriven pin) was checked and is + not the cause — the ESP32 GPIO drives push-pull and its own readback + confirms it reaches a valid HIGH regardless. + +**Crystal location determined**: the BT1035 datasheet's own block diagram +shows "32MHz Crystal" as an internal block of the QCC3056 die, and the +DigiRadio schematic netlist (`Netlist_Schematic1_2026-08-07.asc`) has no +XTAL_IN/XTAL_OUT pins wired to any external crystal for U11 — confirming +the oscillator is sealed inside the Feasycom module, not on our PCB. This +is why nothing on our side (layout, load caps, our firmware) can affect +it; if the failure really is a marginal oscillator-startup margin, it's a +property of that specific physical module unit (or the part's design +tolerance in general). + +**Decisive evidence of intermittency, not a dead unit**: across repeated +reboots in the same session (physical power-cycles and serial-port-open +resets, which also hard-reset this ESP32-S3's native USB-CDC), the +identical physical module was observed to boot **completely successfully** +at least once — spontaneous banner `+VER=FSC-BT1035,V6.1.1,20240521` + +`+DEVSTAT=1`, then `AT` and `AT+AUXCFG=3` both answered `OK` — and to fail +completely silently on other attempts, with no physical change in between. +This rules out "defective/dead module" as an explanation; ordering a +replacement module is therefore *not* a guaranteed fix, since the same +physical unit demonstrably works when it works. + +**UART loopback test attempt — inconclusive, logged for future +reference.** Tried to isolate ESP32 vs. module by bridging the ESP32-S3's +own GPIO40 (BT1035 UART TX)/GPIO41 (BT1035 UART RX) pins with a jumper +held by hand on the ESP32 module's castellated pads (no series +resistor/test point exists on this net per the schematic netlist — U8.33 +↔ U11.P$14 and U8.34 ↔ U11.P$13 directly, nothing else). Twice +reproducibly, bridging those pins from cold boot caused the ESP32 itself +to hang very early in boot (right after the bootloader's "Disabling RNG +early entropy source" line, before `app_main()` even starts) — harmless +(board recovers fully once the jumper is removed) but unexplained, and it +sidesteps the actual test rather than answering it. Not pursued further +this session given the practical difficulty of hand-holding a wire onto +castellated pads without a proper SMD test hook. If retried: attach the +jumper *after* the ESP32 has already booted past that early stage (there's +a ~25s window before the BT1035 AT command is actually sent) rather than +from a cold boot. + +**Mitigation implemented: indefinite background boot retry.** Since the +module's own internal fault (if that's what it is) isn't something we can +fix, and since it demonstrably self-clears on a later attempt rather than +needing repair, `main/hardware_bootstrap.cpp` now spawns a +`bt1035RetryTask` FreeRTOS task whenever the initial `HardwareBootstrap:: +boot()`'s call to `Bt1035Driver::boot()` fails. The task loops calling +`boot()` again with **no artificial delay** between attempts — each +attempt already blocks for ~25-60s on its own (the banner wait times +`kBootAttempts`, plus an 8-step baud-rate sweep on final failure), so no +extra backoff is needed on top — until it succeeds, at which point it runs +the same post-boot setup (device name, auto-reconnect) the normal success +path does, then exits. The rest of the system (Wi-Fi, tuner, web UI) never +blocks on this and stays fully usable throughout. Verified live: after a +forced failure (2 attempts + baud sweep, ~62s), the retry task started +immediately, the HTTP server and heartbeat came up normally in parallel, +and the retry task began a fresh attempt right away without any pause. + +**Open going forward**: root cause of the intermittent total-silence mode +is still not identified — this session's diagnosis exhausted what's +possible with a multimeter alone. Real progress would need either an +oscilloscope on SYS_CTRL/RESET/crystal across several boots to correlate +success/failure with power-on timing jitter, or a large-N automated +reboot-cycle statistic (attempted this session via a `pyserial` script, +but the ESP32-S3's native USB-CDC re-enumerating on every hardware reset +made a fully unattended multi-cycle script unreliable — a naive read loop +silently produced a false "0/5 success" result once across a reconnect +window). A future attempt at that statistic needs to detect the USB path +disappearing/reappearing and reopen the port, or use a separate +hardware UART-to-USB adapter that doesn't disconnect when the target +resets. + +**Also discussed this session (not implemented, for a future hardware +revision)**: whether a different/newer SoC could eliminate the need for +the external BT1035 module entirely. Confirmed via web search that +Espressif's new **ESP32-S31** (RISC-V, announced April 2026) has +integrated **Bluetooth 5.4 with both LE and Classic (BR/EDR)** support — +unlike the ESP32-S3 used today, which is BLE-only at the silicon level +(confirmed: no Classic BT/A2DP hardware exists on S3, this is not a +firmware limitation). An `ESP32-S31-WROOM-3` module also exists. This +would be a significant main-MCU redesign, not a drop-in swap, and its +ESP-IDF support maturity/availability wasn't independently verified this +session — worth a dedicated evaluation before committing to it for a +future hardware revision. diff --git a/Software/main/hardware_bootstrap.cpp b/Software/main/hardware_bootstrap.cpp index 87eaa0b..c2cbf71 100644 --- a/Software/main/hardware_bootstrap.cpp +++ b/Software/main/hardware_bootstrap.cpp @@ -32,6 +32,8 @@ #include "driver/spi_master.h" #include "esp_log.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" namespace hardware { @@ -96,6 +98,58 @@ bool gReady = false; busHandle, static_cast(board::pins::Eeprom24aaAddr)); } + +/** + * @brief applyBt1035PostBootSetup — device name + auto-reconnect, run + * once after any successful BT1035 boot (first attempt or a + * later background retry). + */ +void applyBt1035PostBootSetup() +{ + if (auto nameResult = gBt1035.setDeviceName(gDeviceIdentity.bluetoothName()); + !nameResult) { + ESP_LOGW(kTag, "BT1035 device name set failed"); + } + if (auto reconnectResult = gBt1035.setAutoReconnect(3U); !reconnectResult) { + ESP_LOGW(kTag, "BT1035 auto-reconnect set failed"); + } +} + +/** + * @brief bt1035RetryTask — keep retrying Bt1035Driver::boot() in the + * background after the initial boot() attempt fails. + * + * @dname bt1035RetryTask + * @pubstate loops gBt1035.boot() with no artificial delay between + * attempts — each call already blocks for tens of seconds + * (kBootBannerWaitMs's banner wait, times kBootAttempts), so no + * extra backoff is added on top. Exits once boot() succeeds. + * + * Why: BT1035 boot failure has been observed to be intermittent on + * identical, correctly-wired, correctly-powered hardware — the same + * physical module has booted successfully and failed silently across + * different attempts in the same session, with the crystal oscillator + * inside the (sealed, non-serviceable) module the leading suspect. Since + * the fault clears on a later attempt rather than needing repair, retrying + * indefinitely in the background turns a permanent-until-manual-reboot + * failure into a bounded, self-recovering delay. + * + * @author Michele Bigi + * @date 2026-08-21 + */ +void bt1035RetryTask(void* /*arg*/) +{ + ESP_LOGW(kTag, "BT1035 background retry started"); + while (true) { + if (auto result = gBt1035.boot(); result) { + applyBt1035PostBootSetup(); + ESP_LOGI(kTag, "BT1035 background retry succeeded"); + break; + } + ESP_LOGW(kTag, "BT1035 background retry attempt failed, trying again"); + } + vTaskDelete(nullptr); +} } // namespace std::expected HardwareBootstrap::boot() @@ -164,18 +218,15 @@ std::expected HardwareBootstrap::boot() } if (auto btResult = gBt1035.boot(); !btResult) { - ESP_LOGE(kTag, "BT1035 boot failed — continuing without Bluetooth"); + ESP_LOGE(kTag, "BT1035 boot failed — continuing without Bluetooth, " + "retrying in background"); + if (xTaskCreate(bt1035RetryTask, "bt1035_retry", 4096, nullptr, 3, + nullptr) + != pdPASS) { + ESP_LOGW(kTag, "BT1035 background retry task create failed"); + } } else { - if (auto nameResult = - gBt1035.setDeviceName(gDeviceIdentity.bluetoothName()); - !nameResult) { - ESP_LOGW(kTag, "BT1035 device name set failed"); - } - - if (auto reconnectResult = gBt1035.setAutoReconnect(3U); - !reconnectResult) { - ESP_LOGW(kTag, "BT1035 auto-reconnect set failed"); - } + applyBt1035PostBootSetup(); } gReady = true; From 3ffc9930cdaa84e578b8106c54ceafa055d8da19 Mon Sep 17 00:00:00 2001 From: Michele Bigi Date: Fri, 21 Aug 2026 17:11:36 +0200 Subject: [PATCH 07/12] Remove intra-boot() BT1035 retry loop; restore validated single-attempt design MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Git archaeology traced the boot sequence back to fd9d4ae (2026-08-15, documented 5/5 clean boots), which removed a redundant AT+RESET and added the boot-banner listen window in a single commit. Comparing that validated design to today's working tree found one real structural deviation: an intra-boot() retry loop (2 attempts, only 300ms between hardware reset pulses) added earlier today, which never existed in the validated baseline. The BT1035 datasheet's own Reset Protection timeout (typically >1.8s) means a second pulse fired only 300ms later may not reach a clean power-off state before repowering. Removed the intra-boot() retry loop entirely (kBootAttempts, kBootRetryDelayMs deleted) — boot() now makes exactly one attempt per call, matching fd9d4ae. Retries remain exclusively at the bt1035RetryTask level (whole clean boot() calls, confirmed live at ~31.8s apart). Banner wait (25s) and GPIO readback left untouched. Documented the full commit-by-commit analysis and live test result (structurally correct, hit-rate inconclusive on this sample) in the RF investigation report and TODO for future sessions. Co-Authored-By: Claude Sonnet 5 --- .../drivers/bt1035/src/Bt1035Driver.cpp | 36 ++++---- Software/docs/TODO.md | 24 +++++ .../docs/si4684-rf-investigation-report.md | 90 +++++++++++++++++++ 3 files changed, 131 insertions(+), 19 deletions(-) diff --git a/Software/components/drivers/bt1035/src/Bt1035Driver.cpp b/Software/components/drivers/bt1035/src/Bt1035Driver.cpp index 1b5e7de..60d8ba7 100644 --- a/Software/components/drivers/bt1035/src/Bt1035Driver.cpp +++ b/Software/components/drivers/bt1035/src/Bt1035Driver.cpp @@ -51,10 +51,20 @@ constexpr int kSysCtlSettleMs = 50; * RESET# releases — full BT stack init, not just the internal regulator. * The previous 3500ms wait here was never enough for the module to say * anything, so every prior boot attempt cut power and restarted before - * the module could finish booting even once. See kBootAttempts below. */ + * the module could finish booting even once. + * + * boot() makes exactly one resetAndInitOnce() attempt per call (matching + * fd9d4ae's validated 5/5-clean-boot design): the BT1035 datasheet's own + * "Reset Protection timeout (typically >1.8s)" means a second + * SYS_CTRL/RESET pulse fired shortly after a failed attempt would not + * reliably reach a clean power-off state before repowering — an internal + * retry loop here risks re-interrupting the module mid bring-up, the + * same class of bug fixed in fd9d4ae (redundant AT+RESET). Retries now + * live one layer up, in hardware::bt1035RetryTask + * (main/hardware_bootstrap.cpp), which only re-invokes a full, clean + * boot() call — never re-pulses the pins faster than a whole boot cycle + * apart. */ constexpr int kBootBannerWaitMs = 25000; -constexpr int kBootAttempts = 2; -constexpr int kBootRetryDelayMs = 300; void flushUartRx(int uartPort) noexcept { @@ -66,7 +76,7 @@ void flushUartRx(int uartPort) noexcept } } -// Diagnostic only, run once if all kBootAttempts fail at 115200 (the +// Diagnostic only, run if the single boot attempt fails at 115200 (the // datasheet's own default). AT+BAUD persists across RESET#/SYS_CTRL power // cycles (programming guide §5.1.3), so a stray manual AT+BAUD or // AT+RESTORE sent during earlier interactive testing could have left the @@ -77,8 +87,7 @@ constexpr std::array kBaudProbeCandidates = { void probeBaudRates(int uartPort) noexcept { - ESP_LOGW(kTag, "115200 unresponsive after %d attempts — sweeping baud rates", - kBootAttempts); + ESP_LOGW(kTag, "115200 unresponsive — sweeping baud rates"); for (const int baud : kBaudProbeCandidates) { if (uart_set_baudrate(static_cast(uartPort), baud) != ESP_OK) { @@ -1052,19 +1061,8 @@ std::expected Bt1035Driver::boot() uartInstalled_ = true; } - std::expected init = std::unexpected(Bt1035Error::UnexpectedResponse); - for (int attempt = 1; attempt <= kBootAttempts; ++attempt) { - init = resetAndInitOnce(); - if (init) { - break; - } - ESP_LOGW(kTag, "boot attempt %d/%d failed", attempt, kBootAttempts); - if (attempt < kBootAttempts) { - vTaskDelay(pdMS_TO_TICKS(kBootRetryDelayMs)); - } - } - if (!init) { - ESP_LOGE(kTag, "AT init failed after %d attempts", kBootAttempts); + if (auto init = resetAndInitOnce(); !init) { + ESP_LOGE(kTag, "AT init failed"); probeBaudRates(uartPort_); return init; } diff --git a/Software/docs/TODO.md b/Software/docs/TODO.md index 5c82b0b..092cb7f 100644 --- a/Software/docs/TODO.md +++ b/Software/docs/TODO.md @@ -134,6 +134,30 @@ Short version: session narrative, including a UART TX/RX loopback test attempt that was inconclusive (bridging the ESP32's own TX/RX pins from cold boot caused an unrelated, reproducible, harmless early-boot hang, not yet explained). +- **BT1035 — git archaeology + minimal patch, follow-up (2026-08-21).** + Traced the full commit history of `Bt1035Driver.cpp` from the last + documented-good boot (`6ca40f1`) through the regression (`6f7b6dd`, a + redundant `AT+RESET`) and its fix (`fd9d4ae`, 5/5 clean boots — removed + the `AT+RESET` and introduced the boot-banner listen at 3500ms in the + same commit). Comparing `fd9d4ae` to this session's working tree found + one real structural difference beyond the justified 25s banner window: + today's earlier commit (`3a58d33`) had added an intra-`boot()` retry + loop (2 attempts, only 300ms between hardware reset pulses) that never + existed in the validated baseline — shorter than the BT1035 datasheet's + own "Reset Protection timeout (typically >1.8s)", so the second pulse + may not have reached a clean power-off state. **Fixed**: removed the + intra-`boot()` retry loop entirely (`kBootAttempts`/`kBootRetryDelayMs` + deleted); `boot()` now makes exactly one attempt per call, matching + `fd9d4ae`. Retries remain exclusively at the `bt1035RetryTask` level + (whole clean `boot()` calls, never re-pulsing pins faster than one full + cycle apart — confirmed live, ~31.8s between attempts). Host tests + (20/20) and firmware build green; flashed and observed live. **Result + inconclusive on hit rate**: a 20-minute post-flash window captured 31 + consecutive silent retry attempts, zero successes — worse than earlier + the same day. The patch is kept because it's structurally correct (only + known deviation from the historically validated design removed), not + because this sample proved a better success rate. Root cause of the + underlying intermittent silence is still open (see entry above). - **Still open**: intermittent multi-second HTTP unresponsiveness under load; DAB signal quality still antenna-limited; 24 KB `nvs` partition may be undersized (`saveProfile()` `store_failed` seen intermittently, diff --git a/Software/docs/si4684-rf-investigation-report.md b/Software/docs/si4684-rf-investigation-report.md index 59bc7ef..8937890 100644 --- a/Software/docs/si4684-rf-investigation-report.md +++ b/Software/docs/si4684-rf-investigation-report.md @@ -908,3 +908,93 @@ would be a significant main-MCU redesign, not a drop-in swap, and its ESP-IDF support maturity/availability wasn't independently verified this session — worth a dedicated evaluation before committing to it for a future hardware revision. + +## 2026-08-21 follow-up: git archaeology on the boot-retry structure; +## minimal patch to restore the validated single-attempt design + +Separate follow-up session, requested specifically to re-derive the +BT1035 boot regression analysis directly from git history rather than +from further live hardware probing, per the project's own house rule +(2026-08-14 postmortem): exhaust the code-path diff against a known-good +commit before floating new hardware theories. + +**Full commit archaeology** (`git log --follow` on +`Bt1035Driver.cpp`): +``` +6ca40f1 "all companion chips ready" — baseline, 0 known bugs +6f7b6dd added a redundant AT+RESET right after the hardware reset pulse +fd9d4ae (2026-08-15) fixed 6f7b6dd in one commit: removed the redundant + AT+RESET AND introduced logRawUartBoot() for the first time, + already at its final 3500ms window (the "1500ms too short" + text in the report/commit message describes an intermediate + value tried live during that debugging session, never itself + committed) — 5/5 clean boots documented after this fix. +3a58d33 (2026-08-20, this project's own earlier commit today) widened + the banner wait 3500ms → 25000ms (real banner measured arriving + up to ~18.5-42s post-reset) AND, in the same commit, introduced + a NEW intra-boot() retry loop (kBootAttempts=2, only + kBootRetryDelayMs=300ms between the two hardware reset pulses) + that did not exist in fd9d4ae's validated design. +``` + +**Finding**: comparing `fd9d4ae` (the last commit with a documented, +validated 5/5 clean-boot run) against the working tree confirmed exactly +three differences, only one of them structural: +1. Banner wait 3500ms → 25000ms — justified by this session's own real + measurements, kept. +2. `GPIO_MODE_OUTPUT` → `GPIO_MODE_INPUT_OUTPUT` on RESET/SYS_CTRL — + purely additive (enables `gpio_get_level()` readback for the + pre-power/post-syscl/post-reset diagnostic logs), electrically + neutral, kept. +3. **A new intra-`boot()` retry loop with only 300ms between the two + hardware reset pulses — this did not exist in the validated baseline.** + The BT1035 datasheet's own "Reset Protection timeout (typically + >1.8s)" (already gathered earlier this session) means a second + SYS_CTRL/RESET pulse fired only 300ms after a failed attempt would not + reliably reach a clean power-off state — risking re-interrupting the + module mid bring-up, the same class of bug 6f7b6dd/fd9d4ae already + dealt with once (redundant AT+RESET). This is the only difference + flagged as a plausible contributor, not asserted as certain. + +Also confirmed via repo-wide search: `AT+RESET` (`Bt1035AtCommand::Reset`) +is referenced only in the unit test, never in production code; no other +task/thread touches the BT1035 UART during its boot window +(`savedSpeakerReconnectTask` only starts after `HardwareBootstrap::boot()` +returns; the new `bt1035RetryTask` calls `boot()` sequentially, never +concurrently). `logRawUartBoot()`'s single `uart_read_bytes()` call and +the following `uart_flush_input()` were confirmed, both by code reading +and by this session's own successful-boot log capture (banner appeared, +then `AT`→`OK` immediately after, no stall), to not swallow or discard +data that `runInitSequence()` would otherwise need — `runInitSequence()` +does its own fresh TX/RX cycle regardless of what the banner-capture step +saw. + +**Minimal patch applied** (user-directed, exact scope agreed before +touching code): removed the intra-`boot()` retry loop entirely — +`boot()` now makes exactly one `resetAndInitOnce()` call per invocation, +structurally identical to `fd9d4ae`. Removed `kBootAttempts` and +`kBootRetryDelayMs` (dead after the loop's removal); `probeBaudRates()`'s +log line adjusted accordingly (no longer references the removed +attempt count). `kBootBannerWaitMs=25000` and the `GPIO_MODE_INPUT_OUTPUT` +readback were explicitly left untouched. Retries now live exclusively one +layer up, in `hardware::bt1035RetryTask` (`main/hardware_bootstrap.cpp`, +added earlier this session), which only re-invokes a full, clean `boot()` +call — never re-pulses the pins faster than one whole boot cycle apart. +Host tests (20/20) and firmware build both green before flashing. + +**Live result after flashing**: structurally the retry cadence is now +clean — confirmed via serial log, each `bt1035RetryTask` iteration is +spaced ~31.8s apart (25s banner wait + ~2s AT timeout + ~4s baud sweep, +no extra gap), matching the intended single-attempt-per-call design +exactly, versus the old back-to-back double-pulse. **However, a 20-minute +monitoring window immediately after flashing captured 31 consecutive +retry attempts, all silent — zero successes**, a worse hit rate in this +specific sample than earlier in the day (which had at least one clean +success among fewer attempts). This neither confirms nor refutes the +Reset-Protection-timing hypothesis on its own — the patch is kept because +it's structurally correct (matches the one historically validated design, +removes the only unexplained difference from it), not because this +sample proves it improved the success rate. The underlying intermittent +root cause (most likely the module's internal, sealed 32MHz crystal +startup margin — see the 2026-08-21 entry above) remains unresolved and +would need an oscilloscope to pin down further. From a91c63fc3177d29a7602c4949e30fdad22a45464 Mon Sep 17 00:00:00 2001 From: Michele Bigi Date: Sat, 22 Aug 2026 13:58:27 +0200 Subject: [PATCH 08/12] Redesign BT1035 RESET#/SYS_CTRL handling, add CTS/RTS diagnostics MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RESET# (pin 8) is no longer driven at all: reconfigured as a floating input relying entirely on the module's own internal pull-up per datasheet §4.8, as a diagnostic test to rule out host-side RESET# drive as a contributor to intermittent boot failures. SYS_CTRL (pin 34) now performs a genuine LOW(2.5s)->HIGH power-cycle on every resetAndInitOnce() call rather than being asserted once ever: previously every later retry from bt1035RetryTask silently reused an already-HIGH SYS_CTRL line without ever actually power-cycling the module. Added read-only diagnostics on the CTS/RTS pins (physically wired, named in board_pins.hpp since their original definition, never configured by any driver code, host flow control disabled) to observe their level around the boot-banner wait, after reviewing a sibling project's PinScope report and re-reading the datasheet's UART flow control and PA_MUTE default-function documentation. Across ~45 minutes of live testing after these changes, zero successful boots were observed - inconclusive on whether this improves anything, but each change is independently correct per the datasheet. Escalated to Feasycom support with the full findings. Documented in the RF investigation report and TODO. Co-Authored-By: Claude Sonnet 5 --- .../bt1035/include/bt1035/Bt1035Driver.hpp | 15 ++- .../drivers/bt1035/src/Bt1035Driver.cpp | 120 +++++++++++++----- Software/docs/TODO.md | 25 ++++ .../docs/si4684-rf-investigation-report.md | 113 +++++++++++++++++ Software/main/hardware_bootstrap.cpp | 2 + 5 files changed, 239 insertions(+), 36 deletions(-) diff --git a/Software/components/drivers/bt1035/include/bt1035/Bt1035Driver.hpp b/Software/components/drivers/bt1035/include/bt1035/Bt1035Driver.hpp index 3d65d06..169852f 100644 --- a/Software/components/drivers/bt1035/include/bt1035/Bt1035Driver.hpp +++ b/Software/components/drivers/bt1035/include/bt1035/Bt1035Driver.hpp @@ -37,8 +37,19 @@ namespace bt1035 { struct Bt1035Pins { int uartTx; ///< ESP32 TX -> module RX. int uartRx; ///< ESP32 RX <- module TX. - int resetGpio; ///< Module RESET (active level per schematic). - int sysCtlGpio; ///< SYS_CTL (optional module enable). + int resetGpio; ///< Module RESET# (pin 8). Read-only: configured as a + ///< floating input (2026-08-22), never driven — relies + ///< entirely on the module's own internal pull-up. + int sysCtlGpio; ///< SYS_CTL (pin 34). Driven HIGH once at boot, then + ///< never touched again for the process lifetime. + int ctsGpio; ///< Host->module UART_CTS (module pin 15). Diagnostic + ///< only (2026-08-22): read-only floating input, never + ///< driven — this driver does not implement hardware + ///< flow control. See boot()'s comment. + int rtsGpio; ///< Module UART_RTS/PIO2 (module pin 16), factory + ///< default function is PA_MUTE, not flow control + ///< (Feasycom programming guide). Diagnostic only + ///< (2026-08-22): read-only floating input. }; /** diff --git a/Software/components/drivers/bt1035/src/Bt1035Driver.cpp b/Software/components/drivers/bt1035/src/Bt1035Driver.cpp index 60d8ba7..6866f7d 100644 --- a/Software/components/drivers/bt1035/src/Bt1035Driver.cpp +++ b/Software/components/drivers/bt1035/src/Bt1035Driver.cpp @@ -36,14 +36,18 @@ constexpr int kBaudRate = 115200; constexpr int kUartRxBuffer = 4096; constexpr int kUartTxBuffer = 256; constexpr int kResponseTimeoutMs = 2000; -constexpr int kPostResetMs = 500; constexpr int kPostUartMs = 100; -/** Feasycom BT1035 programming user guide §2.2 (pin 34 SYS_CTRL): "Delay - * 100ms, pull high". */ -constexpr int kSysCtlLeadInMs = 100; -/** Margin beyond the datasheet's own >20ms SYS_CTRL-assertion-to-power-up - * minimum (§4.7), for regulator/crystal settling before RESET releases. */ -constexpr int kSysCtlSettleMs = 50; +/** Datasheet §4.7: "From the OFF state, SYS_CTRL must be asserted for + * >20 ms to start power up." */ +constexpr int kSysCtlAssertMs = 20; +/** Datasheet §4.8: "Reset Protection timeout (typically greater than + * ~1.8 s) causes the device to power down if VCHG is not present and + * SYS_CTRL is low." Held comfortably longer than that before each + * power-up assert, to guarantee a genuine full power-down rather than a + * pulse too short for the module's own protection timer to act on — see + * resetAndInitOnce()'s comment for why this now runs on every attempt, + * not just the first. */ +constexpr int kSysCtlDeassertMs = 2500; /** Measured live (2026-08-20, power/wiring confirmed sound with a * multimeter — VBAT_IN/SYS_CTRL/1.8V_OUT/VDD_IO all correct, TX/RX pins * verified via continuity): the module's spontaneous boot banner @@ -970,35 +974,47 @@ std::expected Bt1035Driver::runInitSequence() std::expected Bt1035Driver::resetAndInitOnce() { - // Feasycom BT1035 programming user guide §2.2, pin 34 SYS_CTRL: - // "Delay 100ms, pull high" — the datasheet's own OFF-state timing spec - // (§4.7) says SYS_CTRL must be asserted >20ms before the internal - // regulators start powering up at all, so pulling it high with no - // lead-in delay (the previous sequence here) races the chip's own - // power-on requirement. Held low with RESET already asserted, then a - // 100ms lead-in exactly matching the guide, then SYS_CTRL high, then - // extra settle time before releasing RESET into a chip that's had a - // chance to actually power up first. + // RESET# (pin 8) is deliberately never driven by this driver (see + // boot()'s GPIO config below): it's left a floating input, relying + // entirely on the BT1035's own "fixed strong pull-up to VDD_IO" + // (datasheet §4.8), which the datasheet explicitly says means the pin + // "can therefore be left unconnected". Diagnostic test (2026-08-22) to + // check whether the previous external RESET# drive was contributing to + // the intermittent boot failures. + // + // SYS_CTRL (pin 34) alternative usage (2026-08-22 experiment): drive a + // genuine LOW-then-HIGH cycle every time this function runs, not just + // once ever. With RESET# now Hi-Z, SYS_CTRL is the only pin this + // driver can still use to force a real power-cycle — previously it + // was asserted HIGH exactly once at first boot and never touched + // again, which meant every later retry (hardware::bt1035RetryTask + // calls boot() again on failure) just re-listened on an + // already-asserted line without ever actually power-cycling the + // module. Held LOW for kSysCtlDeassertMs first so the module's own + // Reset Protection timeout actually elapses (a shorter pulse risks + // the module staying "protected" on, per datasheet §4.8, so the + // retry wouldn't be a real fresh power-on at all), then HIGH for the + // datasheet's own >=20ms minimum. const auto sysCtlPin = static_cast(pins_.sysCtlGpio); const auto resetPin = static_cast(pins_.resetGpio); + const auto ctsPin = static_cast(pins_.ctsGpio); + const auto rtsPin = static_cast(pins_.rtsGpio); gpio_set_level(sysCtlPin, 0); - gpio_set_level(resetPin, 0); - vTaskDelay(pdMS_TO_TICKS(kSysCtlLeadInMs)); - ESP_LOGI(kTag, "pre-power: SYS_CTRL=%d RESET=%d (want 0,0)", - gpio_get_level(sysCtlPin), gpio_get_level(resetPin)); - + vTaskDelay(pdMS_TO_TICKS(kSysCtlDeassertMs)); gpio_set_level(sysCtlPin, 1); - vTaskDelay(pdMS_TO_TICKS(kSysCtlSettleMs)); - ESP_LOGI(kTag, "post-syscl: SYS_CTRL=%d RESET=%d (want 1,0)", - gpio_get_level(sysCtlPin), gpio_get_level(resetPin)); - - gpio_set_level(resetPin, 1); - vTaskDelay(pdMS_TO_TICKS(kPostResetMs)); - ESP_LOGI(kTag, "post-reset: SYS_CTRL=%d RESET=%d (want 1,1)", + vTaskDelay(pdMS_TO_TICKS(kSysCtlAssertMs)); + ESP_LOGI(kTag, "power-up: SYS_CTRL=%d (want 1, driven) RESET#=%d " + "(want 1, Hi-Z + internal pull-up, not driven by us)", gpio_get_level(sysCtlPin), gpio_get_level(resetPin)); + ESP_LOGI(kTag, "before banner wait: CTS=%d (module's flow-control " + "input, host side floating) RTS/PIO2=%d (module's " + "PA_MUTE by factory default)", + gpio_get_level(ctsPin), gpio_get_level(rtsPin)); logRawUartBoot(uartPort_); + ESP_LOGI(kTag, "after banner wait: CTS=%d RTS/PIO2=%d", + gpio_get_level(ctsPin), gpio_get_level(rtsPin)); uart_flush_input(static_cast(uartPort_)); vTaskDelay(pdMS_TO_TICKS(kPostUartMs)); @@ -1011,18 +1027,25 @@ std::expected Bt1035Driver::boot() return {}; } - // GPIO_MODE_INPUT_OUTPUT (not plain GPIO_MODE_OUTPUT): gpio_config() - // only enables the pad's input buffer when the INPUT bit is set, so a - // pure-output config leaves gpio_get_level() reading a stale/always-0 - // register instead of the real driven level — needed for the readback - // diagnostic below to be meaningful. + // RESET# (pin 8) is never driven by this driver (2026-08-22 diagnostic + // change): configured as a pure floating input, pull-up/pull-down both + // explicitly disabled, so nothing on the ESP32 side influences this + // net electrically — the BT1035's own internal RESET# pull-up (§4.8) + // is the only thing holding it high. GPIO_MODE_INPUT (not OUTPUT) still + // lets gpio_get_level() read it back for the diagnostic log below, + // without ever driving it. gpio_config_t resetCfg = {}; resetCfg.pin_bit_mask = 1ULL << pins_.resetGpio; - resetCfg.mode = GPIO_MODE_INPUT_OUTPUT; + resetCfg.mode = GPIO_MODE_INPUT; + resetCfg.pull_up_en = GPIO_PULLUP_DISABLE; + resetCfg.pull_down_en = GPIO_PULLDOWN_DISABLE; if (gpio_config(&resetCfg) != ESP_OK) { return std::unexpected(Bt1035Error::ResetFailed); } + // SYS_CTRL (pin 34) stays actively driven (GPIO_MODE_INPUT_OUTPUT: + // the INPUT bit is what makes gpio_get_level() read the real driven + // level instead of a stale register, for the diagnostic log). gpio_config_t sysCfg = {}; sysCfg.pin_bit_mask = 1ULL << pins_.sysCtlGpio; sysCfg.mode = GPIO_MODE_INPUT_OUTPUT; @@ -1030,6 +1053,35 @@ std::expected Bt1035Driver::boot() return std::unexpected(Bt1035Error::ResetFailed); } + // CTS (module pin 15, host->module) and RTS (module pin 16/PIO2, + // factory default function PA_MUTE per the Feasycom programming + // guide) — diagnostic only (2026-08-22): this driver does not + // implement UART hardware flow control (UART_HW_FLOWCTRL_DISABLE + // below), so these are wired but otherwise unused. Configured as + // floating inputs, pull-up/pull-down both disabled, purely to read + // back their level for the diagnostic log — never driven. Exploring + // whether the module's CTS input floating could be gating its own + // UART TX (a common hardware-flow-control behavior), and whether RTS + // toggles at all (would indicate the module's internal firmware is + // alive even when UART TX is silent). + gpio_config_t ctsCfg = {}; + ctsCfg.pin_bit_mask = 1ULL << pins_.ctsGpio; + ctsCfg.mode = GPIO_MODE_INPUT; + ctsCfg.pull_up_en = GPIO_PULLUP_DISABLE; + ctsCfg.pull_down_en = GPIO_PULLDOWN_DISABLE; + if (gpio_config(&ctsCfg) != ESP_OK) { + return std::unexpected(Bt1035Error::ResetFailed); + } + + gpio_config_t rtsCfg = {}; + rtsCfg.pin_bit_mask = 1ULL << pins_.rtsGpio; + rtsCfg.mode = GPIO_MODE_INPUT; + rtsCfg.pull_up_en = GPIO_PULLUP_DISABLE; + rtsCfg.pull_down_en = GPIO_PULLDOWN_DISABLE; + if (gpio_config(&rtsCfg) != ESP_OK) { + return std::unexpected(Bt1035Error::ResetFailed); + } + if (!uartInstalled_) { const uart_config_t uartCfg = { .baud_rate = kBaudRate, diff --git a/Software/docs/TODO.md b/Software/docs/TODO.md index 092cb7f..bb70865 100644 --- a/Software/docs/TODO.md +++ b/Software/docs/TODO.md @@ -158,6 +158,31 @@ Short version: known deviation from the historically validated design removed), not because this sample proved a better success rate. Root cause of the underlying intermittent silence is still open (see entry above). +- **BT1035 — RESET#/SYS_CTRL redesign, CTS/RTS diagnostics, Feasycom + escalation (2026-08-22).** A sibling project's PinScope netlist report + (RESET# pulled to GND, SYS_CTRL pulled HIGH by stray resistors) was + checked against our own schematic and does **not** apply to us — our + RESET#/SYS_CTRL wiring is correct, verified via netlist. Its RF_OUT/pin + 51 floating finding **does** also apply to us, but is a separate, + RF-range-only concern (datasheet documents both internal- and + external-antenna variants; can't tell which we have), not the cause of + the digital/UART boot silence. As a diagnostic test, RESET# is now + never driven at all (floating input, relying on the module's own + internal pull-up per §4.8) and SYS_CTRL now does a genuine LOW(2.5s)→ + HIGH power-cycle on every retry attempt (previously asserted once ever + and left alone, meaning retries never actually power-cycled the + module). Also added read-only diagnostics on the previously-unused + CTS/RTS pins (physically wired, named in `board_pins.hpp`, never + configured by any driver code, host flow control disabled) — live + readings were perfectly stable (CTS=HIGH, RTS=LOW) across ~12 samples + over 30+ minutes, arguing against pure floating-noise. **Across all of + today's changes combined, zero successful boots were observed in + cumulative 45+ minutes of live testing** — inconclusive-to-negative, + not proof any change helped or hurt. Escalated to Feasycom support with + a detailed email (drafted, kept outside the repo) covering the + symptom, everything ruled out, the CTS/RTS open question, and the + antenna-variant question; paused further live experimentation pending + their reply rather than keep permuting timing parameters blind. - **Still open**: intermittent multi-second HTTP unresponsiveness under load; DAB signal quality still antenna-limited; 24 KB `nvs` partition may be undersized (`saveProfile()` `store_failed` seen intermittently, diff --git a/Software/docs/si4684-rf-investigation-report.md b/Software/docs/si4684-rf-investigation-report.md index 8937890..b1527a7 100644 --- a/Software/docs/si4684-rf-investigation-report.md +++ b/Software/docs/si4684-rf-investigation-report.md @@ -998,3 +998,116 @@ sample proves it improved the success rate. The underlying intermittent root cause (most likely the module's internal, sealed 32MHz crystal startup margin — see the 2026-08-21 entry above) remains unresolved and would need an oscilloscope to pin down further. + +## 2026-08-22 update: RESET#/SYS_CTRL redesign, CTS/RTS diagnostics, +## PinScope report from a sibling project, Feasycom support escalation + +**PinScope findings from a sibling "DigiRadio evolution" project with the +same BT1035 wiring pattern** were reviewed for transferability. Checked +each finding against our own schematic netlist (exact BT1035 pin numbers +cross-referenced against the datasheet's own pin table) rather than +assuming they apply: +- **U16-001 (RESET# pulled to GND by a stray R67) and U16-002 (SYS_CTRL + pulled HIGH by a stray R69): do NOT apply to our board.** Verified via + netlist: our RESET# net (`GPIO17`) has only the ESP32 and the BT1035, + no resistor; our SYS_CTRL pull-down (R12) genuinely goes to GND (pins + 1/22, confirmed GND in the datasheet), not a stray pull-up. +- **U16-003 (VCHG/VCHG_SENSE unconnected): same on our board, presumably + intentional** (no USB charging via the BT1035). +- **U16-004 (RF_OUT/pin 51 floating): also true on our board** — but this + is a genuinely open question, not a confirmed defect: the BT1035 + datasheet documents both an "Internal Antenna" (§9.2, on-board antenna, + no RF_OUT routing needed, PCB keep-out area required instead) and + "External Antenna" (§9.3, RF_OUT routed out) layout option, and we + cannot tell from the datasheet alone which variant this specific module + part/order uses. This affects actual Bluetooth RF range once the module + boots — separate from, and does not explain, the intermittent + total-silence boot symptom (RF_OUT is downstream of the digital + baseband processor that generates the boot banner and answers AT + commands). +- The sibling report's "RESET#/SYS_CTRL/VCHG compound badly" warning + does not transfer to us, since our RESET#/SYS_CTRL wiring is correct. + +**RESET#/SYS_CTRL hardware-management redesign (diagnostic test, +requested explicitly to see if external RESET# control was itself +contributing to the intermittent failures):** +- RESET# (pin 8) is no longer driven by this driver at all: reconfigured + as a floating input (`GPIO_MODE_INPUT`, pull-up/pull-down both + explicitly disabled), relying entirely on the BT1035's own datasheet- + documented "fixed strong pull-up to VDD_IO" (§4.8). GPIO17 confirmed + reading HIGH via this internal pull-up, live. +- SYS_CTRL (pin 34) redesigned to perform a genuine LOW→HIGH power-cycle + on *every* `resetAndInitOnce()` call (held LOW 2.5s — comfortably + longer than the datasheet's "~1.8s typical" Reset Protection timeout — + then HIGH for >=20ms per §4.7), rather than being asserted once ever at + first boot and left alone: previously, every later retry from + `bt1035RetryTask` was silently reusing an already-HIGH SYS_CTRL line + and never actually power-cycling the module at all. +- Both changes build/test clean and were confirmed live to behave exactly + as designed (RESET# reads HIGH via internal pull-up; SYS_CTRL cadence + matches the 2.5s+20ms design on every retry). + +**Result: inconclusive-to-negative on hit rate.** Across a cumulative +~45+ minutes of live monitoring after these changes (two separate +sessions, dozens of retry attempts), **zero successful boots were +observed** — no banner, ever, in this window. This is not better than, +and arguably worse than, the small sample seen the same week under the +*previous* (RESET#-driven) design, which did show at least one clean +success among fewer attempts. This should not be read as proof the new +design is wrong — the previous design also produced a 31-attempt/0-success +streak in one session this same week — but it is also not evidence the +redesign helped. Kept anyway because it is independently correct per the +datasheet (RESET# genuinely can be left unconnected; SYS_CTRL retries +should be genuine power-cycles), not because it demonstrably fixed +anything. + +**New CTS/RTS diagnostic instrumentation.** Discovered, previously +unexamined this entire investigation: the board physically wires +`board::pins::Bt1035Cts` (GPIO21 -> BT1035 pin 15, UART_CTS) and +`board::pins::Bt1035Rts` (BT1035 pin 16, UART_RTS -> GPIO14) — both +**named in `board_pins.hpp` since the pin's original definition, but +never configured or used by any driver code**, and the UART is +initialized with `UART_HW_FLOWCTRL_DISABLE`. Added both as read-only +floating-input diagnostics (`Bt1035Pins::ctsGpio`/`rtsGpio`, logged +before and after the banner-wait window in `resetAndInitOnce()`), purely +to observe — never driven. + +Datasheet research (not just speculation) on what this could mean: +- §4.1 Table 4-1 lists flow control as one of several **configurable** + UART settings ("Supports Automatic Flow Control (CTS and RTS lines)"), + not stated as active by default. +- No AT command to explicitly enable/disable flow control was found in + the programming guide. +- Pin 16 (UART_RTS/PIO2)'s documented **factory-default alternate + function is "PA mute pin"** (`AT+MUTEPIO`'s own default parameter is + PIO2) — i.e., out of the box this pin most likely isn't acting as RTS + at all. +- Live readings, across ~12 samples over 30+ minutes and multiple + power-cycles: **CTS=HIGH, RTS=LOW, perfectly stable, zero variation.** + This argues against a genuinely floating/noisy input (which would be + expected to show at least some jitter across dozens of samples) — + something is holding both at a fixed level, whether that's incidental + ESP32 GPIO leakage, an internal pull inside the module, or the module + actively driving its own RTS/PA_MUTE output. We do not yet have a + reading from a *successful* boot to compare against, since none + occurred in this session's remaining test window. + +**Escalated to Feasycom support** (email drafted, not yet sent by the +user) with: the full symptom description, everything ruled out this +session (power rails, RESET#/SYS_CTRL sequencing variants), the CTS/RTS +finding reframed as an open question rather than a confirmed cause (per +the datasheet nuance above), and the RF_OUT/antenna-variant question. +Decided to pause further live hardware experimentation until a reply is +received, rather than keep varying RESET#/SYS_CTRL/timing parameters +without new information — see the email draft (kept outside the repo, in +the session's scratch directory) for the exact wording sent. + +**How to apply, for a future session**: don't re-propose "try removing +RESET# drive" or "try a genuine SYS_CTRL power-cycle on retry" as fresh +ideas — both were tried this session, both are justified independently, +neither showed a measurable improvement in a non-trivial sample. Don't +assume CTS/RTS floating is confirmed as the cause either — the CTS=1/ +RTS=0 stability argues against pure floating-noise, and flow control may +not even be engaged by default per the datasheet. The single most +valuable next input is Feasycom's own answer, not another round of +timing-parameter permutation. diff --git a/Software/main/hardware_bootstrap.cpp b/Software/main/hardware_bootstrap.cpp index c2cbf71..5e11de0 100644 --- a/Software/main/hardware_bootstrap.cpp +++ b/Software/main/hardware_bootstrap.cpp @@ -79,6 +79,8 @@ bt1035::Bt1035Driver gBt1035( .uartRx = board::pins::Bt1035UartRx, .resetGpio = board::pins::Bt1035Reset, .sysCtlGpio = board::pins::Bt1035SysCtl, + .ctsGpio = board::pins::Bt1035Cts, + .rtsGpio = board::pins::Bt1035Rts, }); core::DeviceIdentity gDeviceIdentity = core::DeviceIdentity::unknown(); From fae230516415d4ef77d14c833bc15ec82fb6efa1 Mon Sep 17 00:00:00 2001 From: Michele Bigi Date: Sun, 23 Aug 2026 20:30:42 +0200 Subject: [PATCH 09/12] Fix Si4684 pitch distortion: uncalibrated crystal (CTUN/XTAL_FREQ) Root cause of FM/DAB "stonata" audio: xtalCtun=31/nominal XTAL_FREQ defaults were never measured against this board's actual crystal (Abracon ABM8-19.200MHZ-10-1-U-T, CL=10pF, two external 15pF load caps). Fixed via CTUN trim by ear (31->0) plus a new live calibration loop that reads the Si4684's own FM_RSQ FREQOFF field (broadcast carriers are GPS-locked, so it's a free precision frequency reference) to trim XTAL_FREQ with no lab equipment. Converged to CTUN=0, XTAL_FREQ=19199750 Hz, residual -3/-4 ppm cross-checked on two stations. New tools/infrastructure (kept, not one-off): - Si4684Driver::recalibrateXtal() + POST /api/tuner/xtal-calibrate: live crystal re-trim without an ESP32 reflash. - FM_RSQ FREQOFF exposed as "freqoff_ppm" in GET /api/tuner/status. - tools/si4684_xtal_calibration.py: automates the trim loop. - tools/si4684_antenna_calibration.py: AN851 Appendix A ANTCAP/VARM/VARB sweep tool (same session, separate calibration). - CONFIG_ESP32_I2S_TEST_TONE (off by default): isolates Si4684-specific audio issues from shared-downstream ones by writing a tone directly over the I2S bus the Si4684 also uses. - SIGMA_WRITE_REGISTER_BLOCK/sigma_safeload_block now retry on NACK and verify via read-back instead of firing I2C writes blind. - FM de-emphasis set to European 50us (was left at the US 75us default). - DAB_ACF_ENABLE restored to its previous 0x0000 with a citation explaining why (tested the datasheet default of 0x0003, made things audibly worse). See docs/si4684-rf-investigation-report.md's 2026-08-23 entry for the full elimination chain and docs/TODO.md for follow-up work (persisting calibration results to EEPROM instead of requiring a firmware edit). Co-Authored-By: Claude Sonnet 5 --- .../ADAU1701-Firmware/SigmaStudioFW.h | 28 +- .../core/include/core/TunerJson.hpp | 37 ++ .../core/include/core/TunerStatus.hpp | 5 + Software/components/core/src/TunerJson.cpp | 41 +++ .../drivers/adau1701/src/Adau1701Driver.cpp | 92 ++++- .../drivers/adau1701/src/SigmaStudioFW.c | 126 ++++++- .../si4684/include/si4684/Si4684Driver.hpp | 33 +- .../si4684/include/si4684/Si4684Tuner.hpp | 18 + .../si4684/include/si4684/Si4684Types.hpp | 6 + .../drivers/si4684/src/Si4684Driver.cpp | 51 ++- .../drivers/si4684/src/Si4684Tuner.cpp | 11 + .../net/include/net/SetupWebServer.hpp | 7 + .../components/net/src/SetupWebServer.cpp | 59 +++ Software/docs/TODO.md | 31 ++ .../docs/si4684-rf-investigation-report.md | 120 ++++++ Software/main/CMakeLists.txt | 1 + Software/main/Kconfig.projbuild | 13 + Software/main/antenna_calibration.cpp | 14 + Software/main/esp32_i2s_test_tone.cpp | 57 +++ Software/main/esp32_i2s_test_tone.hpp | 28 ++ Software/main/hardware_bootstrap.cpp | 16 +- Software/main/main.cpp | 19 + Software/tools/si4684_antenna_calibration.py | 341 ++++++++++++++++++ Software/tools/si4684_xtal_calibration.py | 184 ++++++++++ 24 files changed, 1309 insertions(+), 29 deletions(-) create mode 100644 Software/main/esp32_i2s_test_tone.cpp create mode 100644 Software/main/esp32_i2s_test_tone.hpp create mode 100644 Software/tools/si4684_antenna_calibration.py create mode 100644 Software/tools/si4684_xtal_calibration.py diff --git a/Software/Firmware/ADAU1701-Firmware/SigmaStudioFW.h b/Software/Firmware/ADAU1701-Firmware/SigmaStudioFW.h index b961913..3462031 100644 --- a/Software/Firmware/ADAU1701-Firmware/SigmaStudioFW.h +++ b/Software/Firmware/ADAU1701-Firmware/SigmaStudioFW.h @@ -49,15 +49,19 @@ void sigma_studio_unlock(void); /** * @brief Write a contiguous register/data block to the ADAU1701. * + * Retries each I2C chunk up to 3 times on NACK, matching the reliability + * already applied to the runtime safeload path (see sigma_i2c_write). + * * @param devAddress SigmaStudio device address (0x68 write addr). * @param address 16-bit target address in DSP memory map. * @param length Payload length in bytes (may exceed 255). * @param pData Payload bytes. + * @return 0 on success, non-zero if any chunk failed after retries. */ -void SIGMA_WRITE_REGISTER_BLOCK(unsigned char devAddress, - unsigned int address, - unsigned int length, - ADI_REG_TYPE* pData); +int SIGMA_WRITE_REGISTER_BLOCK(unsigned char devAddress, + unsigned int address, + unsigned int length, + ADI_REG_TYPE* pData); /** * @brief Read a contiguous register/data block from the ADAU1701. @@ -69,6 +73,22 @@ void SIGMA_WRITE_REGISTER_BLOCK(unsigned char devAddress, */ int sigma_i2c_read(unsigned int reg, unsigned char* data, unsigned int length); +/** + * @brief Read back a previously written block and compare it byte-for-byte. + * + * Diagnostic aid for boot-time DSP program replay: a chunk that ACKed but + * still landed wrong (or a register that doesn't hold the value it was + * given) shows up here even though SIGMA_WRITE_REGISTER_BLOCK reported + * success. Read-only — never aborts anything by itself, callers decide. + * + * @param address 16-bit address the block was written to. + * @param expected Bytes that were written. + * @param length Payload length in bytes (may exceed 255). + * @return 0 if the read-back matches, non-zero on mismatch or read failure. + */ +int sigma_verify_block(unsigned int address, const ADI_REG_TYPE* expected, + unsigned int length); + /** Safeload data register base (0x0810..0x0814). */ #define ADAU1701_SAFELOAD_DATA_BASE 0x0810U /** Safeload address register base (0x0815..0x0819). */ diff --git a/Software/components/core/include/core/TunerJson.hpp b/Software/components/core/include/core/TunerJson.hpp index edad96f..a63d35f 100644 --- a/Software/components/core/include/core/TunerJson.hpp +++ b/Software/components/core/include/core/TunerJson.hpp @@ -276,4 +276,41 @@ struct AntennaCalibrationRequest { [[nodiscard]] std::expected parseAntennaCalibrationJson(std::string_view json); +/** + * @brief XtalCalibrationRequest — parsed POST + * /api/tuner/xtal-calibrate body. + * + * @dname XtalCalibrationRequest + * @return n/a (type) + * @pubstate Plain DTO filled by parseXtalCalibrationJson at the HTTP + * boundary. Diagnostic-only: live Si4684 crystal parameter + * recalibration, no ESP32 restart required. + * + * @author Michele Bigi + * @date 2026-08-23 + */ +struct XtalCalibrationRequest { + std::uint8_t ibias; ///< POWER_UP ARG3 IBIAS (0-127). + std::uint8_t ctun; ///< POWER_UP ARG8 CTUN (0-63). + std::uint32_t xtalFreqHz; ///< POWER_UP ARG4-7 XTAL_FREQ in Hz. +}; + +/** + * @brief parseXtalCalibrationJson — validate POST + * /api/tuner/xtal-calibrate body. + * + * @dname parseXtalCalibrationJson + * @param json Untrusted request body from the HTTP handler. + * @return Crystal parameters on success, or a ParseError. `ibias` and + * `ctun` default to the values already loaded at boot when + * omitted (unusual to omit, but harmless); `xtal_freq_hz` + * defaults to the nominal 19,200,000 Hz. + * @pubstate none + * + * @author Michele Bigi + * @date 2026-08-23 + */ +[[nodiscard]] std::expected +parseXtalCalibrationJson(std::string_view json); + } // namespace core diff --git a/Software/components/core/include/core/TunerStatus.hpp b/Software/components/core/include/core/TunerStatus.hpp index f93963d..c8e4828 100644 --- a/Software/components/core/include/core/TunerStatus.hpp +++ b/Software/components/core/include/core/TunerStatus.hpp @@ -63,6 +63,11 @@ struct TunerStatus { std::optional fmChipReadFrequency; ///< FM_RSQ READFREQ (may lag commanded). std::optional fmRssiDbuV; ///< FM RSSI in dBµV. std::optional fmSnrDb; ///< FM SNR in dB. + /** AN649 FM_RSQ_STATUS FREQOFF, units of 2 PPM. Crystal calibration + * signal: real broadcast carriers are GPS/rubidium-locked, so a + * nonzero reading here reflects the local XTAL_FREQ/CTUN reference + * error, not the station. */ + std::optional fmFreqOffBppm; std::optional fmStereo; ///< FM stereo pilot detected. std::optional fmStationName; ///< FM RDS program service name. std::optional fmRadiotext; ///< FM RDS radiotext (RT). diff --git a/Software/components/core/src/TunerJson.cpp b/Software/components/core/src/TunerJson.cpp index 3df7952..84297ad 100644 --- a/Software/components/core/src/TunerJson.cpp +++ b/Software/components/core/src/TunerJson.cpp @@ -129,6 +129,10 @@ std::string serializeTunerStatusJson(const TunerStatus& status) if (status.fmSnrDb) { out << ",\"snr_db\":" << static_cast(*status.fmSnrDb); } + if (status.fmFreqOffBppm) { + out << ",\"freqoff_ppm\":" + << (static_cast(*status.fmFreqOffBppm) * 2); + } if (status.fmStereo) { out << ",\"stereo\":" << (*status.fmStereo ? "true" : "false"); } @@ -364,4 +368,41 @@ parseAntennaCalibrationJson(std::string_view json) return req; } +std::expected +parseXtalCalibrationJson(std::string_view json) +{ + if (json.find('{') == std::string_view::npos) { + return std::unexpected(ParseError::InvalidJson); + } + // Defaults match Si4684Driver::boot()'s own defaults -- a request that + // only wants to change one parameter can omit the others. + XtalCalibrationRequest req = {}; + req.ibias = 72U; + req.ctun = 31U; + req.xtalFreqHz = 19200000U; + + unsigned long ibias = 0U; + if (extractJsonUint(json, "ibias", ibias)) { + if (ibias > 127U) { + return std::unexpected(ParseError::InvalidJson); + } + req.ibias = static_cast(ibias); + } + + unsigned long ctun = 0U; + if (extractJsonUint(json, "ctun", ctun)) { + if (ctun > 63U) { + return std::unexpected(ParseError::InvalidJson); + } + req.ctun = static_cast(ctun); + } + + unsigned long xtalFreqHz = 0U; + if (extractJsonUint(json, "xtal_freq_hz", xtalFreqHz)) { + req.xtalFreqHz = static_cast(xtalFreqHz); + } + + return req; +} + } // namespace core diff --git a/Software/components/drivers/adau1701/src/Adau1701Driver.cpp b/Software/components/drivers/adau1701/src/Adau1701Driver.cpp index e4d2b6f..a664f70 100644 --- a/Software/components/drivers/adau1701/src/Adau1701Driver.cpp +++ b/Software/components/drivers/adau1701/src/Adau1701Driver.cpp @@ -68,6 +68,7 @@ namespace adau1701 { const unsigned char deviceAddr = static_cast(pins_.i2cAddr7 << 1); + std::size_t index = 0U; for (const core::RegisterWrite &write : program.writes()) { const auto data = write.data(); @@ -75,12 +76,35 @@ namespace adau1701 { return std::unexpected(Adau1701Error::DownloadFailed); } - SIGMA_WRITE_REGISTER_BLOCK( - deviceAddr, - write.address(), - static_cast(data.size()), - const_cast( - reinterpret_cast(data.data()))); + auto *bytes = const_cast( + reinterpret_cast(data.data())); + const auto length = static_cast(data.size()); + if (SIGMA_WRITE_REGISTER_BLOCK(deviceAddr, write.address(), length, + bytes) != 0) + { + ESP_LOGE(kTag, + "program write #%u failed (addr=0x%04X len=%u) " + "after retries", + static_cast(index), + static_cast(write.address()), + static_cast(length)); + return std::unexpected(Adau1701Error::DownloadFailed); + } + // Diagnostic only: an ACKed write that still reads back wrong + // would otherwise be invisible (see the SIGMA_WRITE_REGISTER_BLOCK + // comment in SigmaStudioFW.c). Logged, not fatal -- some + // addresses in this range may be self-clearing/status bits + // that legitimately don't read back what was written. + if (sigma_verify_block(write.address(), bytes, length) != 0) + { + ESP_LOGW(kTag, + "program write #%u read-back mismatch (addr=0x%04X " + "len=%u) -- ACKed but did not land as written", + static_cast(index), + static_cast(write.address()), + static_cast(length)); + } + ++index; } return {}; } @@ -158,19 +182,69 @@ namespace adau1701 return replay; } + // Diagnostic: log EQ band 0's live Param RAM contents. This band is + // the fixed high-pass (SigmaStudio band 1) and applyEq() always + // skips it -- no runtime code path ever safeloads it, so whatever + // landed here at program-load time is what plays, permanently, + // regardless of the audio-profile API (which reports a fictional + // value for it). Read straight from the chip rather than trusting + // the compiled source, in case the two have ever diverged. + { + const unsigned baseAddr = paramAddrEqBandBase(0U); + for (unsigned i = 0U; i < 5U; ++i) + { + unsigned char raw[4U] = {0U, 0U, 0U, 0U}; + if (sigma_i2c_read(baseAddr + i, raw, sizeof(raw)) == 0) + { + const std::int32_t fixpoint = + static_cast( + (static_cast(raw[0]) << 24) | + (static_cast(raw[1]) << 16) | + (static_cast(raw[2]) << 8) | + static_cast(raw[3])); + ESP_LOGI(kTag, + "EQ band0 param[%u] addr=0x%04X raw=0x%08X " + "value=%f", + i, baseAddr + i, + static_cast(fixpoint), + static_cast(fixpoint) / + static_cast(1U << 23)); + } + else + { + ESP_LOGW(kTag, "EQ band0 param[%u] read-back failed", i); + } + } + } + // SerialInputRegister (0x081F) override: bit3 IBP=1, matching the // BCLK edge the Si4684's I2S output actually changes data on // (compiled DSP program default is 0x00 = IBP=0, which produced // pure static on a strong locked signal). ILP=1 was also tried // (0x18) and made it worse (pure white noise again) — IBP alone // (0x08) is the correct override, confirmed live: real, recognizable - // music instead of static/noise on a locked FM station. + // music instead of static/noise on a locked FM station. Redundant + // with the R3_HWCONFIGURATION program write above (SigmaStudio's + // own export already places 0x08 at this address) — kept as a + // belt-and-braces re-assert in case that specific chunk silently + // failed; now checked/verified like everything else instead of + // fire-and-forget. { const unsigned char deviceAddr = static_cast(pins_.i2cAddr7 << 1); ADI_REG_TYPE serialInFix = 0x08U; - SIGMA_WRITE_REGISTER_BLOCK(deviceAddr, 0x081FU, 1U, - &serialInFix); + if (SIGMA_WRITE_REGISTER_BLOCK(deviceAddr, 0x081FU, 1U, + &serialInFix) != 0) + { + ESP_LOGE(kTag, "SerialInputRegister override failed after retries"); + return std::unexpected(Adau1701Error::DownloadFailed); + } + if (sigma_verify_block(0x081FU, &serialInFix, 1U) != 0) + { + ESP_LOGW(kTag, + "SerialInputRegister read-back mismatch -- ACKed " + "but did not land as 0x08"); + } } booted_ = true; diff --git a/Software/components/drivers/adau1701/src/SigmaStudioFW.c b/Software/components/drivers/adau1701/src/SigmaStudioFW.c index cefd7c6..38c37d7 100644 --- a/Software/components/drivers/adau1701/src/SigmaStudioFW.c +++ b/Software/components/drivers/adau1701/src/SigmaStudioFW.c @@ -14,12 +14,14 @@ #include "SigmaStudioFW.h" #include "driver/i2c_master.h" +#include "esp_log.h" #include "freertos/FreeRTOS.h" #include "freertos/semphr.h" #include "freertos/task.h" #include +static const char* kTag = "SigmaStudioFW"; static i2c_master_dev_handle_t s_dev = NULL; static SemaphoreHandle_t s_lock = NULL; @@ -73,14 +75,45 @@ static unsigned int sigmaWordSize(unsigned int address) return 4U; } -void SIGMA_WRITE_REGISTER_BLOCK(unsigned char devAddress, - unsigned int address, - unsigned int length, - ADI_REG_TYPE* pData) +/* + * The boot-time program/param replay was previously fire-and-forget: the + * i2c_master_transmit result was discarded outright, so a single transient + * NACK anywhere in the hundreds of chunked writes that make up a DSP + * program load silently left that one chunk (a gain, a filter, a mux) + * at its power-on-reset RAM contents instead of the SigmaStudio-designed + * value -- indistinguishable at the time from a correct load, but capable + * of producing exactly a persistent, localized audio artifact rather than + * gross silence. Give it the same retry-on-NACK reliability the runtime + * safeload path already got in sigma_i2c_write (see its comment) and make + * failure observable instead of silent. + */ +static int sigmaTransmitChunk(unsigned int addr, const ADI_REG_TYPE* payload, + unsigned int chunkBytes) +{ + unsigned char buf[2U + 64U]; + buf[0] = (unsigned char)((addr >> 8) & 0xFFU); + buf[1] = (unsigned char)(addr & 0xFFU); + memcpy(buf + 2U, payload, chunkBytes); + + static const int kMaxAttempts = 3; + for (int attempt = 0; attempt < kMaxAttempts; ++attempt) { + if (i2c_master_transmit(s_dev, buf, (size_t)(2U + chunkBytes), 1000) == + ESP_OK) { + return 0; + } + vTaskDelay(pdMS_TO_TICKS(2)); + } + return -1; +} + +int SIGMA_WRITE_REGISTER_BLOCK(unsigned char devAddress, + unsigned int address, + unsigned int length, + ADI_REG_TYPE* pData) { (void)devAddress; if (s_dev == NULL || pData == NULL || length == 0U) { - return; + return -1; } enum { kChunkBytesMax = 64U }; @@ -96,15 +129,48 @@ void SIGMA_WRITE_REGISTER_BLOCK(unsigned char devAddress, const unsigned int chunk = remaining > chunkBytes ? chunkBytes : remaining; const unsigned int words = chunk / wordSize; - unsigned char buf[2U + kChunkBytesMax]; - buf[0] = (unsigned char)((addr >> 8) & 0xFFU); - buf[1] = (unsigned char)(addr & 0xFFU); - memcpy(buf + 2U, cursor, chunk); - i2c_master_transmit(s_dev, buf, (size_t)(2U + chunk), 1000); + if (sigmaTransmitChunk(addr, cursor, chunk) != 0) { + return -1; + } addr += words; cursor += chunk; remaining -= chunk; } + return 0; +} + +int sigma_verify_block(unsigned int address, const ADI_REG_TYPE* expected, + unsigned int length) +{ + if (s_dev == NULL || expected == NULL || length == 0U) { + return -1; + } + + enum { kChunkBytesMax = 64U }; + const unsigned int wordSize = sigmaWordSize(address); + const unsigned int wordsPerChunk = kChunkBytesMax / wordSize; + const unsigned int chunkBytes = wordsPerChunk * wordSize; + + unsigned int addr = address; + unsigned int remaining = length; + const ADI_REG_TYPE* cursor = expected; + + while (remaining > 0U) { + const unsigned int chunk = + remaining > chunkBytes ? chunkBytes : remaining; + const unsigned int words = chunk / wordSize; + unsigned char readBack[kChunkBytesMax]; + if (sigma_i2c_read(addr, readBack, chunk) != 0) { + return -1; + } + if (memcmp(readBack, cursor, chunk) != 0) { + return -1; + } + addr += words; + cursor += chunk; + remaining -= chunk; + } + return 0; } int sigma_i2c_read(unsigned int reg, unsigned char* data, unsigned int length) @@ -198,6 +264,37 @@ int sigma_safeload_param(unsigned int paramAddr, int fixpoint) return sigma_safeload_block(1U, addrs, values); } +/* + * Diagnostic only, mirrors the boot-replay read-back in + * SIGMA_WRITE_REGISTER_BLOCK: a safeload can ACK every transaction and + * still not land as intended (address/data written to the wrong Param RAM + * slot, a stale value left from a previous session's committed-but-later- + * garbled write, etc). This is the runtime path applied on every EQ/gain + * change and on the stored-profile replay at boot -- unlike the one-time + * program load, it was previously unverified. Log-only: some callers pass + * addresses this can't independently confirm are readable Param RAM (vs. + * a write-only control register), so a mismatch here is a strong signal, + * not a hard boot/apply-time failure. + */ +static void sigmaVerifyParam(unsigned int paramAddr, int fixpoint) +{ + unsigned char readBack[4U]; + if (sigma_i2c_read(paramAddr, readBack, sizeof(readBack)) != 0) { + ESP_LOGW(kTag, "safeload verify: read-back failed for param 0x%04X", + paramAddr); + return; + } + const int actual = (int)(((unsigned int)readBack[0] << 24) | + ((unsigned int)readBack[1] << 16) | + ((unsigned int)readBack[2] << 8) | + (unsigned int)readBack[3]); + if (actual != fixpoint) { + ESP_LOGW(kTag, + "safeload verify: param 0x%04X mismatch, wrote %d read %d", + paramAddr, fixpoint, actual); + } +} + int sigma_safeload_block(unsigned char count, const unsigned int* paramAddrs, const int* fixpoints) { @@ -218,7 +315,14 @@ int sigma_safeload_block(unsigned char count, const unsigned int* paramAddrs, } } - return sigma_trigger_safeload(); + if (sigma_trigger_safeload() != 0) { + return -1; + } + + for (unsigned char i = 0U; i < count; ++i) { + sigmaVerifyParam(paramAddrs[i], fixpoints[i]); + } + return 0; } int sigma_safeload_raw_block(unsigned char count, const unsigned int* paramAddrs, diff --git a/Software/components/drivers/si4684/include/si4684/Si4684Driver.hpp b/Software/components/drivers/si4684/include/si4684/Si4684Driver.hpp index 91e00f2..46d96a6 100644 --- a/Software/components/drivers/si4684/include/si4684/Si4684Driver.hpp +++ b/Software/components/drivers/si4684/include/si4684/Si4684Driver.hpp @@ -112,6 +112,16 @@ public: * verified live (72 = 720 uA startup bias). * @param xtalCtun POWER_UP ARG8 CTUN, 0-63 (AN649 §Command 0x01); * default matches the values already verified live. + * @param xtalFreqHz POWER_UP ARG4-7 XTAL_FREQ in Hz (AN649 §Command + * 0x01); default 19,200,000 (nominal crystal + * frequency). Deliberately overriding this away + * from the crystal's true nominal value is a valid + * software calibration trick: it tells the chip's + * internal PLL math "the crystal actually runs at + * this rate," compensating any real physical + * offset (from load-cap mismatch etc) without + * touching CTUN. See FM_RSQ_STATUS FREQOFF for the + * measurement this is meant to null out. * @return Ok on success, or Si4684Error. * @pubstate writes booted_ and loadedBand_ on success. * @@ -120,7 +130,28 @@ public: */ [[nodiscard]] std::expected boot( Si4684Band band, std::uint8_t xtalIbias = 72U, - std::uint8_t xtalCtun = 31U); + std::uint8_t xtalCtun = 31U, std::uint32_t xtalFreqHz = 19200000U); + + /** + * @brief recalibrateXtal — re-run boot() with new crystal parameters. + * + * @dname recalibrateXtal + * @param xtalIbias New POWER_UP ARG3 IBIAS. + * @param xtalCtun New POWER_UP ARG8 CTUN. + * @param xtalFreqHz New POWER_UP ARG4-7 XTAL_FREQ in Hz. + * @return Ok on success, or Si4684Error::NotBooted if never booted. + * @pubstate forces booted_=false then re-runs boot() for the currently + * loaded band -- full RSTB# pulse + patch/image reload, same + * as a cold boot, just without an ESP32 restart. Diagnostic: + * lets a calibration script iterate crystal parameters live + * over HTTP instead of a firmware rebuild+reflash per value. + * + * @author Michele Bigi + * @date 2026-08-23 + */ + [[nodiscard]] std::expected recalibrateXtal( + std::uint8_t xtalIbias, std::uint8_t xtalCtun, + std::uint32_t xtalFreqHz); /** * @brief isBooted — query whether boot completed successfully. diff --git a/Software/components/drivers/si4684/include/si4684/Si4684Tuner.hpp b/Software/components/drivers/si4684/include/si4684/Si4684Tuner.hpp index a11be0c..5bc1c24 100644 --- a/Software/components/drivers/si4684/include/si4684/Si4684Tuner.hpp +++ b/Software/components/drivers/si4684/include/si4684/Si4684Tuner.hpp @@ -178,6 +178,24 @@ public: [[nodiscard]] std::expected setVolume( std::uint8_t level) override; + /** + * @brief recalibrateXtal — re-run driver boot() with new crystal + * parameters, without an ESP32 restart. + * + * @dname recalibrateXtal + * @param ibias New POWER_UP ARG3 IBIAS. + * @param ctun New POWER_UP ARG8 CTUN. + * @param xtalFreqHz New POWER_UP ARG4-7 XTAL_FREQ in Hz. + * @return Ok on success, or a mapped TunerError. + * @pubstate delegates to driver_.recalibrateXtal(); caller must re-tune + * afterwards, this only reboots the chip. + * + * @author Michele Bigi + * @date 2026-08-23 + */ + [[nodiscard]] std::expected recalibrateXtal( + std::uint8_t ibias, std::uint8_t ctun, std::uint32_t xtalFreqHz); + private: /** * @brief mapError — translate Si4684Error to core::TunerError. diff --git a/Software/components/drivers/si4684/include/si4684/Si4684Types.hpp b/Software/components/drivers/si4684/include/si4684/Si4684Types.hpp index 2656181..cbe1f67 100644 --- a/Software/components/drivers/si4684/include/si4684/Si4684Types.hpp +++ b/Software/components/drivers/si4684/include/si4684/Si4684Types.hpp @@ -98,6 +98,12 @@ struct Si4684FmRsq { std::int8_t snrDb; ///< SNR in dB. bool valid; ///< RSQ valid flag from the chip. bool stereo; ///< Stereo pilot detected. + /** AN649 FM_RSQ_STATUS RESP8 FREQOFF: signed offset in units of 2 PPM + * (range -128..127, i.e. -256..+254 PPM). Crystal calibration signal: + * every locked station's carrier reads the same PPM error when the + * XTAL_FREQ/CTUN reference is off, since real broadcast transmitters + * are themselves GPS/rubidium-locked. */ + std::int8_t freqOffBppm; }; /** diff --git a/Software/components/drivers/si4684/src/Si4684Driver.cpp b/Software/components/drivers/si4684/src/Si4684Driver.cpp index 27f41e1..5934402 100644 --- a/Software/components/drivers/si4684/src/Si4684Driver.cpp +++ b/Software/components/drivers/si4684/src/Si4684Driver.cpp @@ -40,6 +40,8 @@ constexpr std::size_t kSpiReplyLeadIn = 1U; /** FM_RSQ_STATUS field indices with kSpiReplyLeadIn (AN649 RESP5–10). */ constexpr std::size_t kFmRsqOffValid = 6U; constexpr std::size_t kFmRsqOffReadFreq = 7U; +/** AN649 FM_RSQ_STATUS RESP8 FREQOFF: signed offset in 2 PPM units. */ +constexpr std::size_t kFmRsqOffFreqOff = 9U; constexpr std::size_t kFmRsqOffRssi = 10U; constexpr std::size_t kFmRsqOffSnr = 11U; @@ -64,6 +66,12 @@ constexpr std::uint16_t kSi4684I2sOutEnable = 0x8002U; /** Si4684 volume: 0=mute, 63=max (AN649 AUDIO_ANALOG_VOLUME). */ constexpr std::uint8_t kSi4684VolumeMax = 63U; constexpr std::uint16_t kPropFmRdsConfig = 0x3C02U; +/** AN649 FM_AUDIO_DE_EMPHASIS (0x3900): 0=75us/US (chip default), 1=50us/ + * Europe, 2=disabled. FM seek band/spacing above is already the European + * 87.5-107.9 MHz/100 kHz plan, so the chip must not stay on its 75us/US + * default -- that under-de-emphasizes treble on every station. */ +constexpr std::uint16_t kPropFmAudioDeEmphasis = 0x3900U; +constexpr std::uint16_t kFmAudioDeEmphasisEurope = 0x0001U; /** AN649 FM valid tune properties (defaults RSSI 17 dBµV, SNR 10 dB). */ constexpr std::uint16_t kPropFmValidRssiThreshold = 0x3202U; constexpr std::uint16_t kPropFmValidSnrThreshold = 0x3204U; @@ -486,6 +494,16 @@ std::expected Si4684Driver::configureAfterBoot( {0xB302U, 0x0000U}, {0xB303U, 0x0000U}, {0xB401U, 0x0002U}, + // AN649 Property 0xB500 DAB_ACF_ENABLE: bit0 SOFTMUTE_ENABLE, + // bit1 COMF_NOISE_ENABLE, datasheet default 0x0003 (both on). + // Tried 0x0003 on 2026-08-23 hoping it would mask the periodic + // hiss/unintelligible-voice symptom on real DAB reception -- + // live A/B test made it worse (COMF_NOISE injects synthetic + // noise on every brief signal-quality dip, and frequent + // softmute engagement chopped up speech). PE5PVB's independent + // SI4684-DAB-Receiver project also explicitly disables this + // (Set_Property(0xB500, 0x0000)), matching what real testing + // shows here -- disabled deliberately, not an oversight. {0xB500U, 0x0000U}, }; for (const auto& prop : kDabProps) { @@ -542,6 +560,11 @@ std::expected Si4684Driver::configureAfterBoot( if (auto rds = setProperty(kPropFmRdsConfig, 0x0001U); !rds) { return rds; } + if (auto deEmph = setProperty(kPropFmAudioDeEmphasis, + kFmAudioDeEmphasisEurope); + !deEmph) { + return deEmph; + } // AN649 §0x3202/0x3204: lower seek/tune validity for weak lab antennas. if (auto rssi = setProperty(kPropFmValidRssiThreshold, kFmValidRssiThresholdDbuV); @@ -597,7 +620,8 @@ std::expected Si4684Driver::configureAfterBoot( } std::expected Si4684Driver::boot( - Si4684Band band, std::uint8_t xtalIbias, std::uint8_t xtalCtun) + Si4684Band band, std::uint8_t xtalIbias, std::uint8_t xtalCtun, + std::uint32_t xtalFreqHz) { if (booted_ && loadedBand_ == band) { return {}; @@ -672,10 +696,19 @@ std::expected Si4684Driver::boot( } // ARG2=0x17(CLK_MODE=crystal,TR_SIZE), ARG3=IBIAS, ARG4-7=XTAL_FREQ - // 19.2 MHz (0x0124F800), ARG8=CTUN, ARG9=0x10 (fixed bit4=1 per AN649 + // (little-endian, nominal 19.2 MHz = 0x0124F800; may be intentionally + // offset from nominal as a software crystal calibration -- see the + // xtalFreqHz doc comment), ARG8=CTUN, ARG9=0x10 (fixed bit4=1 per AN649 // §Command 0x01), ARG10-15=0 (AN649 POWER_UP argument table). std::uint8_t powerUp[] = { - 0x17, xtalIbias, 0x00, 0xf8, 0x24, 0x01, xtalCtun, 0x10, + 0x17, + xtalIbias, + static_cast(xtalFreqHz & 0xFFU), + static_cast((xtalFreqHz >> 8) & 0xFFU), + static_cast((xtalFreqHz >> 16) & 0xFFU), + static_cast((xtalFreqHz >> 24) & 0xFFU), + xtalCtun, + 0x10, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, }; if (auto pu = writeCommand(Command::PowerUp, powerUp, sizeof(powerUp)); @@ -745,6 +778,17 @@ std::expected Si4684Driver::boot( return {}; } +std::expected Si4684Driver::recalibrateXtal( + std::uint8_t xtalIbias, std::uint8_t xtalCtun, std::uint32_t xtalFreqHz) +{ + if (auto ready = ensureBooted(); !ready) { + return ready; + } + const Si4684Band band = loadedBand_; + booted_ = false; + return boot(band, xtalIbias, xtalCtun, xtalFreqHz); +} + bool Si4684Driver::isBooted() const noexcept { return booted_; @@ -920,6 +964,7 @@ std::expected Si4684Driver::readFmRsq() static_cast(raw[kFmRsqOffSnr]), freqInBand && chipValid, false, + static_cast(raw[kFmRsqOffFreqOff]), }; return rsq; } diff --git a/Software/components/drivers/si4684/src/Si4684Tuner.cpp b/Software/components/drivers/si4684/src/Si4684Tuner.cpp index 3330599..d13af7e 100644 --- a/Software/components/drivers/si4684/src/Si4684Tuner.cpp +++ b/Software/components/drivers/si4684/src/Si4684Tuner.cpp @@ -169,6 +169,7 @@ std::expected Si4684Tuner::readStatus() status.locked = rsq->valid; status.fmRssiDbuV = rsq->rssiDbuV; status.fmSnrDb = rsq->snrDb; + status.fmFreqOffBppm = rsq->freqOffBppm; status.fmStereo = rsq->stereo; status.fmChipReadFrequency = rsq->frequency; // Keep commanded frequency when chip READFREQ is stale (stuck at band @@ -342,4 +343,14 @@ std::expected Si4684Tuner::setVolume(std::uint8_t level) return {}; } +std::expected Si4684Tuner::recalibrateXtal( + std::uint8_t ibias, std::uint8_t ctun, std::uint32_t xtalFreqHz) +{ + if (auto result = driver_.recalibrateXtal(ibias, ctun, xtalFreqHz); + !result) { + return std::unexpected(mapError(result.error())); + } + return {}; +} + } // namespace si4684 diff --git a/Software/components/net/include/net/SetupWebServer.hpp b/Software/components/net/include/net/SetupWebServer.hpp index 7953dc7..b8af885 100644 --- a/Software/components/net/include/net/SetupWebServer.hpp +++ b/Software/components/net/include/net/SetupWebServer.hpp @@ -105,6 +105,13 @@ struct AntennaCalibration { /** Persist a new DAB ANTCAP calibration value to EEPROM. * @return false on an I2C failure. */ bool (*saveDab)(std::uint8_t antCap); + /** Diagnostic-only, 2026-08-23: re-run Si4684Driver::boot() with new + * crystal parameters (no ESP32 restart, no persistence). Bridged here + * rather than through a new context field to avoid a second plumbing + * chain for what is a temporary calibration tool. + * @return false if the chip was never booted or the reboot failed. */ + bool (*recalibrateXtal)(std::uint8_t ibias, std::uint8_t ctun, + std::uint32_t xtalFreqHz); }; /** diff --git a/Software/components/net/src/SetupWebServer.cpp b/Software/components/net/src/SetupWebServer.cpp index e7c1547..0271ee1 100644 --- a/Software/components/net/src/SetupWebServer.cpp +++ b/Software/components/net/src/SetupWebServer.cpp @@ -699,6 +699,57 @@ esp_err_t tunerCalibrateAntennaPostHandler(httpd_req_t* req) return httpd_resp_send(req, json.c_str(), json.size()); } +/** + * @brief tunerXtalCalibratePostHandler — POST /api/tuner/xtal-calibrate. + * + * Diagnostic-only, 2026-08-23: reboots the Si4684 with new IBIAS/CTUN/ + * XTAL_FREQ (AN649 §Command 0x01 POWER_UP) without an ESP32 restart or + * NVS persistence, so a calibration script can iterate crystal parameters + * live. Caller must re-tune afterwards -- this only reboots the chip. + * See FM_RSQ_STATUS FREQOFF (GET /api/tuner/status, "freqoff_ppm") for the + * measurement this is meant to null out. + */ +esp_err_t tunerXtalCalibratePostHandler(httpd_req_t* req) +{ + auto* ctx = routeContextFrom(req); + if (ctx == nullptr || ctx->antennaCalibration == nullptr + || ctx->antennaCalibration->recalibrateXtal == nullptr) { + httpd_resp_set_status(req, "503 Service Unavailable"); + return httpd_resp_send(req, nullptr, 0); + } + + std::array body{}; + if (!readRequestBody(req, body)) { + httpd_resp_set_status(req, "400 Bad Request"); + return httpd_resp_send(req, nullptr, 0); + } + + const auto parsed = + core::parseXtalCalibrationJson(std::string_view(body.data())); + if (!parsed) { + const std::string json = core::serializeTunerErrorJson("invalid_json"); + httpd_resp_set_status(req, "400 Bad Request"); + httpd_resp_set_type(req, "application/json"); + return httpd_resp_send(req, json.c_str(), json.size()); + } + + if (!ctx->antennaCalibration->recalibrateXtal( + parsed->ibias, parsed->ctun, parsed->xtalFreqHz)) { + const std::string json = core::serializeTunerErrorJson("boot_failed"); + httpd_resp_set_status(req, "500 Internal Server Error"); + httpd_resp_set_type(req, "application/json"); + return httpd_resp_send(req, json.c_str(), json.size()); + } + + const std::string json = + std::string("{\"status\":\"recalibrated\",\"ibias\":") + + std::to_string(parsed->ibias) + ",\"ctun\":" + + std::to_string(parsed->ctun) + ",\"xtal_freq_hz\":" + + std::to_string(parsed->xtalFreqHz) + "}"; + httpd_resp_set_type(req, "application/json"); + return httpd_resp_send(req, json.c_str(), json.size()); +} + /** * @brief audioProfileGetHandler — serve GET /api/audio/profile JSON. * @@ -2075,6 +2126,14 @@ std::expected SetupWebServer::start( }; httpd_register_uri_handler(server_, &tunerCalibrateAntennaUri); + const httpd_uri_t tunerXtalCalibrateUri = { + .uri = "/api/tuner/xtal-calibrate", + .method = HTTP_POST, + .handler = tunerXtalCalibratePostHandler, + .user_ctx = routeCtx, + }; + httpd_register_uri_handler(server_, &tunerXtalCalibrateUri); + const httpd_uri_t audioProfileGetUri = { .uri = "/api/audio/profile", .method = HTTP_GET, diff --git a/Software/docs/TODO.md b/Software/docs/TODO.md index bb70865..84a1036 100644 --- a/Software/docs/TODO.md +++ b/Software/docs/TODO.md @@ -204,6 +204,37 @@ Done in fw 0.8.5 unless noted: --- +## TODO — calibration functions need to become permanent, in-firmware, on-demand tools (2026-08-23) + +Both ANTCAP calibration (`tools/si4684_antenna_calibration.py`) and Si4684 +crystal calibration (`tools/si4684_xtal_calibration.py`, +`POST /api/tuner/xtal-calibrate`) currently exist as **host-side Python +scripts driving live-but-unpersisted HTTP endpoints** — they compute a +result but the operator has to hand-edit firmware source (constants in +`Si4684Driver.cpp` / the `gSi4684.boot(...)` call in +`hardware_bootstrap.cpp`) and reflash to make a result permanent. + +**Wanted instead**: both calibration procedures should be triggerable +on-demand *from the device itself* (an HTTP endpoint is enough — no UI +required yet) and, once a result converges, **write the result to the +24AA025E48 EEPROM** (same chip/pattern already used for ANTCAP +persistence, see `Eeprom24aa::writeFmAntCap`/`writeDabAntCap`) so it +survives a reboot without a firmware reflash. `recalibrateXtal()` +(`Si4684Driver.cpp`) already does the live re-boot-with-new-params part; +what's missing is EEPROM persistence for CTUN/XTAL_FREQ (ANTCAP already +persists this way — the xtal calibration should follow the same shape, +likely a new EEPROM word address alongside the existing FM/DAB ANTCAP +ones) and doing the FREQOFF-averaging/damping/convergence-loop logic +in firmware (or keeping it host-side and just adding the EEPROM-persist +step at the end — decide when picked up). + +Not started — explicitly deferred to a future session, noted here only so +it isn't lost. See `docs/si4684-rf-investigation-report.md`'s 2026-08-23 +entry for full context on why this calibration was needed and how it +currently works. + +--- + ## Quality gates (run from `Software/` before merge) ```bash diff --git a/Software/docs/si4684-rf-investigation-report.md b/Software/docs/si4684-rf-investigation-report.md index b1527a7..9125700 100644 --- a/Software/docs/si4684-rf-investigation-report.md +++ b/Software/docs/si4684-rf-investigation-report.md @@ -1111,3 +1111,123 @@ RTS=0 stability argues against pure floating-noise, and flow control may not even be engaged by default per the datasheet. The single most valuable next input is Feasycom's own answer, not another round of timing-parameter permutation. + +## 2026-08-23 update: FM/DAB pitch-distortion root-caused and fixed (uncalibrated Si4684 crystal reference); a separate downstream audio-quality issue found and left open + +**Symptom**: user reported FM+DAB audio hiss/distortion, later sharpened to +"voce stonata" (mistuned/off-pitch voice), present on both bands. + +**Elimination chain, each step verified on real hardware, not theory**: +RF signal strength (new antenna, RSSI/SNR excellent — see the ANTCAP +section above) → boot-time ADAU1701 DSP program load (added read-back +verification to `SIGMA_WRITE_REGISTER_BLOCK`/`sigma_safeload_block`, +confirmed clean on every boot and every runtime safeload) → ADAU1701 EQ +band 0 ("fixed high-pass," never touched at runtime) — initially +miscalculated as an unstable filter from a wrong fixed-point bit-width +assumption (8.23 vs the chip's actual 5.23/28-bit format), corrected and +confirmed stable via SigmaStudio's own live register capture connected to +the device → ADAU1701 mixer (all input knobs centered, confirmed) → +`DAB_ACF_ENABLE` (0xB500, found disabled with no citation; datasheet +default is 3; tried enabling it — made things audibly *worse*, likely +because COMF_NOISE_ENABLE literally injects synthetic noise on signal +dips; reverted to 0x0000, matching what PE5PVB's independent +SI4684-DAB-Receiver project also does deliberately — not the cause, but +no longer an unexplained magic number) → **decisive test**: a 440 Hz tone +generated by the ESP32 and written directly over the shared I2S bus +(`main/esp32_i2s_test_tone.{hpp,cpp}`, `CONFIG_ESP32_I2S_TEST_TONE`) came +through clean and perfectly in-tune, verified with a real tuner — isolated +the pitch problem to the Si4684 itself, ruling out ADAU1701/mixer/I2S +receiving/BT1035/speaker → TR_SIZE (0x7) and IBIAS (72 = 720µA) checked +against AN649 Figure 13 ("Safe Range of Operation for a 19.2 MHz +Crystal"), both comfortably inside the safe range for this crystal's ESR. + +**Root cause**: `Si4684Driver::boot()`'s `xtalCtun=31`/`xtalIbias=72` +defaults had only a vague "already verified live" justification — no +actual measurement for *this* board's crystal (Abracon +ABM8-19.200MHZ-10-1-U-T, CL=10pF decoded from the part number's own +ordering-code table, two external 15pF load caps per the schematic) ever +existed. + +**Fix, two stages**: +1. CTUN empirical trim by ear (AN649 §9.3 says trim by measurement; no + oscilloscope available, and a multimeter's frequency counter reads the + strong I2S LRCLK signal fine but returns 0 on the crystal pins + themselves — too weak/high-impedance for a general-purpose meter to + trigger on). Swept 31→5→0 (0 = the floor of the 0-63 range), + monotonic improvement each step, still "less bad, not fixed" at the + floor. +2. **XTAL_FREQ precision trim using the chip's own measurement, no lab + equipment needed**: real FM/DAB transmitters are GPS/rubidium-locked, + so FM_RSQ_STATUS's FREQOFF field (AN649 Command 0x32 RESP8, signed, + units of 2 PPM) directly reports the *receiver's* crystal error on any + locked station. Added `Si4684Driver::recalibrateXtal()` (forces a full + re-boot with new IBIAS/CTUN/XTAL_FREQ, no ESP32 restart), a new + endpoint `POST /api/tuner/xtal-calibrate`, exposed FREQOFF as + `"freqoff_ppm"` in `GET /api/tuner/status`, and + `tools/si4684_xtal_calibration.py` to automate the trim loop + (tune → average several FREQOFF samples → correct → repeat). + **Non-obvious gotcha, found only by watching the first attempt + diverge, not documented anywhere**: the correction sign is + `xtal_freq *= (1 - ppm/1e6)`, not `(1 + ppm/1e6)` — the "tell it the + truth" sign convention makes the error grow, not shrink (confirmed + live: ppm went 28→60→122→254/no-lock across 3 iterations before the + sign was flipped). Averaging + damping (0.6) were both needed for + smooth convergence; a single raw FREQOFF sample has enough + reception-noise jitter (~±20-35 ppm swings observed) to make an + undamped loop oscillate instead of settling. + +**Final calibrated values** (now the firmware default, +`main/hardware_bootstrap.cpp`): `CTUN=0`, `XTAL_FREQ=19,199,750 Hz` +(≈-13 ppm off the 19.2 MHz nominal). Converged residual: **-3.8 ppm at +87.6 MHz, -3.0 ppm at 105.1 MHz** — consistent across two stations at +opposite ends of the FM band (the cross-check AN649 itself recommends), +confirming this really is the crystal reference and not something +frequency-dependent. Down from **+70 ppm** uncorrected at nominal +XTAL_FREQ. No physical hardware change (different load-cap values) ended +up being necessary — contrary to what seemed likely after CTUN alone. + +**User-confirmed result**: "migliorato moltissimo" (improved a lot) after +this fix — the systematic pitch/tuning distortion is resolved. + +### Still open: a separate downstream hiss/intelligibility issue, NOT the Si4684 + +After the crystal fix, the user still reported residual hiss and, more +seriously, words being unintelligible on both FM and DAB. Recordings sent +for spectral analysis showed no gross technical defects (no clipping, no +dropouts, no dominant isolated resonance) — inconclusive from the +recordings alone (phone-mic-through-air recordings are a poor tool for +this specific symptom; room acoustics and mic response confound the +signal). The user's direct listening judgement (confirmed repeatedly: +"si sente ancora fruscio e le parole sono incomprensibili") is the +ground truth here, not the recordings. + +**Decisive test**: enabled `web_radio_stream` (internet radio via ESP32, +`POST /api/streaming`) with a direct HTTP MP3 stream +(`http://icecast.radiofrance.fr/franceinter-midfi.mp3`), routed through +the same shared ADAU1701 mixer/EQ/output/BT1035/Bluetooth-speaker chain +as FM/DAB but **never touching the Si4684 at all**. User confirmed: +**same symptom** (hiss + unintelligible). This is different from the +earlier synthetic-440Hz-tone test, which came through clean — the tone +test used a trivial, CPU-cheap sine generator with no decode/buffering +involved, so it never exercised whatever a real MP3-decode-under-WiFi-load +pipeline does. + +**Conclusion**: the pitch/tuning problem (fixed) and this hiss/ +intelligibility problem are two separate, independently-confirmed root +causes that happened to co-occur and get conflated as "one bug" for most +of this session. The Si4684/crystal is now cleared for *this* symptom — +next session should look at: (a) `web_radio_stream`'s MP3 decode/I2S +buffer-feed path for underrun/overrun under real WiFi jitter, since that +was the actual reproducer, and (b) whether the same class of issue could +independently affect the ADAU1701 mixer/EQ path under real dynamic +program content generally (the passing tone test doesn't rule this out +for FM/DAB specifically, only for a pure sine wave). Don't re-open the +Si4684/crystal-calibration question for this symptom without new +evidence — it's a different, still-unidentified mechanism. + +**New permanent diagnostic tools from this session** (kept in the repo, +not removed): `GET /api/tuner/status` now reports `"freqoff_ppm"` for FM; +`POST /api/tuner/xtal-calibrate` for live Si4684 crystal re-trim without +reflashing; `CONFIG_ESP32_I2S_TEST_TONE` Kconfig option (off by default) +for isolating Si4684-specific vs. shared-downstream audio issues; +`tools/si4684_xtal_calibration.py` and `tools/si4684_antenna_calibration.py`. diff --git a/Software/main/CMakeLists.txt b/Software/main/CMakeLists.txt index 8893b97..a46d76e 100644 --- a/Software/main/CMakeLists.txt +++ b/Software/main/CMakeLists.txt @@ -8,6 +8,7 @@ idf_component_register( "antenna_calibration.cpp" "$<$:test_firmware.cpp>" "$<$:i2s_sdata_probe.cpp>" + "$<$:esp32_i2s_test_tone.cpp>" INCLUDE_DIRS "." REQUIRES core net secure_store adau1701 si4684 tuner audio bt1035 bluetooth station integration ota eeprom24aa webradio driver PRIV_REQUIRES esp_timer esp_http_client diff --git a/Software/main/Kconfig.projbuild b/Software/main/Kconfig.projbuild index 6f4467a..2789a1b 100644 --- a/Software/main/Kconfig.projbuild +++ b/Software/main/Kconfig.projbuild @@ -17,4 +17,17 @@ config I2S_SDATA_PROBE input trace (or an equivalent test point) -- enabling this without the rewire just reads whatever GPIO16 happens to be floating/connected to. +config ESP32_I2S_TEST_TONE + bool "ESP32-generated sine tone over the shared I2S TX channel" + default n + help + Continuously writes a sine wave to esp32_i2s_sink (the same shared + BCLK/LRCLK the Si4684 uses on its own SDATA_IN0 pin, but over + SDATA_IN1). Diagnostic: if this sounds clean while FM/DAB is + distorted, the ADAU1701's I2S input receiving is fine in general and + the problem is specific to the Si4684 side. Remember the ESP32 + mixer channel is muted (-96 dB) by default in the stored audio + profile -- raise it via PUT /api/audio/profile to actually hear + this. + endmenu diff --git a/Software/main/antenna_calibration.cpp b/Software/main/antenna_calibration.cpp index d727ec1..aab8b0d 100644 --- a/Software/main/antenna_calibration.cpp +++ b/Software/main/antenna_calibration.cpp @@ -11,14 +11,28 @@ #include "antenna_calibration.hpp" #include "hardware_bootstrap.hpp" +#include "si4684/Si4684Tuner.hpp" namespace antenna_calibration { +namespace { + +/** Diagnostic-only, 2026-08-23: see net::AntennaCalibration::recalibrateXtal. */ +bool recalibrateXtal(std::uint8_t ibias, std::uint8_t ctun, + std::uint32_t xtalFreqHz) +{ + return static_cast(hardware::HardwareBootstrap::si4684Tuner() + .recalibrateXtal(ibias, ctun, xtalFreqHz)); +} + +} // namespace + net::AntennaCalibration& bridge() noexcept { static net::AntennaCalibration instance{ .save = &hardware::HardwareBootstrap::saveFmAntCapCalibration, .saveDab = &hardware::HardwareBootstrap::saveDabAntCapCalibration, + .recalibrateXtal = &recalibrateXtal, }; return instance; } diff --git a/Software/main/esp32_i2s_test_tone.cpp b/Software/main/esp32_i2s_test_tone.cpp new file mode 100644 index 0000000..6ed220b --- /dev/null +++ b/Software/main/esp32_i2s_test_tone.cpp @@ -0,0 +1,57 @@ +/** + * @file esp32_i2s_test_tone.cpp + * @brief esp32_i2s_test_tone implementation. + * + * DigiRadio firmware — https://github.com/manvalan/DigiRadio + * + * Copyright 2026 Michele Bigi + * SPDX-License-Identifier: Apache-2.0 + */ +#include "esp32_i2s_test_tone.hpp" + +#include "esp32_i2s_sink.hpp" + +#include +#include +#include + +namespace esp32_i2s_test_tone { + +namespace { +constexpr int kSampleRateHz = 48000; +constexpr int kFramesPerBlock = 256; +/** writeSamples() wants top-aligned 24-bit in a 32-bit slot -- 8-bit shift. */ +constexpr int kTopAlignShift = 8; +/** Modest amplitude: audible but not a full-scale blast. */ +constexpr float kAmplitude = 0.4F * 8388607.0F; +constexpr float kTwoPi = 2.0F * std::numbers::pi_v; +} // namespace + +[[noreturn]] void run(float freqHz) +{ + esp32_i2s_sink::tryAcquire(); + + const float phaseStep = kTwoPi * freqHz / static_cast(kSampleRateHz); + float phase = 0.0F; + std::int32_t buf[kFramesPerBlock * 2]; + + for (;;) + { + for (int i = 0; i < kFramesPerBlock; ++i) + { + phase += phaseStep; + if (phase > kTwoPi) + { + phase -= kTwoPi; + } + const auto sample = + static_cast(std::sin(phase) * kAmplitude) + << kTopAlignShift; + buf[(i * 2) + 0] = sample; + buf[(i * 2) + 1] = sample; + } + esp32_i2s_sink::writeSamples(buf, kFramesPerBlock * 2); + } +} + +} // namespace esp32_i2s_test_tone diff --git a/Software/main/esp32_i2s_test_tone.hpp b/Software/main/esp32_i2s_test_tone.hpp new file mode 100644 index 0000000..40478ae --- /dev/null +++ b/Software/main/esp32_i2s_test_tone.hpp @@ -0,0 +1,28 @@ +/** + * @file esp32_i2s_test_tone.hpp + * @brief Continuous sine tone over the shared ESP32 -> ADAU1701 I2S TX + * channel, for isolating ADAU1701 I2S-input problems from the + * Si4684 specifically. + * + * DigiRadio firmware — https://github.com/manvalan/DigiRadio + * + * Copyright 2026 Michele Bigi + * SPDX-License-Identifier: Apache-2.0 + */ +#pragma once + +namespace esp32_i2s_test_tone { + +/** + * Generate and write a sine-wave tone over esp32_i2s_sink forever. Intended + * to run as its own FreeRTOS task (never returns). Unlike the ADAU1701's + * internal Beep cell (POST /api/audio/beep), this tone travels over the + * physical SDATA_IN1 wire and the shared BCLK/LRCLK the Si4684 also uses on + * SDATA_IN0 -- if it sounds clean while FM/DAB is distorted, the problem is + * specific to the Si4684 side, not the ADAU1701's I2S input path in general. + * + * @param freqHz Tone frequency in Hz. + */ +[[noreturn]] void run(float freqHz); + +} // namespace esp32_i2s_test_tone diff --git a/Software/main/hardware_bootstrap.cpp b/Software/main/hardware_bootstrap.cpp index 5e11de0..169e210 100644 --- a/Software/main/hardware_bootstrap.cpp +++ b/Software/main/hardware_bootstrap.cpp @@ -160,7 +160,21 @@ std::expected HardwareBootstrap::boot() return {}; } - if (auto tunerResult = gSi4684.boot(si4684::Si4684Band::Dab); !tunerResult) { + // xtalCtun=0, xtalFreqHz=19199750 (2026-08-23): the compiled-in + // defaults (ctun=31, xtal=19200000 nominal) were never measured + // against this board's actual crystal (Abracon ABM8-19.200MHZ-10-1-U-T, + // CL=10pF per part number, plus two external 15pF load caps per the + // schematic). CTUN=0 was found audibly best via A/B listening (0/5/31), + // then xtalFreqHz was trimmed properly using the chip's own FM_RSQ + // FREQOFF measurement (tools/si4684_xtal_calibration.py) against two + // real, GPS-locked broadcast carriers 87.6/105.1 MHz -- converged to + // -3 to -4 ppm residual on both (cross-check confirms it's the + // crystal, not something frequency-dependent), down from +70 ppm + // uncorrected. See POST /api/tuner/xtal-calibrate to re-trim live if + // this ever needs revisiting (e.g. after a board/crystal change). + if (auto tunerResult = + gSi4684.boot(si4684::Si4684Band::Dab, 72U, 0U, 19199750U); + !tunerResult) { ESP_LOGE(kTag, "Si4684 boot failed: error %d", static_cast(tunerResult.error())); return std::unexpected(HardwareBootError::Si4684BootFailed); } diff --git a/Software/main/main.cpp b/Software/main/main.cpp index 9cebc32..0c5473d 100644 --- a/Software/main/main.cpp +++ b/Software/main/main.cpp @@ -45,6 +45,10 @@ #include "i2s_sdata_probe.hpp" #endif +#if CONFIG_ESP32_I2S_TEST_TONE +#include "esp32_i2s_test_tone.hpp" +#endif + namespace { constexpr char kTag[] = "digiradio"; @@ -133,6 +137,14 @@ void heartbeatTask(void* arg) } } +#if CONFIG_ESP32_I2S_TEST_TONE +[[noreturn]] void i2sTestToneTask(void* arg) +{ + (void)arg; + esp32_i2s_test_tone::run(440.0F); +} +#endif + } // namespace /** @@ -213,6 +225,13 @@ extern "C" void app_main() "streaming will be unavailable"); } +#if CONFIG_ESP32_I2S_TEST_TONE + if (xTaskCreate(i2sTestToneTask, "i2s_test_tone", 4096, nullptr, 4, + nullptr) != pdPASS) { + ESP_LOGW(kTag, "ESP32 I2S test tone task create failed"); + } +#endif + auto netResult = net::NetBootstrap::start( store, tunerService, diff --git a/Software/tools/si4684_antenna_calibration.py b/Software/tools/si4684_antenna_calibration.py new file mode 100644 index 0000000..f8e9972 --- /dev/null +++ b/Software/tools/si4684_antenna_calibration.py @@ -0,0 +1,341 @@ +#!/usr/bin/env python3 +"""si4684_antenna_calibration.py — AN851 Appendix A varactor-tuning sweep. + +DigiRadio firmware — https://github.com/manvalan/DigiRadio + +Copyright 2026 Michele Bigi +SPDX-License-Identifier: Apache-2.0 + +Runs the Si4684 antenna varactor calibration procedure described in AN851 +("Si468x AM/AMHD-FM/FMHD-DAB/DAB+ antenna/matching network design +guidelines"), Appendix A, driving the device's existing HTTP tuner API +instead of a bench Test_Get_RSSI command: + + 1. At each of several test frequencies, sweep ANTCAP from --antcap-min + to --antcap-max (AN851: 1-128) via POST /api/tuner/tune, averaging + --samples (AN851: 5) reads per step via GET /api/tuner/status. + 2. Pick the ANTCAP with the best average reading at each frequency. + 3. Linear-fit best_antcap(frequency_MHz) = (m/1000)*frequency_MHz + b, + AN851's own formula, giving VARM (m, property 0x1710) and VARB + (b, property 0x1711) as signed 16-bit integers. + +Scope and limits (read before running): + +* This firmware's HTTP API has no endpoint to write VARM/VARB directly + (see components/net/src/SetupWebServer.cpp) -- only a single fixed + ANTCAP override can be persisted, via POST /api/tuner/calibrate-antenna. + This script only COMPUTES the fit and prints it; it does not write + anything to the device. Applying the result means hand-editing the + kFmTuneFeVarm/kFmTuneFeVarb (or the DAB kDabProps table) constants in + components/drivers/si4684/src/Si4684Driver.cpp and reflashing. + +* DAB caveat: DAB_TUNE_FREQ (AN649 Command 0xB0) addresses frequencies by + an index into a chip-side table (default "European frequency list") + loaded via DAB_SET_FREQ_LIST. AN649's copy in this repo does not publish + that table's contents, and neither the firmware nor its HTTP API expose + the real MHz for a given freq_index. This script does NOT assume any + index<->MHz mapping -- for --band dab you must supply the real + frequency for each index yourself (e.g. read off a known ensemble's + published transmitter frequency). Do not guess; a wrong MHz value here + silently produces a wrong VARM/VARB fit. + +* DAB metric caveat: AN851 Appendix A calls for RSSI at each step, but + this firmware's DAB status JSON does not expose RSSI (only + dab.fic_quality and dab.cnr_db -- see core/TunerStatus.hpp). This + script uses dab.cnr_db as the optimization metric for --band dab + instead. That is a deliberate substitution, not a datasheet value -- + treat DAB fit results with more skepticism than FM ones. + +* Known dead zone (docs/si4684-rf-investigation-report.md, 2026-08-20): + DAB freq_index 22 lost lock entirely at antcap 72 and 80 on this board. + This script does not skip any antcap value in range; a step with no + lock is logged as "NO LOCK" and simply excluded from that point's + average, not treated as a fatal error. + +Usage: + python3 tools/si4684_antenna_calibration.py --host digiradio-XXXXXX.local \\ + --band fm --point 88500 --point 98000 --point 107900 + + python3 tools/si4684_antenna_calibration.py --host 192.168.1.56 \\ + --band dab --point 5:174928 --point 12:198160 --point 23:225648 \\ + --raw-csv dab_sweep.csv +""" + +from __future__ import annotations + +import argparse +import csv +import json +import socket +import statistics +import sys +import time +import urllib.error +import urllib.request +from pathlib import Path +from typing import Optional + + +def parse_point(band: str, raw: str) -> dict: + """Parse one --point argument into a tune target for the given band.""" + if band == "dab": + if ":" not in raw: + raise ValueError( + "DAB points must be 'freq_index:frequency_khz', e.g. 12:198160" + ) + idx_s, khz_s = raw.split(":", 1) + idx = int(idx_s) + khz = int(khz_s) + if not 0 <= idx <= 37: + raise ValueError("freq_index must be 0-37 (core/TunerJson.cpp limit)") + if khz <= 0: + raise ValueError("frequency_khz must be positive") + return { + "index": idx, + "khz": khz, + "mhz": khz / 1000.0, + "label": f"idx{idx}@{khz / 1000:.3f}MHz", + } + khz = int(raw) + if not 87500 <= khz <= 108000: + raise ValueError("FM frequency_khz should be within 87500-108000") + return {"khz": khz, "mhz": khz / 1000.0, "label": f"{khz / 1000:.3f}MHz"} + + +def tune_payload(band: str, point: dict, antcap: int) -> dict: + if band == "dab": + return {"band": "dab", "freq_index": point["index"], "antcap": antcap} + return {"band": "fm", "frequency_khz": point["khz"], "antcap": antcap} + + +def extract_metric(status: dict, band: str) -> Optional[float]: + """AN851's optimization metric: RSSI for FM, CNR for DAB (see docstring).""" + if not status.get("locked", False): + return None + if band == "fm": + fm = status.get("fm") or {} + rssi = fm.get("rssi_dbuv") + return float(rssi) if rssi is not None else None + dab = status.get("dab") or {} + cnr = dab.get("cnr_db") + return float(cnr) if cnr is not None else None + + +def http_request( + host: str, + method: str, + path: str, + payload: Optional[dict], + timeout: float, + retries: int, +) -> dict: + url = f"http://{host}{path}" + # The firmware's JSON parser (components/core/src/TunerJson.cpp) does + # plain substring search for e.g. `"key":` and is not whitespace- + # tolerant -- json.dumps()'s default ": "/", " separators break every + # request with "invalid_json". Compact separators match what curl's + # -d with no spaces sends, which the firmware does accept. + data = json.dumps(payload, separators=(",", ":")).encode("utf-8") if payload is not None else None + headers = {"Content-Type": "application/json"} if data is not None else {} + last_err: Optional[BaseException] = None + for attempt in range(retries + 1): + try: + req = urllib.request.Request(url, data=data, method=method, headers=headers) + with urllib.request.urlopen(req, timeout=timeout) as resp: + body = resp.read() + return json.loads(body) if body else {} + except (urllib.error.URLError, socket.timeout, TimeoutError) as exc: + last_err = exc + if attempt < retries: + time.sleep(1.0 + attempt) # device is known to stall briefly under HTTP load + raise RuntimeError(f"{method} {path} failed after {retries + 1} attempt(s): {last_err}") + + +def sweep_point( + host: str, + band: str, + point: dict, + antcap_values: list, + samples: int, + settle_s: float, + sample_gap_s: float, + timeout: float, + retries: int, + raw_rows: Optional[list], +) -> list: + results = [] + for antcap in antcap_values: + try: + status = http_request( + host, "POST", "/api/tuner/tune", tune_payload(band, point, antcap), timeout, retries + ) + except RuntimeError as exc: + print(f" antcap={antcap:3d} tune failed: {exc}", file=sys.stderr) + results.append((antcap, None)) + continue + + time.sleep(settle_s) + readings = [] + first = extract_metric(status, band) + if first is not None: + readings.append(first) + for _ in range(max(0, samples - 1)): + time.sleep(sample_gap_s) + try: + status = http_request(host, "GET", "/api/tuner/status", None, timeout, retries) + except RuntimeError as exc: + print(f" antcap={antcap:3d} status read failed: {exc}", file=sys.stderr) + continue + reading = extract_metric(status, band) + if reading is not None: + readings.append(reading) + + if raw_rows is not None: + for i, reading in enumerate(readings): + raw_rows.append( + {"point": point["label"], "antcap": antcap, "sample": i, "metric": reading} + ) + + avg = statistics.mean(readings) if readings else None + state = "locked" if readings else "NO LOCK" + avg_str = f"{avg:6.2f}" if avg is not None else " - " + print(f" antcap={antcap:3d} samples={len(readings)}/{samples} avg={avg_str} [{state}]") + results.append((antcap, avg)) + return results + + +def linear_fit(xs: list, ys: list) -> Optional[tuple]: + n = len(xs) + if n < 2: + return None + mean_x = sum(xs) / n + mean_y = sum(ys) / n + num = sum((x - mean_x) * (y - mean_y) for x, y in zip(xs, ys)) + den = sum((x - mean_x) ** 2 for x in xs) + if den == 0: + return None + slope = num / den + intercept = mean_y - slope * mean_x + return slope, intercept + + +def main() -> int: + parser = argparse.ArgumentParser( + description="AN851 Appendix A ANTCAP sweep + VARM/VARB linear fit over the tuner HTTP API.", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=__doc__, + ) + parser.add_argument("--host", required=True, help="Device IP or mDNS host, e.g. digiradio-XXXXXX.local") + parser.add_argument("--band", required=True, choices=["fm", "dab"]) + parser.add_argument( + "--point", + action="append", + required=True, + metavar="FREQ_KHZ|INDEX:FREQ_KHZ", + help="FM: frequency in kHz (e.g. 98000). DAB: freq_index:frequency_khz " + "(e.g. 12:198160) -- see script docstring for why the MHz value must " + "come from you, not this script.", + ) + parser.add_argument("--antcap-min", type=int, default=1) + parser.add_argument("--antcap-max", type=int, default=128) + parser.add_argument("--antcap-step", type=int, default=1) + parser.add_argument( + "--samples", type=int, default=5, help="Reads averaged per ANTCAP step (AN851 Appendix A: 5)" + ) + parser.add_argument( + "--settle-ms", + type=int, + default=150, + help="Delay after tune before the first reading (engineering margin, not from AN851)", + ) + parser.add_argument("--sample-gap-ms", type=int, default=80, help="Delay between repeated status reads") + parser.add_argument("--timeout", type=float, default=10.0, help="HTTP timeout per request, seconds") + parser.add_argument( + "--retries", type=int, default=2, help="Retries per request (device is known to stall briefly)" + ) + parser.add_argument("--raw-csv", type=Path, default=None, help="Optional path to dump every raw sample") + args = parser.parse_args() + + if not 1 <= args.antcap_min <= args.antcap_max <= 128: + print("error: antcap range must be within 1-128 (0 = auto, not part of the sweep)", file=sys.stderr) + return 1 + + points = [] + for raw in args.point: + try: + points.append(parse_point(args.band, raw)) + except ValueError as exc: + print(f"error: invalid --point {raw!r}: {exc}", file=sys.stderr) + return 1 + + antcap_values = list(range(args.antcap_min, args.antcap_max + 1, args.antcap_step)) + raw_rows: Optional[list] = [] if args.raw_csv else None + + fit_points = [] + for point in points: + print(f"\n=== sweeping {args.band} @ {point['label']} ===") + results = sweep_point( + args.host, + args.band, + point, + antcap_values, + args.samples, + args.settle_ms / 1000.0, + args.sample_gap_ms / 1000.0, + args.timeout, + args.retries, + raw_rows, + ) + valid = [(a, v) for a, v in results if v is not None] + if not valid: + print(f" WARNING: no locked reading anywhere -- excluding {point['label']} from the fit") + continue + best_antcap, best_val = max(valid, key=lambda t: t[1]) + print(f" best: antcap={best_antcap} ({best_val:.2f})") + fit_points.append((point["mhz"], best_antcap, point["label"])) + + if args.raw_csv: + with args.raw_csv.open("w", newline="") as f: + writer = csv.DictWriter(f, fieldnames=["point", "antcap", "sample", "metric"]) + writer.writeheader() + writer.writerows(raw_rows) + print(f"\nraw samples written to {args.raw_csv}") + + print(f"\n=== AN851 Appendix A fit (band={args.band}) ===") + if len(fit_points) < 2: + print( + "Not enough locked points to fit VARM/VARB (need >= 2 usable frequencies). " + "Try different --point values or check the antenna/signal." + ) + return 1 + + xs = [p[0] for p in fit_points] + ys = [p[1] for p in fit_points] + fit = linear_fit(xs, ys) + if fit is None: + print("Fit failed (degenerate frequency set -- all points at the same MHz?).") + return 1 + + slope, intercept = fit + m = round(slope * 1000) + b = round(intercept) + m_ok = -32768 <= m <= 32767 + b_ok = -32768 <= b <= 32767 + + print(" points used: " + ", ".join(f"{label} -> antcap {a}" for _, a, label in fit_points)) + print(" varactor_value = (m/1000)*frequency_MHz + b [AN851 Appendix A]") + print(f" m (slope x1000, property 0x1710) = {m}" + ("" if m_ok else " ** OUT OF int16 RANGE **")) + print(f" b (intercept, property 0x1711) = {b}" + ("" if b_ok else " ** OUT OF int16 RANGE **")) + if m_ok and b_ok: + print(f" -> 0x1710 = 0x{m & 0xFFFF:04X} 0x1711 = 0x{b & 0xFFFF:04X}") + print() + print(" Not written to the device. To apply: edit the k{Fm,Dab}TuneFeVarm/") + print(" k{Fm,Dab}TuneFeVarb constants in components/drivers/si4684/src/Si4684Driver.cpp") + print(" (current values and AN851 Appendix B citation are next to them), reflash, then") + print(" re-run this sweep with antcap=0 (auto) at the same points to confirm auto-tune") + print(" now tracks the measured optimum.") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/Software/tools/si4684_xtal_calibration.py b/Software/tools/si4684_xtal_calibration.py new file mode 100644 index 0000000..97ce6e3 --- /dev/null +++ b/Software/tools/si4684_xtal_calibration.py @@ -0,0 +1,184 @@ +#!/usr/bin/env python3 +"""si4684_xtal_calibration.py — live Si4684 crystal trim via FM_RSQ FREQOFF. + +DigiRadio firmware — https://github.com/manvalan/DigiRadio + +Copyright 2026 Michele Bigi +SPDX-License-Identifier: Apache-2.0 + +Uses the Si4684's own FM_RSQ_STATUS FREQOFF field (AN649 Command 0x32, +RESP8, signed offset in units of 2 PPM) as a precision frequency reference +-- real FM broadcast transmitters are GPS/rubidium-locked, so a locked +station's carrier reads the same PPM error as the receiver's own crystal +reference error, no lab equipment required. + +Procedure (matches AN649 §9.3's own recommendation to trim by measurement, +just automated over HTTP instead of by ear): + + 1. Tune to a strong, stable FM station. + 2. Read GET /api/tuner/status -> fm.freqoff_ppm. + 3. new_xtal_freq_hz = 19200000 * (1 + ppm/1e6) + 4. POST /api/tuner/xtal-calibrate {"xtal_freq_hz": new_xtal_freq_hz} + (this reboots the Si4684, no ESP32 restart, no persistence yet). + 5. Re-tune, re-read freqoff_ppm. Repeat until it converges near 0. + 6. Optionally cross-check on a second station at the other end of the + band -- if the residual offset differs systematically between the two, + the error isn't (only) the crystal; don't chase it further with this + tool. + +Nothing here is persisted to flash. Once you have a converged xtal_freq_hz, +hand it to a human/Claude to bake into the Si4684Driver::boot() default +call in main/hardware_bootstrap.cpp -- this script only calibrates the +currently-running session. + +Usage: + python3 tools/si4684_xtal_calibration.py --host 192.168.1.62 \\ + --frequency-khz 87600 + + python3 tools/si4684_xtal_calibration.py --host 192.168.1.62 \\ + --frequency-khz 87600 --once # single read, no correction loop +""" + +from __future__ import annotations + +import argparse +import json +import sys +import time +import urllib.error +import urllib.request +from typing import Optional + + +def http_request(host: str, method: str, path: str, payload: Optional[dict], + timeout: float) -> dict: + url = f"http://{host}{path}" + # Compact separators: the firmware's hand-rolled JSON parser is not + # whitespace-tolerant (confirmed bug in components/core/src/TunerJson.cpp). + data = json.dumps(payload, separators=(",", ":")).encode() if payload is not None else None + headers = {"Content-Type": "application/json"} if data is not None else {} + req = urllib.request.Request(url, data=data, method=method, headers=headers) + with urllib.request.urlopen(req, timeout=timeout) as resp: + body = resp.read() + return json.loads(body) if body else {} + + +def tune_fm(host: str, frequency_khz: int, timeout: float) -> dict: + return http_request(host, "POST", "/api/tuner/tune", + {"band": "fm", "frequency_khz": frequency_khz}, timeout) + + +def read_status(host: str, timeout: float) -> dict: + return http_request(host, "GET", "/api/tuner/status", None, timeout) + + +def recalibrate(host: str, xtal_freq_hz: int, ibias: int, ctun: int, + timeout: float) -> dict: + return http_request( + host, "POST", "/api/tuner/xtal-calibrate", + {"xtal_freq_hz": xtal_freq_hz, "ibias": ibias, "ctun": ctun}, timeout) + + +def main() -> int: + parser = argparse.ArgumentParser( + description="Trim Si4684 XTAL_FREQ live using FM_RSQ_STATUS FREQOFF " + "-- no lab equipment, uses locked broadcast carriers as reference.") + parser.add_argument("--host", required=True, help="Device IP or mDNS host") + parser.add_argument("--frequency-khz", type=int, required=True, + help="A strong, stable FM station to lock onto, e.g. 87600") + parser.add_argument("--ibias", type=int, default=72, help="POWER_UP IBIAS (0-127)") + parser.add_argument("--ctun", type=int, default=0, + help="POWER_UP CTUN (0-63) -- default 0, the best value " + "found by ear on 2026-08-23; leave alone unless you have " + "a reason to also move it") + parser.add_argument("--start-xtal-freq-hz", type=int, default=19200000, + help="Starting XTAL_FREQ before the first correction") + parser.add_argument("--max-iterations", type=int, default=6) + parser.add_argument("--converge-ppm", type=float, default=1.0, + help="Stop once |freqoff_ppm| is under this many ppm") + parser.add_argument("--settle-s", type=float, default=1.0, + help="Delay after tune/recalibrate before reading status") + parser.add_argument("--samples", type=int, default=5, + help="FREQOFF reads averaged per iteration (reduces " + "reception-noise jitter in the ppm estimate)") + parser.add_argument("--sample-gap-s", type=float, default=0.3) + parser.add_argument("--damping", type=float, default=0.6, + help="Fraction of the measured ppm correction applied " + "per iteration (<1.0 avoids overshoot/oscillation " + "around the true value)") + parser.add_argument("--timeout", type=float, default=10.0) + parser.add_argument("--once", action="store_true", + help="Single read only, no correction loop") + args = parser.parse_args() + + xtal_freq_hz = args.start_xtal_freq_hz + + for iteration in range(1 if args.once else args.max_iterations): + try: + recalibrate(args.host, xtal_freq_hz, args.ibias, args.ctun, args.timeout) + except (urllib.error.URLError, OSError) as exc: + print(f"error: xtal-calibrate failed: {exc}", file=sys.stderr) + return 1 + time.sleep(args.settle_s) + + try: + status = tune_fm(args.host, args.frequency_khz, args.timeout) + except (urllib.error.URLError, OSError) as exc: + print(f"error: tune failed: {exc}", file=sys.stderr) + return 1 + time.sleep(args.settle_s) + + samples = [] + for s in range(args.samples): + if s > 0: + time.sleep(args.sample_gap_s) + try: + status = read_status(args.host, args.timeout) + except (urllib.error.URLError, OSError) as exc: + print(f"error: status read failed: {exc}", file=sys.stderr) + return 1 + fm = status.get("fm") or {} + if status.get("locked", False) and fm.get("freqoff_ppm") is not None: + samples.append((fm["freqoff_ppm"], fm.get("rssi_dbuv"), fm.get("snr_db"))) + + if not samples: + print(" no lock / no FREQOFF reading across all samples -- pick " + "a stronger station and retry", file=sys.stderr) + return 1 + + ppm_avg = sum(s[0] for s in samples) / len(samples) + rssi = samples[-1][1] + snr = samples[-1][2] + print(f"iter {iteration}: xtal_freq_hz={xtal_freq_hz} " + f"samples={len(samples)}/{args.samples} rssi={rssi} snr={snr} " + f"freqoff_ppm_avg={ppm_avg:.1f} " + f"raw={[s[0] for s in samples]}") + + if args.once: + return 0 + + if abs(ppm_avg) <= args.converge_ppm: + print(f"\nConverged: xtal_freq_hz={xtal_freq_hz} " + f"(residual {ppm_avg:.1f} ppm avg, ibias={args.ibias}, " + f"ctun={args.ctun})") + print("Not persisted -- to make this the boot default, edit the " + "gSi4684.boot(...) call in main/hardware_bootstrap.cpp.") + return 0 + + # Empirically determined 2026-08-23: correcting in the "+ppm" + # direction diverges (each iteration made freqoff_ppm larger, not + # smaller). The correct sign is "-ppm" -- confirmed by a live A/B + # test, not derived from AN649 (which doesn't specify the relation + # between XTAL_FREQ and FREQOFF's sign convention). Damping avoids + # overshoot from single-reading reception noise. + xtal_freq_hz = round(xtal_freq_hz * (1.0 - args.damping * ppm_avg / 1e6)) + + print(f"\nDid not converge within {args.max_iterations} iterations " + f"(last xtal_freq_hz={xtal_freq_hz}). Try again, or check the " + f"second-station cross-check described in this script's docstring " + f"-- a residual that varies with frequency isn't the crystal.") + return 1 + + +if __name__ == "__main__": + raise SystemExit(main()) From 38e768edbb45d0ae41428a02d7afcbb804b4c35b Mon Sep 17 00:00:00 2001 From: Michele Bigi Date: Mon, 24 Aug 2026 14:37:57 +0200 Subject: [PATCH 10/12] Fix ADAU1701 SerialInputRegister IBP override causing persistent hiss The 2026-08-16 IBP=1 override (commit 6974095) was applied alongside a separate, simultaneous fix to Si4684's PIN_CONFIG_ENABLE and credited with turning static into audible music. SerialInputRegister (0x081F) is a single register shared by every SDATA_INx pin on the ADAU1701, so the override also applied to the ESP32 streaming input, not just Si4684's. Live A/B testing today (DAB, FM, and ESP32 web-radio, all isolated via mixer gain, with and without ADAU1701 DSP bypass, with and without BT A2DP codec changes) narrowed the hiss to this one shared register. Removing the override and leaving IBP at its compiled default (0x00) resolved the hiss on both the Si4684 and ESP32 paths, confirmed by ear. Also: - Add a runtime HTTP API (GET/POST /api/bluetooth/a2dp-codec) to change the BT1035 A2DP codec bitmask without reflashing, used to rule out AAC/SBC codec choice as a contributing cause. - Extend the ADAU1701 boot-time EQ diagnostic to read back all 6 bands (previously band 0 only) using the correct 5.23 fixed-point format. - Note EEPROM persistence for ANTCAP/crystal calibration as TODO. Co-Authored-By: Claude Sonnet 5 --- .../core/include/core/BluetoothJson.hpp | 28 ++++ .../components/core/include/core/Bt1035At.hpp | 60 ++++++-- .../components/core/src/BluetoothJson.cpp | 27 ++++ Software/components/core/src/Bt1035At.cpp | 17 ++- .../components/core/test/bt1035_at_test.cpp | 18 ++- .../drivers/adau1701/src/Adau1701Driver.cpp | 140 +++++++++++++----- .../bt1035/include/bt1035/Bt1035Driver.hpp | 32 +++- .../drivers/bt1035/src/Bt1035Driver.cpp | 108 +++++++++----- .../components/net/src/SetupWebServer.cpp | 73 +++++++++ .../include/bluetooth/BluetoothService.hpp | 24 +++ .../bluetooth/src/BluetoothService.cpp | 12 ++ Software/docs/TODO.md | 20 ++- 12 files changed, 460 insertions(+), 99 deletions(-) diff --git a/Software/components/core/include/core/BluetoothJson.hpp b/Software/components/core/include/core/BluetoothJson.hpp index 80c2842..84d088c 100644 --- a/Software/components/core/include/core/BluetoothJson.hpp +++ b/Software/components/core/include/core/BluetoothJson.hpp @@ -112,6 +112,34 @@ struct BluetoothConnectRequest { [[nodiscard]] std::expected parseBluetoothAutoReconnectJson(std::string_view json); +/** + * @brief parseBluetoothA2dpCodecConfigJson — validate POST codec_mask field. + * + * @dname parseBluetoothA2dpCodecConfigJson + * @param json Request body with \c codec_mask 0–63 (see AT+A2DPCFG bits). + * @return Bitmask on success, or ParseError. + * @pubstate none + * + * @author Michele Bigi + * @date 2026-08-24 + */ +[[nodiscard]] std::expected +parseBluetoothA2dpCodecConfigJson(std::string_view json); + +/** + * @brief serializeBluetoothA2dpCodecJson — negotiated codec for HTTP. + * + * @dname serializeBluetoothA2dpCodecJson + * @param codec Parsed negotiated codec (AT+A2DPENC reply). + * @return JSON object \c {"codec":"..."}. + * @pubstate none + * + * @author Michele Bigi + * @date 2026-08-24 + */ +[[nodiscard]] std::string serializeBluetoothA2dpCodecJson( + Bt1035A2dpCodec codec); + /** * @brief serializeBluetoothScanJson — serialise scan result list. * diff --git a/Software/components/core/include/core/Bt1035At.hpp b/Software/components/core/include/core/Bt1035At.hpp index 1e63175..a52dc54 100644 --- a/Software/components/core/include/core/Bt1035At.hpp +++ b/Software/components/core/include/core/Bt1035At.hpp @@ -40,7 +40,8 @@ enum class Bt1035AtCommand { Reset, ///< AT+RESET — software reset (best-effort, not gated on OK). Ping, ///< AT — link check. I2sMode, ///< AT+AUXCFG=3 — I2S input from ADAU1701 (mandatory). - I2sSlave48k24, ///< AT+I2SCFG=35 — I2S slave 48 kHz 24-bit (§5.1.4). + I2sSlave48k32, ///< AT+I2SCFG=67 — I2S slave 48 kHz 32-bit (§5.1.4). + A2dpCodecConfig, ///< AT+A2DPCFG=1 — enable AAC alongside mandatory SBC (§5.3.4). PairDiscoverable, ///< AT+PAIR=1 — enter BR/EDR/BLE discoverable mode. PairHidden, ///< AT+PAIR=0 — leave discoverable mode. A2dpStat, ///< AT+A2DPSTAT — read A2DP link state. @@ -105,16 +106,40 @@ enum class Bt1035AtResponseKind { }; /** Number of commands in bootInitSequence(). */ -inline constexpr std::size_t kBt1035BootInitCommandCount = 3U; +inline constexpr std::size_t kBt1035BootInitCommandCount = 4U; /** - * Feasycom programming guide §5.1.4: I2S slave, 48 kHz, 24-bit — matches the - * ADAU1701 serial output word length (SigmaStudio Hardware Configuration). - * Bit field: BIT[0]=enable(1), BIT[1]=slave(1), BIT[2]=FS 48kHz(0), - * BIT[3]=left justified(0), BIT[4]=data 1-bit delay(0), BIT[5:6]=24-bit(01) - * -> 1 + 2 + 32 = 35. + * Feasycom programming guide §5.3.4: AT+A2DPCFG bit field enables optional + * codecs on top of the mandatory baseline SBC (BIT0=AAC, BIT1=aptX, + * BIT2=aptX-LL, BIT3=aptX-HD, BIT4=aptX-Adaptive, BIT5=LDAC; BT1035 does + * not support BT806's FastStream). Never sent before 2026-08-24, so every + * A2DP link negotiated SBC only regardless of what the paired speaker + * supports. Value 1 = AAC only, matching the guide's own worked example + * -- picked over a wider bitmask because AAC is the most broadly + * supported optional codec on consumer speakers (native on iOS) and this + * is the datasheet-validated example, not an untested combination. */ -inline constexpr std::uint8_t kBt1035I2sSlave48k24Param = 35U; +inline constexpr std::uint8_t kBt1035A2dpCodecConfigParam = 1U; + +/** + * Feasycom programming guide §5.1.4: I2S slave, 48 kHz, 32-bit -> value 67. + * This is one of only two configurations the guide validates with explicit + * bit-clock math (the other being value 3, 16-bit); 24-bit (35, used here + * until 2026-08-24) is not a documented example. Matches the ADAU1701's + * actual physical output framing: the Serial Output Control Register + * (0x081E, R15_BCLK_FREQ/R15_LRCLK_FREQ fields in the compiled SigmaStudio + * export) fixes BCLK=3.072 MHz / LRCLK=48 kHz regardless of the OWL + * "word length" field -- i.e. a 64-BCLK-cycle (32-bit) frame is what's + * physically on the wire no matter what OWL says, so 67 is the value that + * matches reality; 35 (24-bit) silently regressed in commit 6f7b6dd + * (2026-08-08, an unrelated large feature commit) and was found to + * contradict every other citation of this value in the repo + * (instructions.md, README.md, CLAUDE.md, AGENTS.md, docs/TODO.md). + * Bit field: BIT[0]=enable(1), BIT[1]=slave(1), BIT[2]=FS 48kHz(0), + * BIT[3]=left justified(0), BIT[4]=data 1-bit delay(0), BIT[5:6]=32-bit(10) + * -> 1 + 2 + 64 = 67. + */ +inline constexpr std::uint8_t kBt1035I2sSlave48k32Param = 67U; /** * @brief buildBt1035AtLine — serialise a command with CRLF terminator. @@ -133,7 +158,7 @@ inline constexpr std::uint8_t kBt1035I2sSlave48k24Param = 35U; * @brief bootInitSequence — mandatory bring-up commands in order. * * @dname bootInitSequence - * @return Ping, I2sMode (AUXCFG=3), I2sSlave48k24 (I2SCFG=35). + * @return Ping, I2sMode (AUXCFG=3), I2sSlave48k32 (I2SCFG=67). * @pubstate none * * @author Michele Bigi @@ -210,6 +235,23 @@ parseBt1035A2dpEncoderResponse(std::string_view response); */ [[nodiscard]] const char* a2dpCodecToken(Bt1035A2dpCodec codec) noexcept; +/** + * @brief buildBt1035A2dpCodecConfigLine — AT+A2DPCFG with runtime bitmask. + * + * @dname buildBt1035A2dpCodecConfigLine + * @param bitmask BIT0=AAC, BIT1=aptX, BIT2=aptX-LL, BIT3=aptX-HD, + * BIT4=aptX-Adaptive, BIT5=LDAC (§5.3.4); clamped to 0-63. + * 0 disables every optional codec (SBC-only baseline). + * @return Full AT line including CRLF. + * @pubstate Takes effect on the next A2DP negotiation, not an already + * streaming link — the peer must reconnect for the change to + * apply. + * + * @author Michele Bigi + * @date 2026-08-24 + */ +[[nodiscard]] std::string buildBt1035A2dpCodecConfigLine(std::uint8_t bitmask); + /** * @brief buildBt1035SetAutoConnLine — AT+AUTOCONN with reconnect count. * diff --git a/Software/components/core/src/BluetoothJson.cpp b/Software/components/core/src/BluetoothJson.cpp index 1722f94..a126130 100644 --- a/Software/components/core/src/BluetoothJson.cpp +++ b/Software/components/core/src/BluetoothJson.cpp @@ -96,6 +96,33 @@ parseBluetoothAutoReconnectJson(std::string_view json) return static_cast(raw); } +std::expected +parseBluetoothA2dpCodecConfigJson(std::string_view json) +{ + if (json.find('{') == std::string_view::npos) { + return std::unexpected(ParseError::InvalidJson); + } + const std::string needle = "\"codec_mask\":"; + const std::size_t start = json.find(needle); + if (start == std::string_view::npos) { + return std::unexpected(ParseError::MissingField); + } + char* end = nullptr; + const unsigned long raw = + std::strtoul(json.data() + start + needle.size(), &end, 10); + if (end == json.data() + start + needle.size() || raw > 63U) { + return std::unexpected(ParseError::InvalidJson); + } + return static_cast(raw); +} + +std::string serializeBluetoothA2dpCodecJson(Bt1035A2dpCodec codec) +{ + std::ostringstream out; + out << "{\"codec\":\"" << a2dpCodecToken(codec) << "\"}"; + return out.str(); +} + std::string serializeBluetoothScanJson( const std::vector& devices) { diff --git a/Software/components/core/src/Bt1035At.cpp b/Software/components/core/src/Bt1035At.cpp index 2121052..11de00d 100644 --- a/Software/components/core/src/Bt1035At.cpp +++ b/Software/components/core/src/Bt1035At.cpp @@ -106,8 +106,10 @@ std::string buildBt1035AtLine(Bt1035AtCommand command) return "AT\r\n"; case Bt1035AtCommand::I2sMode: return "AT+AUXCFG=3\r\n"; - case Bt1035AtCommand::I2sSlave48k24: - return "AT+I2SCFG=35\r\n"; + case Bt1035AtCommand::I2sSlave48k32: + return "AT+I2SCFG=67\r\n"; + case Bt1035AtCommand::A2dpCodecConfig: + return buildBt1035A2dpCodecConfigLine(kBt1035A2dpCodecConfigParam); case Bt1035AtCommand::PairDiscoverable: return "AT+PAIR=1\r\n"; case Bt1035AtCommand::PairHidden: @@ -128,6 +130,14 @@ std::string buildBt1035AtLine(Bt1035AtCommand command) return "AT\r\n"; } +std::string buildBt1035A2dpCodecConfigLine(std::uint8_t bitmask) +{ + if (bitmask > 63U) { + bitmask = 63U; + } + return "AT+A2DPCFG=" + std::to_string(bitmask) + "\r\n"; +} + std::string buildBt1035SetAutoConnLine(std::uint8_t times) { if (times > 15U) { @@ -150,7 +160,8 @@ std::array bootInitSequence() noex return std::array{ Bt1035AtCommand::Ping, Bt1035AtCommand::I2sMode, - Bt1035AtCommand::I2sSlave48k24, + Bt1035AtCommand::I2sSlave48k32, + Bt1035AtCommand::A2dpCodecConfig, }; } diff --git a/Software/components/core/test/bt1035_at_test.cpp b/Software/components/core/test/bt1035_at_test.cpp index f529165..8c7ed96 100644 --- a/Software/components/core/test/bt1035_at_test.cpp +++ b/Software/components/core/test/bt1035_at_test.cpp @@ -33,7 +33,7 @@ namespace { std::cerr << "I2S mode must be in init sequence\n"; return EXIT_FAILURE; } - if (sequence[2U] != core::Bt1035AtCommand::I2sSlave48k24) { + if (sequence[2U] != core::Bt1035AtCommand::I2sSlave48k32) { std::cerr << "I2SCFG must be in init sequence\n"; return EXIT_FAILURE; } @@ -44,9 +44,19 @@ namespace { return EXIT_FAILURE; } const std::string i2sCfg = - core::buildBt1035AtLine(core::Bt1035AtCommand::I2sSlave48k24); - if (i2sCfg != "AT+I2SCFG=35\r\n") { - std::cerr << "I2SCFG=35 command line mismatch\n"; + core::buildBt1035AtLine(core::Bt1035AtCommand::I2sSlave48k32); + if (i2sCfg != "AT+I2SCFG=67\r\n") { + std::cerr << "I2SCFG=67 command line mismatch\n"; + return EXIT_FAILURE; + } + if (sequence[3U] != core::Bt1035AtCommand::A2dpCodecConfig) { + std::cerr << "A2DPCFG must be in init sequence\n"; + return EXIT_FAILURE; + } + const std::string a2dpCfg = + core::buildBt1035AtLine(core::Bt1035AtCommand::A2dpCodecConfig); + if (a2dpCfg != "AT+A2DPCFG=1\r\n") { + std::cerr << "A2DPCFG=1 command line mismatch\n"; return EXIT_FAILURE; } const std::string reset = diff --git a/Software/components/drivers/adau1701/src/Adau1701Driver.cpp b/Software/components/drivers/adau1701/src/Adau1701Driver.cpp index a664f70..d0924db 100644 --- a/Software/components/drivers/adau1701/src/Adau1701Driver.cpp +++ b/Software/components/drivers/adau1701/src/Adau1701Driver.cpp @@ -182,69 +182,137 @@ namespace adau1701 return replay; } - // Diagnostic: log EQ band 0's live Param RAM contents. This band is - // the fixed high-pass (SigmaStudio band 1) and applyEq() always - // skips it -- no runtime code path ever safeloads it, so whatever - // landed here at program-load time is what plays, permanently, - // regardless of the audio-profile API (which reports a fictional - // value for it). Read straight from the chip rather than trusting - // the compiled source, in case the two have ever diverged. + // Diagnostic (2026-08-24, extended from band-0-only): log EVERY EQ + // band's live Param RAM contents (bands 0-5, 5 coefficients each -- + // B0,B1,B2,A0,A1 per paramAddrEqBandBase's 5-word stride). Band 0 + // is the fixed high-pass that applyEq() always skips (whatever + // landed at program-load time plays permanently); bands 1-5 are + // runtime-safeloaded whenever the audio profile has a nonzero + // gain, but with the factory-default flat profile (all gains 0) + // they should read back as the identity biquad (B0=0x00800000=1.0, + // rest 0) written by designFlatEq(). A single steady test tone at + // one frequency cannot reveal a bad coefficient elsewhere in a + // band's response curve -- reading the actual RAM contents is the + // only way to confirm what's really there, not what the software + // believes it wrote. Decode as 5.23 (28-bit, matches the ADAU1701's + // real Param RAM format, NOT a naive 32-bit Q8.23 -- see the + // 2026-08-23 band-0 false alarm in + // docs/si4684-rf-investigation-report.md for why that distinction + // matters). + for (unsigned band = 0U; band < 6U; ++band) { - const unsigned baseAddr = paramAddrEqBandBase(0U); + const unsigned baseAddr = paramAddrEqBandBase( + static_cast(band)); for (unsigned i = 0U; i < 5U; ++i) { unsigned char raw[4U] = {0U, 0U, 0U, 0U}; if (sigma_i2c_read(baseAddr + i, raw, sizeof(raw)) == 0) { - const std::int32_t fixpoint = + std::int32_t fixpoint = static_cast( (static_cast(raw[0]) << 24) | (static_cast(raw[1]) << 16) | (static_cast(raw[2]) << 8) | static_cast(raw[3])); + // Sign-extend from bit 27 (28-bit/5.23 format). + if ((fixpoint & (1 << 27)) != 0) + { + fixpoint -= (1 << 28); + } ESP_LOGI(kTag, - "EQ band0 param[%u] addr=0x%04X raw=0x%08X " + "EQ band%u param[%u] addr=0x%04X raw=0x%08X " "value=%f", - i, baseAddr + i, - static_cast(fixpoint), + band, i, baseAddr + i, + static_cast( + (static_cast(raw[0]) << 24) + | (static_cast(raw[1]) << 16) + | (static_cast(raw[2]) << 8) + | static_cast(raw[3])), static_cast(fixpoint) / static_cast(1U << 23)); } else { - ESP_LOGW(kTag, "EQ band0 param[%u] read-back failed", i); + ESP_LOGW(kTag, "EQ band%u param[%u] read-back failed", + band, i); } } } - // SerialInputRegister (0x081F) override: bit3 IBP=1, matching the - // BCLK edge the Si4684's I2S output actually changes data on - // (compiled DSP program default is 0x00 = IBP=0, which produced - // pure static on a strong locked signal). ILP=1 was also tried - // (0x18) and made it worse (pure white noise again) — IBP alone - // (0x08) is the correct override, confirmed live: real, recognizable - // music instead of static/noise on a locked FM station. Redundant - // with the R3_HWCONFIGURATION program write above (SigmaStudio's - // own export already places 0x08 at this address) — kept as a - // belt-and-braces re-assert in case that specific chunk silently - // failed; now checked/verified like everything else instead of - // fire-and-forget. + // SerialInputRegister (0x081F) IBP override -- REMOVED 2026-08-24, + // CONFIRMED LIVE as the root cause of the multi-day hiss/ + // unintelligible-speech investigation. History: on 2026-08-16 + // (commit 6974095) this was set to IBP=1 (0x08) alongside a + // SEPARATE, simultaneous fix to Si4684's PIN_CONFIG_ENABLE (which + // had been forcing the chip's analog DAC fallback instead of real + // I2S output). Both fixes landed in the same commit and were + // tested together: "static" became "music", credited to IBP=1. + // But SerialInputRegister is a SINGLE register shared by every + // SDATA_INx pin on the ADAU1701 (confirmed against the datasheet + // 2026-08-24: one INPUT_BCLK/INPUT_LRCLK clock pair serves all + // four SDATA_INx pins) -- so the override also applied to the + // ESP32 leg, not just Si4684's. The PIN_CONFIG_ENABLE fix alone + // was what turned static into music (Si4684 finally sending valid + // I2S data at all); IBP=1 had never been validated in isolation + // and was in fact marginal/wrong for both legs. With IBP left at + // the compiled default (0x00, IBP=0) and PIN_CONFIG_ENABLE + // independently correct, live listening confirmed clean audio on + // both the Si4684 (DAB) and ESP32 (web radio) paths -- the hiss + // is gone. Left commented out below rather than deleted, in case + // a future hardware revision needs it revisited. + // + // { + // const unsigned char deviceAddr = + // static_cast(pins_.i2cAddr7 << 1); + // ADI_REG_TYPE serialInFix = 0x08U; + // if (SIGMA_WRITE_REGISTER_BLOCK(deviceAddr, 0x081FU, 1U, + // &serialInFix) != 0) + // { + // ESP_LOGE(kTag, "SerialInputRegister override failed after retries"); + // return std::unexpected(Adau1701Error::DownloadFailed); + // } + // if (sigma_verify_block(0x081FU, &serialInFix, 1U) != 0) + // { + // ESP_LOGW(kTag, + // "SerialInputRegister read-back mismatch -- ACKed " + // "but did not land as 0x08"); + // } + // } + + // Limiter1/Limiter2 threshold override, 2026-08-24: compiled + // program ships both at 0x00800000 = 1.0 linear = 0 dBFS (an + // RMS-detecting limiter, per Analog Devices' own SigmaStudio + // Limiter cell docs), with zero headroom anywhere upstream (every + // mixer/EQ/master-volume gain in the compiled program is unity). + // A quiet synthetic test tone (well under 0 dBFS RMS) never + // engaged it and sounded clean; real loudness-normalized FM/DAB/ + // streamed program content sits close to 0 dBFS RMS routinely, + // triggering continuous gain-reduction ("pumping" per ADI's own + // docs) heard as exactly the hiss/unintelligible-speech symptom + // under investigation. Pulling both thresholds down to -6 dBFS + // gives real margin without being so conservative it can't be + // heard whether this was the mechanism. { - const unsigned char deviceAddr = - static_cast(pins_.i2cAddr7 << 1); - ADI_REG_TYPE serialInFix = 0x08U; - if (SIGMA_WRITE_REGISTER_BLOCK(deviceAddr, 0x081FU, 1U, - &serialInFix) != 0) + constexpr float kLimiterThresholdDb = -6.0F; + constexpr float kLimiterThresholdLinear = 0.50118723F; // 10^(-6/20) + const std::int32_t thresholdFixpoint = + core::floatToFixpoint823(kLimiterThresholdLinear); + if (auto lim1 = safeloadFixpoint( + static_cast(ADDR_LIMITER1_THRESHOLD), + thresholdFixpoint); + !lim1) { - ESP_LOGE(kTag, "SerialInputRegister override failed after retries"); - return std::unexpected(Adau1701Error::DownloadFailed); + ESP_LOGW(kTag, "Limiter1 threshold override failed"); } - if (sigma_verify_block(0x081FU, &serialInFix, 1U) != 0) + if (auto lim2 = safeloadFixpoint( + static_cast(ADDR_LIMITER2_THRESHOLD), + thresholdFixpoint); + !lim2) { - ESP_LOGW(kTag, - "SerialInputRegister read-back mismatch -- ACKed " - "but did not land as 0x08"); + ESP_LOGW(kTag, "Limiter2 threshold override failed"); } + ESP_LOGI(kTag, "Limiter1/2 threshold set to %.1f dBFS", + static_cast(kLimiterThresholdDb)); } booted_ = true; diff --git a/Software/components/drivers/bt1035/include/bt1035/Bt1035Driver.hpp b/Software/components/drivers/bt1035/include/bt1035/Bt1035Driver.hpp index 169852f..ebd7f0e 100644 --- a/Software/components/drivers/bt1035/include/bt1035/Bt1035Driver.hpp +++ b/Software/components/drivers/bt1035/include/bt1035/Bt1035Driver.hpp @@ -37,11 +37,15 @@ namespace bt1035 { struct Bt1035Pins { int uartTx; ///< ESP32 TX -> module RX. int uartRx; ///< ESP32 RX <- module TX. - int resetGpio; ///< Module RESET# (pin 8). Read-only: configured as a - ///< floating input (2026-08-22), never driven — relies - ///< entirely on the module's own internal pull-up. - int sysCtlGpio; ///< SYS_CTL (pin 34). Driven HIGH once at boot, then - ///< never touched again for the process lifetime. + int resetGpio; ///< Module RESET# (pin 8), active-low. Driven (not + ///< floating, since 2026-08-23): held LOW together with + ///< SYS_CTRL past the datasheet §4.8 Reset Protection + ///< timeout (~1.8 s) to force a genuine power-down on + ///< every resetAndInitOnce() attempt, then released HIGH + ///< before SYS_CTRL's power-up pulse (§4.7). + int sysCtlGpio; ///< SYS_CTL (pin 34), active-high. Driven LOW/HIGH on + ///< every resetAndInitOnce() call, together with + ///< resetGpio, to force a real power-cycle each retry. int ctsGpio; ///< Host->module UART_CTS (module pin 15). Diagnostic ///< only (2026-08-22): read-only floating input, never ///< driven — this driver does not implement hardware @@ -249,6 +253,24 @@ public: */ [[nodiscard]] std::expected queryAutoReconnect(); + /** + * @brief setA2dpCodecConfig — enable optional A2DP codecs (AT+A2DPCFG). + * + * @dname setA2dpCodecConfig + * @param bitmask BIT0=AAC, BIT1=aptX, BIT2=aptX-LL, BIT3=aptX-HD, + * BIT4=aptX-Adaptive, BIT5=LDAC (§5.3.4); 0 forces + * the mandatory SBC-only baseline. + * @return Ok on success, or Bt1035Error. Only affects the *next* A2DP + * negotiation — an already-connected peer keeps its current + * codec until it reconnects (see disconnectA2dp()). + * @pubstate writes UART. + * + * @author Michele Bigi + * @date 2026-08-24 + */ + [[nodiscard]] std::expected setA2dpCodecConfig( + std::uint8_t bitmask); + /** * @brief queryPairedList — enumerate paired remotes (AT+PLIST). * diff --git a/Software/components/drivers/bt1035/src/Bt1035Driver.cpp b/Software/components/drivers/bt1035/src/Bt1035Driver.cpp index 6866f7d..c935bcb 100644 --- a/Software/components/drivers/bt1035/src/Bt1035Driver.cpp +++ b/Software/components/drivers/bt1035/src/Bt1035Driver.cpp @@ -42,11 +42,11 @@ constexpr int kPostUartMs = 100; constexpr int kSysCtlAssertMs = 20; /** Datasheet §4.8: "Reset Protection timeout (typically greater than * ~1.8 s) causes the device to power down if VCHG is not present and - * SYS_CTRL is low." Held comfortably longer than that before each - * power-up assert, to guarantee a genuine full power-down rather than a - * pulse too short for the module's own protection timer to act on — see - * resetAndInitOnce()'s comment for why this now runs on every attempt, - * not just the first. */ + * SYS_CTRL is low." RESET# and SYS_CTRL are held asserted/low together for + * comfortably longer than that before each power-up, to guarantee a + * genuine full power-down rather than a pulse too short for the module's + * own protection timer to act on — see resetAndInitOnce()'s comment for + * why this now runs on every attempt, not just the first. */ constexpr int kSysCtlDeassertMs = 2500; /** Measured live (2026-08-20, power/wiring confirmed sound with a * multimeter — VBAT_IN/SYS_CTRL/1.8V_OUT/VDD_IO all correct, TX/RX pins @@ -798,6 +798,15 @@ std::expected Bt1035Driver::setAutoReconnect( return transmitAndExpectOk(core::buildBt1035SetAutoConnLine(times)); } +std::expected Bt1035Driver::setA2dpCodecConfig( + std::uint8_t bitmask) +{ + if (auto ready = ensureBooted(); !ready) { + return ready; + } + return transmitAndExpectOk(core::buildBt1035A2dpCodecConfigLine(bitmask)); +} + std::expected Bt1035Driver::queryAutoReconnect() { if (auto ready = ensureBooted(); !ready) { @@ -924,6 +933,32 @@ bool Bt1035Driver::waitForA2dpStreaming(int timeoutMs) ESP_LOGI(kTag, "stream wait: A2DPSTAT=%u", static_cast(static_cast(*state))); if (*state == core::Bt1035A2dpState::Streaming) { + // Diagnostic, 2026-08-24: confirm which codec actually got + // negotiated now that AT+A2DPCFG=1 (AAC) is sent at boot -- + // previously this was never queried, so every link was + // silently SBC-only with no way to tell from the logs. + if (auto codec = queryA2dpEncoder(); codec) { + // Feasycom guide §5.3.5's AT+A2DPENC response table for + // BT1035 only lists SBC/aptX/aptX-HD/aptX-LL/aptX- + // Adaptive -- no AAC code, even though §5.3.4's + // AT+A2DPCFG can enable it. Not inventing a mapping for + // that gap: an AAC link (or anything else undocumented) + // logs as "unknown" rather than a guessed label. + const char* name = "unknown"; + switch (*codec) { + case core::Bt1035A2dpCodec::Sbc: name = "SBC"; break; + case core::Bt1035A2dpCodec::Aptx: name = "aptX"; break; + case core::Bt1035A2dpCodec::AptxHd: name = "aptX-HD"; break; + case core::Bt1035A2dpCodec::AptxLl: name = "aptX-LL"; break; + case core::Bt1035A2dpCodec::AptxAdaptive: + name = "aptX-Adaptive"; + break; + } + ESP_LOGI(kTag, "A2DP streaming with codec: %s", name); + } else { + ESP_LOGW(kTag, "A2DPENC query failed (%d)", + static_cast(codec.error())); + } return true; } const TickType_t now = xTaskGetTickCount(); @@ -974,38 +1009,35 @@ std::expected Bt1035Driver::runInitSequence() std::expected Bt1035Driver::resetAndInitOnce() { - // RESET# (pin 8) is deliberately never driven by this driver (see - // boot()'s GPIO config below): it's left a floating input, relying - // entirely on the BT1035's own "fixed strong pull-up to VDD_IO" - // (datasheet §4.8), which the datasheet explicitly says means the pin - // "can therefore be left unconnected". Diagnostic test (2026-08-22) to - // check whether the previous external RESET# drive was contributing to - // the intermittent boot failures. - // - // SYS_CTRL (pin 34) alternative usage (2026-08-22 experiment): drive a - // genuine LOW-then-HIGH cycle every time this function runs, not just - // once ever. With RESET# now Hi-Z, SYS_CTRL is the only pin this - // driver can still use to force a real power-cycle — previously it - // was asserted HIGH exactly once at first boot and never touched - // again, which meant every later retry (hardware::bt1035RetryTask - // calls boot() again on failure) just re-listened on an - // already-asserted line without ever actually power-cycling the - // module. Held LOW for kSysCtlDeassertMs first so the module's own - // Reset Protection timeout actually elapses (a shorter pulse risks - // the module staying "protected" on, per datasheet §4.8, so the - // retry wouldn't be a real fresh power-on at all), then HIGH for the - // datasheet's own >=20ms minimum. + // 2026-08-23 fix: force a genuine power-down/restart on every retry by + // driving RESET# together with SYS_CTRL, per datasheet §4.8: "Assertion + // of RESET# beyond the Reset Protection timeout (typically >~1.8 s) + // causes the device to power down if VCHG is not present and SYS_CTRL + // is low. FSC-BT1035 then requires a SYS_CTRL assertion ... to + // restart." The prior design (2026-08-22) only pulsed SYS_CTRL and left + // RESET# floating; per §4.7, "when booted, software takes control of + // the internal regulators and the state of SYS_CTRL is ignored" — so + // once the module had booted once, that pulse alone could never force + // a real power-cycle. Asserting RESET# is the documented way to do it. const auto sysCtlPin = static_cast(pins_.sysCtlGpio); const auto resetPin = static_cast(pins_.resetGpio); const auto ctsPin = static_cast(pins_.ctsGpio); const auto rtsPin = static_cast(pins_.rtsGpio); + // Assert RESET# (active-low) and deassert SYS_CTRL together, held past + // the ~1.8s Reset Protection timeout so the module actually powers + // down rather than staying "protected" on (§4.8). + gpio_set_level(resetPin, 0); gpio_set_level(sysCtlPin, 0); vTaskDelay(pdMS_TO_TICKS(kSysCtlDeassertMs)); + + // Release RESET# before requesting power-up, then assert SYS_CTRL for + // the datasheet's own >=20ms minimum to start the boot (§4.7). + gpio_set_level(resetPin, 1); gpio_set_level(sysCtlPin, 1); vTaskDelay(pdMS_TO_TICKS(kSysCtlAssertMs)); ESP_LOGI(kTag, "power-up: SYS_CTRL=%d (want 1, driven) RESET#=%d " - "(want 1, Hi-Z + internal pull-up, not driven by us)", + "(want 1, driven)", gpio_get_level(sysCtlPin), gpio_get_level(resetPin)); ESP_LOGI(kTag, "before banner wait: CTS=%d (module's flow-control " "input, host side floating) RTS/PIO2=%d (module's " @@ -1027,21 +1059,25 @@ std::expected Bt1035Driver::boot() return {}; } - // RESET# (pin 8) is never driven by this driver (2026-08-22 diagnostic - // change): configured as a pure floating input, pull-up/pull-down both - // explicitly disabled, so nothing on the ESP32 side influences this - // net electrically — the BT1035's own internal RESET# pull-up (§4.8) - // is the only thing holding it high. GPIO_MODE_INPUT (not OUTPUT) still - // lets gpio_get_level() read it back for the diagnostic log below, - // without ever driving it. + // RESET# (pin 8) is actively driven (2026-08-23 fix): datasheet §4.8 + // states the Reset Protection timeout that forces a genuine power-down + // is triggered by *asserting RESET#* (while SYS_CTRL is low), not by + // toggling SYS_CTRL alone. Leaving RESET# floating (the 2026-08-22 + // diagnostic change) meant that mechanism was never actually invoked — + // every retry only pulsed SYS_CTRL, which the module ignores once + // already booted (§4.7: "when booted, software takes control of the + // internal regulators and the state of SYS_CTRL is ignored"). Driven + // HIGH immediately below (deasserted) before anything else runs, so + // configuring the pin never glitches it low. gpio_config_t resetCfg = {}; resetCfg.pin_bit_mask = 1ULL << pins_.resetGpio; - resetCfg.mode = GPIO_MODE_INPUT; + resetCfg.mode = GPIO_MODE_INPUT_OUTPUT; resetCfg.pull_up_en = GPIO_PULLUP_DISABLE; resetCfg.pull_down_en = GPIO_PULLDOWN_DISABLE; if (gpio_config(&resetCfg) != ESP_OK) { return std::unexpected(Bt1035Error::ResetFailed); } + gpio_set_level(static_cast(pins_.resetGpio), 1); // SYS_CTRL (pin 34) stays actively driven (GPIO_MODE_INPUT_OUTPUT: // the INPUT bit is what makes gpio_get_level() read the real driven @@ -1123,7 +1159,7 @@ std::expected Bt1035Driver::boot() ESP_LOGI(kTag, "auto-link disabled (AT+LINKCFG=0,0)"); booted_ = true; - ESP_LOGI(kTag, "I2S slave mode enabled (AT+AUXCFG=3, AT+I2SCFG=35)"); + ESP_LOGI(kTag, "I2S slave mode enabled (AT+AUXCFG=3, AT+I2SCFG=67)"); return {}; } diff --git a/Software/components/net/src/SetupWebServer.cpp b/Software/components/net/src/SetupWebServer.cpp index 0271ee1..43fbd2c 100644 --- a/Software/components/net/src/SetupWebServer.cpp +++ b/Software/components/net/src/SetupWebServer.cpp @@ -1759,6 +1759,63 @@ esp_err_t bluetoothAutoReconnectPostHandler(httpd_req_t* req) return httpd_resp_send(req, "{\"status\":\"saved\"}", 18); } +esp_err_t bluetoothA2dpCodecConfigPostHandler(httpd_req_t* req) +{ + auto* ctx = routeContextFrom(req); + if (ctx == nullptr || ctx->bluetooth == nullptr) { + httpd_resp_set_status(req, "503 Service Unavailable"); + return httpd_resp_send(req, nullptr, 0); + } + + std::array body{}; + (void)readRequestBody(req, body); + const auto mask = core::parseBluetoothA2dpCodecConfigJson( + std::string_view(body.data())); + if (!mask) { + const std::string json = + core::serializeBluetoothErrorJson(parseErrorToken(mask.error())); + httpd_resp_set_status(req, "400 Bad Request"); + httpd_resp_set_type(req, "application/json"); + return httpd_resp_send(req, json.c_str(), json.size()); + } + + if (auto result = ctx->bluetooth->setA2dpCodecConfig(*mask); !result) { + const std::string json = + core::serializeBluetoothErrorJson(bt1035ErrorToken(result.error())); + httpd_resp_set_status(req, "500 Internal Server Error"); + httpd_resp_set_type(req, "application/json"); + return httpd_resp_send(req, json.c_str(), json.size()); + } + + ESP_LOGI(kTag, "A2DP codec config set to bitmask %u — reconnect the " + "peer for it to take effect", + static_cast(*mask)); + httpd_resp_set_type(req, "application/json"); + return httpd_resp_send(req, "{\"status\":\"saved\"}", 18); +} + +esp_err_t bluetoothA2dpCodecGetHandler(httpd_req_t* req) +{ + auto* ctx = routeContextFrom(req); + if (ctx == nullptr || ctx->bluetooth == nullptr) { + httpd_resp_set_status(req, "503 Service Unavailable"); + return httpd_resp_send(req, nullptr, 0); + } + + const auto codec = ctx->bluetooth->queryA2dpCodec(); + if (!codec) { + const std::string json = + core::serializeBluetoothErrorJson(bt1035ErrorToken(codec.error())); + httpd_resp_set_status(req, "500 Internal Server Error"); + httpd_resp_set_type(req, "application/json"); + return httpd_resp_send(req, json.c_str(), json.size()); + } + + const std::string json = core::serializeBluetoothA2dpCodecJson(*codec); + httpd_resp_set_type(req, "application/json"); + return httpd_resp_send(req, json.c_str(), json.size()); +} + esp_err_t stationsGetHandler(httpd_req_t* req) { auto* ctx = routeContextFrom(req); @@ -2334,6 +2391,22 @@ std::expected SetupWebServer::start( }; httpd_register_uri_handler(server_, &bluetoothAutoReconnectUri); + const httpd_uri_t bluetoothA2dpCodecConfigUri = { + .uri = "/api/bluetooth/a2dp-codec", + .method = HTTP_POST, + .handler = bluetoothA2dpCodecConfigPostHandler, + .user_ctx = routeCtx, + }; + httpd_register_uri_handler(server_, &bluetoothA2dpCodecConfigUri); + + const httpd_uri_t bluetoothA2dpCodecGetUri = { + .uri = "/api/bluetooth/a2dp-codec", + .method = HTTP_GET, + .handler = bluetoothA2dpCodecGetHandler, + .user_ctx = routeCtx, + }; + httpd_register_uri_handler(server_, &bluetoothA2dpCodecGetUri); + const httpd_uri_t stationsGetUri = { .uri = "/api/stations", .method = HTTP_GET, diff --git a/Software/components/services/bluetooth/include/bluetooth/BluetoothService.hpp b/Software/components/services/bluetooth/include/bluetooth/BluetoothService.hpp index 5941331..44f9cc7 100644 --- a/Software/components/services/bluetooth/include/bluetooth/BluetoothService.hpp +++ b/Software/components/services/bluetooth/include/bluetooth/BluetoothService.hpp @@ -107,6 +107,30 @@ public: [[nodiscard]] std::expected setAutoReconnect( std::uint8_t times); + /** + * @brief setA2dpCodecConfig — enable optional A2DP codecs at runtime. + * + * @dname setA2dpCodecConfig + * @param bitmask BIT0=AAC, BIT1=aptX, BIT2=aptX-LL, BIT3=aptX-HD, + * BIT4=aptX-Adaptive, BIT5=LDAC; 0 forces SBC-only. + * @return Ok on success, or a Bt1035Error. + * @pubstate Only affects the next negotiation — call disconnectA2dp() + * (or have the peer reconnect) for an already-streaming link + * to pick up the new codec set. + */ + [[nodiscard]] std::expected setA2dpCodecConfig( + std::uint8_t bitmask); + + /** + * @brief queryA2dpCodec — read the currently negotiated A2DP codec. + * + * @dname queryA2dpCodec + * @return Negotiated codec, or a Bt1035Error (e.g. no active link). + * @pubstate none + */ + [[nodiscard]] std::expected + queryA2dpCodec(); + /** * @brief scanNearby — classic BT/EDR discovery, cancelling pairing. * diff --git a/Software/components/services/bluetooth/src/BluetoothService.cpp b/Software/components/services/bluetooth/src/BluetoothService.cpp index 59a9c97..4019e39 100644 --- a/Software/components/services/bluetooth/src/BluetoothService.cpp +++ b/Software/components/services/bluetooth/src/BluetoothService.cpp @@ -148,6 +148,18 @@ std::expected BluetoothService::setAutoReconnect( return driver_.setAutoReconnect(times); } +std::expected BluetoothService::setA2dpCodecConfig( + std::uint8_t bitmask) +{ + return driver_.setA2dpCodecConfig(bitmask); +} + +std::expected +BluetoothService::queryA2dpCodec() +{ + return driver_.queryA2dpEncoder(); +} + std::expected, bt1035::Bt1035Error> BluetoothService::scanNearby(std::uint8_t scanSeconds) { diff --git a/Software/docs/TODO.md b/Software/docs/TODO.md index 84a1036..45d17e0 100644 --- a/Software/docs/TODO.md +++ b/Software/docs/TODO.md @@ -221,12 +221,20 @@ required yet) and, once a result converges, **write the result to the persistence, see `Eeprom24aa::writeFmAntCap`/`writeDabAntCap`) so it survives a reboot without a firmware reflash. `recalibrateXtal()` (`Si4684Driver.cpp`) already does the live re-boot-with-new-params part; -what's missing is EEPROM persistence for CTUN/XTAL_FREQ (ANTCAP already -persists this way — the xtal calibration should follow the same shape, -likely a new EEPROM word address alongside the existing FM/DAB ANTCAP -ones) and doing the FREQOFF-averaging/damping/convergence-loop logic -in firmware (or keeping it host-side and just adding the EEPROM-persist -step at the end — decide when picked up). +what's missing is EEPROM persistence for **all three** crystal +calibration parameters -- `ibias`, `ctun`, AND `xtalFreqHz` (not just +XTAL_FREQ; confirmed explicitly 2026-08-24 that all three need to +persist, not only the one this session happened to tune) -- plus loading +them at boot the same way `main.cpp` already loads the saved FM/DAB +ANTCAP into `TunerService` before the first tune. ANTCAP already +persists this way (2 bytes/band, word addresses 0x00/0x01) — the xtal +calibration needs its own new EEPROM word address(es) alongside those +(ibias fits in 1 byte, ctun in 1 byte, xtalFreqHz needs 4 bytes -- 6 +bytes total, or pack more compactly if EEPROM space is tight). Also +still needed: deciding whether the FREQOFF-averaging/damping/ +convergence-loop logic (currently in `tools/si4684_xtal_calibration.py`) +moves into firmware, or stays host-side with just an EEPROM-persist step +added at the end of the existing HTTP flow. Not started — explicitly deferred to a future session, noted here only so it isn't lost. See `docs/si4684-rf-investigation-report.md`'s 2026-08-23 From 7cb25be3f3dcf9963b4bbccfdf5ca65d05c14d82 Mon Sep 17 00:00:00 2001 From: Michele Bigi Date: Mon, 24 Aug 2026 14:55:11 +0200 Subject: [PATCH 11/12] Fix audio-profile NVS persistence and whitespace-intolerant JSON parsers - NvsAudioProfileStore used the key "audio_profile_json" (18 chars), exceeding NVS's 15-char key-name limit. Every nvs_set_str call failed silently with ESP_ERR_NVS_KEY_TOO_LONG (0x1109): applyProfile() always updated live DSP audio correctly, so the bug was invisible except as "store_failed" in the HTTP response and settings never surviving a reboot. Renamed to "audio_profile" (13 chars); confirmed live, EQ/mixer changes now persist across a reset. - The hand-rolled JSON extractJsonString()/extractJsonBool() helpers (duplicated per-file: TunerJson, DspParamJson, StationListJson, WebRadioJson, WifiProvisionJson, AudioProfileJson, plus inline mac/name/ save parsing in BluetoothJson) matched only the exact literal `"key":"value"` / `"key":true`, with no tolerance for a space after the colon. Standard JSON encoders (e.g. Swift's JSONEncoder in its default, non-compact mode) emit `"key": "value"`, which silently failed to parse as invalid_json/missing_field. Numeric fields were already fine (strtoul/strtof skip leading whitespace per the C standard); fixed only the string/bool extractors to skip whitespace after the colon before matching the value. Co-Authored-By: Claude Sonnet 5 --- .../components/core/src/AudioProfileJson.cpp | 21 +++-- .../components/core/src/BluetoothJson.cpp | 76 ++++++++++++++----- Software/components/core/src/DspParamJson.cpp | 17 ++++- .../components/core/src/StationListJson.cpp | 17 ++++- Software/components/core/src/TunerJson.cpp | 17 ++++- Software/components/core/src/WebRadioJson.cpp | 38 +++++++--- .../components/core/src/WifiProvisionJson.cpp | 17 ++++- .../secure_store/src/NvsAudioProfileStore.cpp | 7 +- 8 files changed, 157 insertions(+), 53 deletions(-) diff --git a/Software/components/core/src/AudioProfileJson.cpp b/Software/components/core/src/AudioProfileJson.cpp index 3edcb72..89e3511 100644 --- a/Software/components/core/src/AudioProfileJson.cpp +++ b/Software/components/core/src/AudioProfileJson.cpp @@ -58,15 +58,24 @@ namespace { std::string_view key, bool& out) { - const std::string trueNeedle = - std::string("\"") + std::string(key) + "\":true"; - const std::string falseNeedle = - std::string("\"") + std::string(key) + "\":false"; - if (json.find(trueNeedle) != std::string_view::npos) { + const std::string needle = + std::string("\"") + std::string(key) + "\":"; + const std::size_t needlePos = json.find(needle); + if (needlePos == std::string_view::npos) { + return false; + } + std::size_t start = needlePos + needle.size(); + while (start < json.size() + && (json[start] == ' ' || json[start] == '\t' + || json[start] == '\r' || json[start] == '\n')) { + ++start; + } + const std::string_view rest = json.substr(start); + if (rest.starts_with("true")) { out = true; return true; } - if (json.find(falseNeedle) != std::string_view::npos) { + if (rest.starts_with("false")) { out = false; return true; } diff --git a/Software/components/core/src/BluetoothJson.cpp b/Software/components/core/src/BluetoothJson.cpp index a126130..4f53950 100644 --- a/Software/components/core/src/BluetoothJson.cpp +++ b/Software/components/core/src/BluetoothJson.cpp @@ -150,12 +150,21 @@ parseBluetoothConnectJson(std::string_view json) if (json.find('{') == std::string_view::npos) { return std::unexpected(ParseError::InvalidJson); } - const std::string needle = "\"mac\":\""; - const std::size_t start = json.find(needle); - if (start == std::string_view::npos) { + const std::string needle = "\"mac\":"; + const std::size_t needlePos = json.find(needle); + if (needlePos == std::string_view::npos) { return std::unexpected(ParseError::MissingField); } - const std::size_t valueStart = start + needle.size(); + std::size_t start = needlePos + needle.size(); + while (start < json.size() + && (json[start] == ' ' || json[start] == '\t' + || json[start] == '\r' || json[start] == '\n')) { + ++start; + } + if (start >= json.size() || json[start] != '"') { + return std::unexpected(ParseError::InvalidJson); + } + const std::size_t valueStart = start + 1U; const std::size_t valueEnd = json.find('"', valueStart); if (valueEnd == std::string_view::npos) { return std::unexpected(ParseError::InvalidJson); @@ -183,17 +192,35 @@ BluetoothConnectRequest parseBluetoothConnectRequest(std::string_view json) if (auto mac = parseBluetoothConnectJson(json); mac) { request.mac = std::move(*mac); } - const std::string nameNeedle = "\"name\":\""; - const std::size_t nameStart = json.find(nameNeedle); - if (nameStart != std::string_view::npos) { - const std::size_t valueStart = nameStart + nameNeedle.size(); - const std::size_t valueEnd = json.find('"', valueStart); - if (valueEnd != std::string_view::npos) { - request.name.assign(json.substr(valueStart, valueEnd - valueStart)); + const std::string nameNeedle = "\"name\":"; + const std::size_t nameNeedlePos = json.find(nameNeedle); + if (nameNeedlePos != std::string_view::npos) { + std::size_t nameStart = nameNeedlePos + nameNeedle.size(); + while (nameStart < json.size() + && (json[nameStart] == ' ' || json[nameStart] == '\t' + || json[nameStart] == '\r' || json[nameStart] == '\n')) { + ++nameStart; + } + if (nameStart < json.size() && json[nameStart] == '"') { + const std::size_t valueStart = nameStart + 1U; + const std::size_t valueEnd = json.find('"', valueStart); + if (valueEnd != std::string_view::npos) { + request.name.assign( + json.substr(valueStart, valueEnd - valueStart)); + } } } - request.save = json.find("\"save\":true") != std::string_view::npos - || json.find("\"save\": true") != std::string_view::npos; + const std::string saveNeedle = "\"save\":"; + const std::size_t saveNeedlePos = json.find(saveNeedle); + if (saveNeedlePos != std::string_view::npos) { + std::size_t saveStart = saveNeedlePos + saveNeedle.size(); + while (saveStart < json.size() + && (json[saveStart] == ' ' || json[saveStart] == '\t' + || json[saveStart] == '\r' || json[saveStart] == '\n')) { + ++saveStart; + } + request.save = json.substr(saveStart).starts_with("true"); + } return request; } @@ -219,13 +246,22 @@ parseBluetoothSpeakerJson(std::string_view json) return std::unexpected(mac.error()); } BtSpeakerTarget target{.mac = *mac, .name = {}}; - const std::string nameNeedle = "\"name\":\""; - const std::size_t nameStart = json.find(nameNeedle); - if (nameStart != std::string_view::npos) { - const std::size_t valueStart = nameStart + nameNeedle.size(); - const std::size_t valueEnd = json.find('"', valueStart); - if (valueEnd != std::string_view::npos) { - target.name.assign(json.substr(valueStart, valueEnd - valueStart)); + const std::string nameNeedle = "\"name\":"; + const std::size_t nameNeedlePos = json.find(nameNeedle); + if (nameNeedlePos != std::string_view::npos) { + std::size_t nameStart = nameNeedlePos + nameNeedle.size(); + while (nameStart < json.size() + && (json[nameStart] == ' ' || json[nameStart] == '\t' + || json[nameStart] == '\r' || json[nameStart] == '\n')) { + ++nameStart; + } + if (nameStart < json.size() && json[nameStart] == '"') { + const std::size_t valueStart = nameStart + 1U; + const std::size_t valueEnd = json.find('"', valueStart); + if (valueEnd != std::string_view::npos) { + target.name.assign( + json.substr(valueStart, valueEnd - valueStart)); + } } } return target; diff --git a/Software/components/core/src/DspParamJson.cpp b/Software/components/core/src/DspParamJson.cpp index 4868f6c..00be98e 100644 --- a/Software/components/core/src/DspParamJson.cpp +++ b/Software/components/core/src/DspParamJson.cpp @@ -24,12 +24,21 @@ namespace { std::string_view key) { const std::string needle = - std::string("\"") + std::string(key) + "\":\""; - const std::size_t start = json.find(needle); - if (start == std::string_view::npos) { + std::string("\"") + std::string(key) + "\":"; + const std::size_t needlePos = json.find(needle); + if (needlePos == std::string_view::npos) { return {}; } - const std::size_t valueStart = start + needle.size(); + std::size_t start = needlePos + needle.size(); + while (start < json.size() + && (json[start] == ' ' || json[start] == '\t' + || json[start] == '\r' || json[start] == '\n')) { + ++start; + } + if (start >= json.size() || json[start] != '"') { + return {}; + } + const std::size_t valueStart = start + 1U; const std::size_t valueEnd = json.find('"', valueStart); if (valueEnd == std::string_view::npos) { return {}; diff --git a/Software/components/core/src/StationListJson.cpp b/Software/components/core/src/StationListJson.cpp index a46287c..faba0fc 100644 --- a/Software/components/core/src/StationListJson.cpp +++ b/Software/components/core/src/StationListJson.cpp @@ -24,12 +24,21 @@ namespace { std::string_view key) { const std::string needle = - std::string("\"") + std::string(key) + "\":\""; - const std::size_t start = json.find(needle); - if (start == std::string_view::npos) { + std::string("\"") + std::string(key) + "\":"; + const std::size_t needlePos = json.find(needle); + if (needlePos == std::string_view::npos) { return {}; } - const std::size_t valueStart = start + needle.size(); + std::size_t start = needlePos + needle.size(); + while (start < json.size() + && (json[start] == ' ' || json[start] == '\t' + || json[start] == '\r' || json[start] == '\n')) { + ++start; + } + if (start >= json.size() || json[start] != '"') { + return {}; + } + const std::size_t valueStart = start + 1U; const std::size_t valueEnd = json.find('"', valueStart); if (valueEnd == std::string_view::npos) { return {}; diff --git a/Software/components/core/src/TunerJson.cpp b/Software/components/core/src/TunerJson.cpp index 84297ad..b69e1fc 100644 --- a/Software/components/core/src/TunerJson.cpp +++ b/Software/components/core/src/TunerJson.cpp @@ -24,12 +24,21 @@ namespace { std::string_view key) { const std::string needle = - std::string("\"") + std::string(key) + "\":\""; - const std::size_t start = json.find(needle); - if (start == std::string_view::npos) { + std::string("\"") + std::string(key) + "\":"; + const std::size_t needlePos = json.find(needle); + if (needlePos == std::string_view::npos) { return {}; } - const std::size_t valueStart = start + needle.size(); + std::size_t start = needlePos + needle.size(); + while (start < json.size() + && (json[start] == ' ' || json[start] == '\t' + || json[start] == '\r' || json[start] == '\n')) { + ++start; + } + if (start >= json.size() || json[start] != '"') { + return {}; + } + const std::size_t valueStart = start + 1U; const std::size_t valueEnd = json.find('"', valueStart); if (valueEnd == std::string_view::npos) { return {}; diff --git a/Software/components/core/src/WebRadioJson.cpp b/Software/components/core/src/WebRadioJson.cpp index fee4b2f..5f4c870 100644 --- a/Software/components/core/src/WebRadioJson.cpp +++ b/Software/components/core/src/WebRadioJson.cpp @@ -24,12 +24,21 @@ constexpr std::string_view kHttpPrefix = "http://"; std::string_view key) { const std::string needle = - std::string("\"") + std::string(key) + "\":\""; - const std::size_t start = json.find(needle); - if (start == std::string_view::npos) { + std::string("\"") + std::string(key) + "\":"; + const std::size_t needlePos = json.find(needle); + if (needlePos == std::string_view::npos) { return {}; } - const std::size_t valueStart = start + needle.size(); + std::size_t start = needlePos + needle.size(); + while (start < json.size() + && (json[start] == ' ' || json[start] == '\t' + || json[start] == '\r' || json[start] == '\n')) { + ++start; + } + if (start >= json.size() || json[start] != '"') { + return {}; + } + const std::size_t valueStart = start + 1U; const std::size_t valueEnd = json.find('"', valueStart); if (valueEnd == std::string_view::npos) { return {}; @@ -40,15 +49,24 @@ constexpr std::string_view kHttpPrefix = "http://"; [[nodiscard]] bool extractJsonBool(std::string_view json, std::string_view key, bool& out) { - const std::string trueNeedle = - std::string("\"") + std::string(key) + "\":true"; - const std::string falseNeedle = - std::string("\"") + std::string(key) + "\":false"; - if (json.find(trueNeedle) != std::string_view::npos) { + const std::string needle = + std::string("\"") + std::string(key) + "\":"; + const std::size_t needlePos = json.find(needle); + if (needlePos == std::string_view::npos) { + return false; + } + std::size_t start = needlePos + needle.size(); + while (start < json.size() + && (json[start] == ' ' || json[start] == '\t' + || json[start] == '\r' || json[start] == '\n')) { + ++start; + } + const std::string_view rest = json.substr(start); + if (rest.starts_with("true")) { out = true; return true; } - if (json.find(falseNeedle) != std::string_view::npos) { + if (rest.starts_with("false")) { out = false; return true; } diff --git a/Software/components/core/src/WifiProvisionJson.cpp b/Software/components/core/src/WifiProvisionJson.cpp index f3590e0..1739dc9 100644 --- a/Software/components/core/src/WifiProvisionJson.cpp +++ b/Software/components/core/src/WifiProvisionJson.cpp @@ -37,12 +37,21 @@ namespace { [[nodiscard]] std::string_view extractJsonString(std::string_view json, std::string_view key) { - const std::string needle = std::string("\"") + std::string(key) + "\":\""; - const std::size_t start = json.find(needle); - if (start == std::string_view::npos) { + const std::string needle = std::string("\"") + std::string(key) + "\":"; + const std::size_t needlePos = json.find(needle); + if (needlePos == std::string_view::npos) { return {}; } - const std::size_t valueStart = start + needle.size(); + std::size_t start = needlePos + needle.size(); + while (start < json.size() + && (json[start] == ' ' || json[start] == '\t' + || json[start] == '\r' || json[start] == '\n')) { + ++start; + } + if (start >= json.size() || json[start] != '"') { + return {}; + } + const std::size_t valueStart = start + 1U; const std::size_t valueEnd = json.find('"', valueStart); if (valueEnd == std::string_view::npos) { return {}; diff --git a/Software/components/secure_store/src/NvsAudioProfileStore.cpp b/Software/components/secure_store/src/NvsAudioProfileStore.cpp index 01c2b5a..9cacbea 100644 --- a/Software/components/secure_store/src/NvsAudioProfileStore.cpp +++ b/Software/components/secure_store/src/NvsAudioProfileStore.cpp @@ -26,7 +26,12 @@ namespace secure_store { namespace { constexpr char kTag[] = "NvsAudioProfileStore"; constexpr char kNamespace[] = "digiradio"; -constexpr char kProfileKey[] = "audio_profile_json"; +// NVS key names are capped at 15 chars (NVS_KEY_NAME_MAX_SIZE=16 incl. NUL); +// the previous "audio_profile_json" (18 chars) made every nvs_set_str call +// fail with ESP_ERR_NVS_KEY_TOO_LONG (0x1109), silently -- applyProfile() +// always updated live audio correctly but persistProfile() never actually +// wrote anything, so nothing survived a reboot. +constexpr char kProfileKey[] = "audio_profile"; } // namespace bool NvsAudioProfileStore::hasProfile() const From d17902f19ce21144015cbb7294ef8eb62b468de4 Mon Sep 17 00:00:00 2001 From: Michele Bigi Date: Mon, 24 Aug 2026 23:21:00 +0200 Subject: [PATCH 12/12] Add DigiRadio PCB photo to README Co-Authored-By: Claude Sonnet 5 --- README.md | 2 ++ docs/images/digiradio-pcb.jpg | Bin 0 -> 565070 bytes 2 files changed, 2 insertions(+) create mode 100644 docs/images/digiradio-pcb.jpg diff --git a/README.md b/README.md index 27f3249..d0953cd 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,8 @@ ![Hardware License](https://img.shields.io/badge/hardware-CERN--OHL--S-lightgrey) [![Firmware License: Apache 2.0](https://img.shields.io/badge/Firmware%20License-Apache%202.0-blue.svg)](Software/LICENSE) +![DigiRadio PCB](docs/images/digiradio-pcb.jpg) + --- diff --git a/docs/images/digiradio-pcb.jpg b/docs/images/digiradio-pcb.jpg new file mode 100644 index 0000000000000000000000000000000000000000..3524b07625abf0d85127cf52f3c1f0e078545048 GIT binary patch literal 565070 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 |ua&1#Z<;Eemp=Z6 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ℜ_ss7Uv>Eg~M zV;ggW&zHx{MOSY#N|h#$_n?u}mB{RBORJVy8v4R8SFDkacKVN@tea}>nsVQkF;`o! zUq8@W7BM5hn@DrSgm$Ws&pflsX|phe$ODC94_~cX^KKp`mJNW<_hRf9w?4Gk+D4UL zDA;*FGoa{Nx!Y+bnMqm6$$!KD09p-O(R%|AB@q(^g7toR{A)taJ1t5ll~K?y3)tm< z`qiHm%uzukmQd{l^5Bf~?de-f6u)bWV=DQm9EJO!^&tD!)aP~YGaN^{pDMR)e(~1q zf@Fec%qQn4KJw%IMOOPw(lC9VL<+@19;EdIRELYfEP+Pk;YV@PHKB52iu}!TW1cOU zg=9HZ1b4@9YVMtsPF2dDt9AW+$uDM!ZQ+=Z9&v!caHkj`kH)LR9F1ny^F*m}3l>M| zxQd1|1G}5H1zh}#*mICYUYhFI%CM#ol>u8lTfTGL){5=uj2h)!v0Cr_ex_nOPcWW* zLLy%%uW}D0nn<-PguH2OB>@xVT&`7t;P7d(M=tp#QY1YB`j2Wzxw}7U8LB$&KPb04DAP}>sfK#%%mt%BytyaI%lmi-TaB}VYvmLaRl`obg1X$vfS(X zyR+tdE}OsjV$^Uq_JsPC%YNy=5suDHT@c&JBS;2gWXaDzFg*okrS5IaF~+iNKDqoU zZs#j)35nGtC;eLUkMe64{zIKQl9EqOpCnBM(i?La1V~9tp2DJv-et&%$;kY^l|xRx ze>LE^LhN!&`c<~ML}h650P1L_)|WJ$DMD^BTb);cEv_HLX!7#OxD4`JBNU$zr7NqO zX)+fBgV#8%e}@-QCcE~T4m`1x$5IEVte=Y44?ec+;1)l`2T*z%=cL-jRCd#q883Sq zNj0vgS)yCQeiO@b${JRc)my&=ab8hj9f*g{m7B~5rFOm_n)QAk&1#2qUuh4|Z>4au zD%=K0vaOtCdevd~3={_-Q|>JdoLdV581BI5A6lgi#OUb* zr_7@Vj-Yp|UQD8PxVjm)et;fxT`8+w?o6WNE$qKCHn@UlR@weW>}j@0u-;muj0TWo zo}}io-V}LRPE?`Fn$WR()VsI7E9NXxDJSS^CgkI((y7KVvU(pti(6S5Xvs3f(H3Pq z4hLHK>s9+RUp~YHmcowM`q#P1tJ+yIvXEZtHrj;p0Ua8=TKYGP+%m(t<#Nm1rh5vU zDtxb-Lyrw58m`@nw$r`E%Dtz^Avkb4aaJzn7Z)JN0y_bP9Wz+gD|w}(xlr4_c>v_7 zCmhw2B(u!7h%g-BXZUl)Y^3>Y&iT+vrQ-BjCN-I@WX1Bc$ft#0PStM^q>HG?N;0qA zJwW#!^<;T4xtK6>(YJH?RdGI_D3x6pO5@Wx6q;6KbMtDV{O$f-3($FU#PUPYxgom_ z^$oqea4NG~AseMUS0p_d$y-mF6f#Na&NE92y)vv!f>dDyD$`A5Ig+N#L6u`r);kx$(} zO0P2a_Z5>a#|vLvnC?fD<>6S4nDwgn0_M?`Ji{y**`B~>054)9p=(k#{pm<|#e7BRvP_QF!zla!IepxJc4pS{PZ1&mRiIjmmpB)3r+q z$!lPiExDs++(GNR9Fa`kuog3APaARX>q{N+xkg)viUi;l>w(^}Z{8*IxY;jvfB0cH z+ru8n(dCmIVDu#5VyfG+UB^niv|noMLVD6Pig4+56fM2X~{=qyh1 zGL&A!)YD73QffTOE46Rx#8(!P%LGN3ZU=4$Ved;F%&|bWOBb54oT2n5twxbVZlqHw z!32iv4_soU5-?DZx60&X{VG$ut-_6l?4-gg8IJc%VUPT@4bF4z z(y1(#*1;|%R#s8+sQw~HLrSY|u_TvlxnYo`{K?!_i%s_`dfLg^Y%HwyS0X!XMbIrLfMh-nsCyLR~E#Q`9nK#P`Y{w5p-gpP| ztW8xcR%ouJ8^dQPIuCl$x73=}0VS`=mBNns>ycVQTo)qW<8O!T?#e`tI9EC4u~ZvdzPN#+ zo!QCbaXkm6Rpw*>j#Tya9+i}7DD5k-bfUM7x^MWMgnk!`Tes8ny(z`ksV($&(8U8}W{d z;sueQlS{jX>2~!VQwA9PtMvZ>UH!N3z9`>Ycyj*G>5xcvE!=JLxW+(lOxMZc@D-(x zRblX}AAgc&v?=A%!^`7r`6uEwgP?d{#ZjBvl3f^?BLn3Geb7G&*q6jU2aXtqpLrF; zmyzvcb^~b(oMW~Pa^Dm*3%zPf`?r(_nnFWv-HpA$`d7mqBhxiYU1_Z>CS;LGAnxNB z>z{h|s$uEWeDz(K?_g?FlZs6J3HWpSM`-^53A{sfX{pQRS~%RQq;1}KR$p)jHTPe^ zFWYNJ@hh}iX!~W`yA7Y*i#PacD=FO{MDaJJEB;@z|xAH!tFQ-~lQmH*Wwm(}mJxb@q zR?VngKIvXHkSlOLZq?Cg*AYa<-Pdn9C$>d-r-R>BxwluHRRK}T@xt&iUWE;$@y`^D zM5^C0CyZCk(VKC1K9V$=J2+(}1IBkIaf9`(6lHEm$JVfsm@+6}i`(m3@w&PMkl~l1 z^{%R6QZI4QyCWQ6f_SRZ$t*7DPR-*v?N15}yGI22_r*bUlgolvD!p@5y983XnR6uR z8%G~9m2IcCNaDXI{B!WPOVxa3s%p;Yw-U_$dJpfZ=O2xJvtI3hDUaq3K2iPCUmbXt z;f}v~;%n_n!ZpRz$2sYiImjNB(J5aJ<0Eu-D&2CPR-0uE1y7nR)F|ZuH0$sB(Aa;n1+`pJRUf&9&wUh zM{27y^k6T=d8+2T@uYAMD-Yt35tD zmAFZ9*yle^Yl5xiSv9ewY@=jwo*K7~3#ld_y_J|8_9M78>{i!COPI{Dq97PR2SR`P z^|PdWD%R`*TboSz{{S}i=m(BR{#KIXb17k12LYv^uSBEyhV z_nUHeb|$*}1es!68TO6j6~V|L^N+^3xMqmRWVw?C#t0wMy1g=IwZ!rK<}wZsJRbGg zPFiYXuVb^dj$3T`h~gw|hBh4Y#~#(RmfKhe4#2#Fk6NUfoZ7Xr7!!2rK`qlDbgrhs zTIwe=uE~^SgUB_~yRsY8Qmj5ijS|RO7db4)zB*vliRZYF5UPWo{@+^FjUszyx4Bh0 z83!JohN}o;iUA@ilp(?Ak@c;ksxe!i|JD1M*c3BDZ~*7)?NCQ16gGEC{k=0#sd$~E z05I+k>r7pye8-G8IPLVW0W0o(Buz+2M%yXc?fykeaX-v0)Q~ZdM{`x(7UC9Q0AbLR z*m_lMs)Y(4nNCJ&@~y_~Y)d?^0{MUjPvY)r>3k(s0S4wEW8SGw@-l8D9CaNtNgUAx z%wKzV9-xkuE+n>SJ;7Usc}L8kjCvZH(&?9SqYcNVdwnX0n-dgi`~BnG)GcuuI=qNo zv(RyzP^087V_)sA){;g??-jZpwN4x7a3$Tf^R)i}_3Jiy<@28APB)R$wt7_Y#SC`n z$Tu?*SdQnuXylJ=MC5NR-;tT5UC!&auYIZ|VA$Z}uVY-AUPvK$-2p$tR>abxp?*|- z?@&!AP4^!$(?sh8rbS>_f=4_Goh~Du7Ia`p&P8+6Tq8%c4!FimBCzt)X$mp(VE+I* zhXlmuBy^V3JezWQ*Mt7j?<_tKvr{+j#m~T3pD{>THAesc|=zBK69TC3m<4?osJ}7)P@Yjga z<^y@4SR!30<9s{zu~XjwX1t75Im;5<7M9TXj0GBXaO;~#f57>3RrpqH{3D`#L%W(Y zrRstvMT~RvjFmr3VAq@Ylfl})h-|gzvbl#z)FvX@Sx}&6Q`0B&75C)+H26v3{{RK} zAHtgNm8ij{IvbBK=8MSy@<;cBV3F-sKWd#@!82p8xQ2c4<6`Hg4OJ|PyyCTK?q^y!8dGtc?tWQYd_D2* zt)~9~ZQAI!O{PSRGu-X{-ozg>DLp_Ql(ydwyhpF;Kiaw-ozA0e6A>@j<7e`)xh079 zuhu)i75FE_9|v7D#ahBH8)ck(b*KDTD%0WO$&jkqVN3DGy z@PpuwjJ_>+GgZIv%&-xrF*{^zJcn-PMf!ooaX%jy!IvKsJVmK^LDpLtBr-;0IE=Yo zO8z;oqQ7JBi`p;44+-mW>eH;#-n?%NT{gblW9i)2rqo zq92{S)gu)Yvg}K&lb5Gs-NfQ@wL()?ve5it)_xy&pTky{8l~Q!GU@S;mVLjvJba)x zLVc^pv`@BO>+nSkVBJqD#}HM|%*Pnp_}AYb5`H^)N*~$hNbnY%$$hCM%NIr&AQD(J zH@8aopTpiMe-C)0Oz~@5M=*xwIc1540Z`zO2jO1*WnEyZHofPvJ~}eN{{U!3LRY4& z+dlS}_CC_QH}K}~S@E6Sv+LTDEb+p}%ff+@iaVcL=)M#F#r_u5ei_H(KO7(+i>s@9 zXmAOII7X4Zc+M-`d~2xqFXI0I!|f~LP5!SQmwRafy`{6tp$_a8iPt=mKPvD15v}-3 z#=iv5g&SIe%l9%`&JuME66BM#=RYv75|#n-Q*zf`Pq(Xy!%lNf+h}}O@H_T0@DIjK zA5_-0N4c_&*74zp#KA{Ag&#%hTaW($1n$rWjb-r8hvG|%YhiBJVQvsJVjwZ*JAt#NBEM<4+nTkC*pUi0?I_;aNA{{T+$Ma}!l*ETc5JRx@o zuyY!z9XA@@rUMe)MNUmcy$lvNE9Hxlj@q70@dx%2i^4w&JW=AiyZJ8lZ7qY@EJJF# zs{q+waBJcZ5`NF08g$Jx&8BF#GC>?)X!2Gw=J_X+EdF+*T-dC zkey3()340(j{<+eH?(~^-sa=Qx~KX@#O5VPfN$Lf>T}SV{QUi;J`ibt2fi=f_$t-f zZ6YLw<|&Hdf=)S7JC!28NxU2X00jZ?jm&;Wj^i4A;!a(iL1i&+`=kA=R!{9yru-WC zuiz&5tKs{&X4V2jGR@~Me5lUtjTt_iSEq!-*22`1sd+Z6ab|do9wQAIP?B7_{Ey8I zC-!{!r{dTqyzu^ob9bk~^G7byJjMedR37A4++VR@?9<`@0EreFkBI&w+Mv`lsoKz3 zN;dgzj1wYt$m9yDI8U5lIFWywqF=kPf1~SN1~qx8c#Q_^Za= zI@V&gms-19ffF{s3zAL{b+4Sx@EF>Z;^(@S%dw_YjLTtP+0)T9?&YPK@*nN7;2#S7 zFZhgYddLQRU^2snfw9pyTuMSkS$Uk9A~-96ZluzzY~5fd?nC7XZ=${vezwc zV!F~=-6M&^Nh{n(g~E-9(a|3|K)y2`4ChtvpSoh-I7wdmUw5JW^+exb zoBb&8q^V`;o|zuieGc}-9UNx^xTKcYY7(ubq-^oSg5-6;>sNIfU6$w}2Ht3o?#bgn zTKcBso4-Tz`jsN(8|dHkigx!A$vZ^5f&h3YkZOOm-#kGlP>Bm+6!W{@pqAF*Zqo8y ztkM7&YXJIQf~U0)a1HpU3ctKmP_FzcLa@zoFVu2sBYoA zc1V|K6JU_1rfSvP(cC@@V7DPwsM?9vc-{sMRe%}nPp*4cElSNr>$$Bf@~K%Xc^kf5 zI&^VE8DQZFZg@V`hi^6PcQQ+4`$Ok<`!#D)nmF}UvXXRZfE$@PMRGY@_r+J5XSbBw zrZC4Pv5fItvZ}t%K&4J`SCYGuzNbxP;v4dfZESGyHuijd?BHz`(a(KimXX{fW;@V9 z4u_!!o;~Y~wbUiJvsl?x7B<_tK>3dcCbX?dhs?g>Xyg0X{t|J5e>&}@TTgpqmZU7~ zm8@#&8lT&e3(J-Ak~~KW3}Xd}XUBY+uHJUxX#h!9snEzv>HX~Susn9gS4Qwcc*-P z4NOR1CHKcFwqd2Qx z{mhasHB@s1>j^;bf;l@!PpPRO`$QIU#$f^xg3>=ZFMx1=3Y?~x@y!fBEzrXwFWqGW zkVmanZ?xW8n`p-ABl(#D;gk-z^&HhWB@+3$ts7Z+n6}2?T_g;;jH)j`dDiXHX0=pDr*9DEj^3Q$=qT-L=c!XUaG`&sAbLKK}I=OJlxkRVi(= zx2Rk*-RW;IoS_YZr=uFqf-xnoZT?aQ2wnixKr6on2VDL&V&*6;t>O(SkqPPQ4{=4r z5hPFcT&!gdMtT1LIjrR?>}gG?$Cf)crh+Tmp|?pGw%q4Du1`V@TbZpOVI)cCUN%5c z_dq=>0rc6WzO>VWv6k_YC_;ijE&!Fp?aD7JR&RfV8%m9^_Xe6f6IlvqPUr=6t!#^CX8&B0Q zt)h;``sOQnXNq-m6G(RLAb(o@O47a^{0{w-{12g8c#B3&OH|iw(mVN#Lg28CL(3xu z1Yi@}ty9cg0quOo5UMCyEwi4l3MMvZ+0o@<-CJr@OnFB(j1+vyO5PEsmJ3L&U3jKHH{ud1Luyhdh7!>yHyY zOU|a-omeb1u@z%V8@&#u>h9hPg}s$l<`j@TSs;uS2WcI^>0P(StLSXC%XhOYF`G+{ zP2`QSG=p$ldW!mo;-Bm(;r{>&{1DQ}6MkYfh`? zYRX;z03+k+yd|l4C*hu@@aJAhblq3QmzI-RK`NDb3NS=pbzT|Vv9?F3u-b0}Wjm{*+s z-G8xvgztP~;Qs&>_;XUZvDEx;;uy58L(P9N3&@`=$fu!DF^-t7JTjdcjyROsf>LdC zZ)2tKy=c>tjP#SYoxi4jM$h8dUJ+puh5g~f5uP*GJ*(P&AM1bF-W>4-)~6x1vTHxx zxckoJ5DI#aTKbFQ@B9<z%5-0a8!lUsC*V z{fE9AT6mAZ9uDyD+AJ-6OEYOxy2x2#c+g1F1wG0JA6o69Qo6;dR82-Yzb=T@93M&1 zP@K1RKNB?X7-=%aai-jm+rwedb;nHoY1Te2(A!Y9T|(4NYiAOSa^;YXS|Z-vhhZ;l$n>;AxQ()x_=)30Kq@J8E^Yg_)p-shjmMf zeHU2KWYq6xeXTe)#!dT2Pn&21(>3)ttj4_?Mw~9%TKep9{{X5OX(>3(-8(N{=gJnI zF7RfB;1BpnbSq+))67@w^L)ULm{aU4nU;MkSkWx6?wiWIxr#UW>JSosJ~ zCqL4@vW_MdJ>~cxABxMWQ*wo6XZf1?Ri>XL9%{Ms%YD<@wPMk4bsLrzNQp9oo_6%? zJ*?a_YA&ohRqIMh-}AtVr%NPOOv@w#f6< zIn*W@M70NYdu3=@f;0#cV}WQbbTm-MdXhA8Q&cK<$is=@m!eP z<}iZxUi0iy$niYS-Dxw+E;QI}8|ySTKyv69yM+<_q={Hl64(2j)Q`Rma7W$mS?9d@o<+?BoWLC!{A5N8>d~F(2N>kUd)BT@b zy$2Of=6NJuWuC2bWpvNBQc8y7XwL`otGad6c2~y6^sAWMmBIO07N@`Z$4=E_aE$QD#{-^6Kar_!RyU9;Dzs7qgPupArb9e&%x1cYOo2=>qj9mx z&TxG?W~^A)y2EjIF$(B;jB~?!dRKianjTGPC?$Q;`^KzRY_ZBvlt#ng^!ijcR*}gu z@*KP%;5IRjZ>?DSE0Bu;8=nLMep6Z&*4JVQUgkA|72xNiNC#E-uEix}k>4*^^Y;c)e~>=4ehX1ElOEx5G2nIs-m^6=F6uq_nB@rP zw(j72QuMU2YR^{ z>sxg)5i3c{0&sFW4{C6`k@qZ$B8+69UYR|KtR|xD_Bs=iO)Y%?0I$fUadwg_#(2XG z#~*Zh)qAKSgt(Grcw`0AMf=2`YKYy%Jk1-R2d?4M-!)ThvEsp6Y2-NC$K_0Qt{Gcq zchaQe-G5#B6(Ew+O$zqec_la?o&c-w0;Bz+4h}(F^as5@&qt0(nA{bLWck2jjo!o3 zqmkw}9!m}o4c|<2Q?{>TqE#a~zDIY~{Ymx%Yc1;S6SDxwJw1OKbUu8_rs6HX#lYjG zW?H?&O(9JA0Qz!qSKm`z#Pf)Um^UCDzO}pYVmKw&KGcJBQelP; zbCFrlrv0p@-$H6x<0z?iCh+S_d=)jL@VIotTw)SF4!8Q{{LYVEx*_?<1h(@!{+@KrkF-l|_p zrNE8JUrhAHW?Ox>SCLq4T#OOl+O&j5**9P@956kHwQ8NyYpJxUDs$iDGU&^2R3hO@ z^V_9H*B4-Xr^^_|2SY-}XK>KD!R&vAtWRuGF>$yQ1S$H~dX;#_&}UDj>J}!@L3VRK zH;G%WM+(16sVtIOGn>{7LCP>cd>)^bWh6>STZJl)FdTOGrHbVxBF5}_p+Q*R)6%Lb zBZ<*Xw;fks_#fnI#NzoFS)z%hQj!D6E7aBo=`Ex-!~#*eb>JGkEu{8{8qAp8er#v& zanKrOnm*GMlKFX(Jntu{=B+-ja_OZAxhHu3BcqXI5B7ysBMppWKJ{J+W|~j*Xw}Od zgCqIU$rM*Is=3R7l>?{H)n%LPzHD%?T#!z9=h}wd^)-xBPTThASo;$xF&Vb8JgMkA z4fO3=655NY ztoQbVeDigOSr{n2P)e7516DxvA!y@uW9lo@W z6v-iFAX_N;fIoeQR{ZLu=VN`RczFK+haHYOS3_xZF{c~KI^8eo-lD5Sw;_JxkO}x9Fe$M4?2NHbWOrEu=Jj--q-k5dCXU741RBtBu{Q1C=1mGNf>~yTIehDXB|77feJh_^ zYE+ty*Dqa3BAP46F(=Af<_de`(w`OcN>i!Z)XgX zMof9YV0}G*8o2gwy}#O-@gR_F!Tuhk{&j7cxVXhCkM8`L5=P%?NJzrBucbC&YiNr;^R2jjkF_K+2KZ9MW6dO)`1U7;Io}?m+4&R$nkuy5>&y-_(vWlEJl~ zY7vDeuN5_|xiDmIDw*lup0!#<7p2T8<;Dgv-A~f47^Sk)@Qhr5dY+@b5|*~Gsyizu zX5Pg|QylCPVn7>*e%&g0r)IWQLA}_5hl7mu&$U;B%$Oy@CU8g~(@bG9jN4C459L~` z>|K@DHt3OJG21+DRoQ_DxXJXYHr8yG=2w>t4hBy(U`HsM0PTwzKA05QB)5o9>@;^eD{Ps^U9{VNLWz16}>F@QlM9et}(IYca3({h#L9nC&+jL4Cw zEx}`sp5Lu*-PMIkP8A%jxYRL9o<+yYAm<$c0-%K?iHDN=1NJ@XjWlaC!gX&n3tLle0Rvm~EiKr0?=yA8#yIx-*`reO+0kCM$Vm1KfR)tTA-6dttsA1#?l#!_kYBbGa7h(<#+u6Bj)51ynN9*C_5gBN_O1fsO-SRC zNR%rm0GBw%eLd^!zu9KW<3iCc{AqGnTFr1RiI0%}Sj#ELZbf+b%uXRmeP3pM9}dx@ zI=8ESSg*wCeja=d@iw*L4S0B(R)!@q!(?9~cJlH!VE+IMV~<++4$H#&kBWXKSolgZ z7Up~9u~8?>lCQJ|{2fJ(=}O6HXY&N|X0e)cx=9AHr=Tz`7m2kNux(355~g z?C1cJMFT8PTJ+0#ZSCY7qH};3j0*gm)c*kBp?)Hc`rQ8jW20EAgpoIX@U}XG>0e!Z z9{8s>#lHD>in7ARO0)Qz@r{qaJs8OYc6rFU;O5e&kw6{S)xL?Tg#& zMhp|>7&yjz*L5B3pidwi`FPxWVER{s_n+$9X+d2U|BwSIPX|GZGg8}yu;89&Fl27NqplubaE3I z0p*%<=AC<0D5y&jj#^Iml@7D>sHjdk1YJ&hO8-Blm$3-H~`e%=GwfrwL|}w;hZ+d)A0S^E`?IkJ6t4m*ytI_0OrL%9z}=xlYmqlYP8w zR~g*CqNkR@Ru%HTOnOv_8!XPm4cO{CjJPpfv~`Oudls*?J`RkcfouoA4>fB@x}d=9yGg}(UsX}U_cy>$3LBP zPLgz*fi$1Ld!1yG6Ekf>K>MSo85ttFjUN8iXpBLcITZfwk3){exVNk!=7G~2Mrrx@zZIpp>8g%;sFqp>R5iF zx$h2J+t^yi6or|H40i&5ZEEvyv#h(A`@kDD)J|t796Z#fqpoIP|WSxs{Lq*86nFERmi-QhEOOG{~bx zX2Oz30Dn54Zr#jIO8wfdqGcvioD+^aSBGmKM}EYy#LpQhPaV68Pu&>ys3pO{9-^fx z$-K|a;-yXEq%uf5VPbNvTERD5^;eEZT+^Zvml`--C<*b(7_84m;Amx)WkjW7tmNhTxCpe>$ZDJ*=BhGsZq`!_d{(JiBNNh)V8-DGb$>{!Q4nA{{XF3)%^I1vUA_t z)}``LLFa*$9Bw~cQdhm$y+;Ve%d;-yj1Vcd_Uyt#pZE#unn-7T#KZC?a1VO38{o0z zGkWzj?$&_rO=1sj+;DXo-pA&ri8Px_`4?D@Qx(AZo9|woQ*PR78tvZ}yYknbXxox#)aHB4R83DTR zJq<3Y;tvt{mq?38yt;2EP7UNEF$w_B%A=-zE7xwm7Wn$#!K?8m{{T%VLzFB@IOi)U z`DFh9cRlNw*M1QA%TMrToAD}JR&5_qM`5sFuGiQPzVB++36H~WrBS8Q%Q@t$eAZO*FOwE5;zXC$%b>x}!~Uty{7; z!em&g&Qg|#q-ZqT}L4 z&xCwEW?>@oOFL-T?_;&LsU!WaKRW6@I(!=ON5d_9L-0H%Tdhw}uuHr9m{hb-&2ZRA zR1BY&1ZTZ&@zJMR)nB}~{S7~`>-!kalD+oxI2)U{mrO5xKGf%7C+6MgYYxX$wYt2H z*H*Y=YA^$5g#x$yC-7t9F0-uJ_-n*g(?d1nn;~+iAuvwur>P)tE4z>OS@?lE@l9=Z3b z!gal!Ztme^jE|95kb|Z%>t35}@E76?`VOz+=(PDIx6$1ro+!a*KBNKYIW^|Dnoo+Z zBA!h$%4uV=K`hM@GbrQ&ta=0O*1I8`;p+XXTe-swqY+95PL2mfD))GK^IzN1i&p zjl1}@p)ZrE%Izx;^`(hcUrhI^Cr*Ib7M5T1ggN7!j-Xd@;r{@He;52yq}=KjS}o-E zmX`kjX!DAnEn^&#So?EJ;Ln9#I`O^kzSm5 znxF8~)Vg^dCDZnf4#$jL!R6~KEszUsc1pB)Ar()l6{~rq>G0bL^pazExsbDP+;*>| zKV%PrR^J*vC0gkE{BqBrh@I{*8F&LFcXqE%{j7gw?Ki?-6yxy6!YJ;R39O)#>}nci zM>zzubAjKQ@-W;{&r+g)40O5l7|s)@sMC^K%kMuuZoG@DcHYd>7Q<()bebietqr=v zb9v?1vJg?6k+k+@&3XroKWBf8It8um?Ako-Yh#6$>NLXSs3(rN9<_(y?|@$#yk(|O zd*RI{d&s_IXKo`TbO4-Ik3OL&!hF_j@ngd{x6et(L~$@*>6!+rZSBy{WLt5K`Y=6z zUX=d;v|h(I+wAB4!^wr%-!w#?GChYC>iP%lwej;)p6Uni-NeaZ7n<`jVL<~XFvqF- zRwcK=FN#;%HP?vrZ4%~tdr3-N#)w14>Gy$8YSHHqmEF+)0M)!ipDKfW&JFBiy_Mcc ze8y69xw@LoyR)~0{cD%n2GW0r{{Yoq&Eao>e-^xD;wxVS=^7))sJa`L^G44m7a0#B zJP&I6C*k+}6Z_&fi#$hrqWHT0Jt3?jSs0AVxk(@#jyl&(JaizFsPzgQDUPWx-LG`` zQ_3acZ<%t-OhKBRf7Zck( z;;)w+tqH>v&r*8vT=#%KWnYZn5%lY=M@7<8?1?Ov=Hh7rEJhE$4}6O2shZM+Zlt>h z`kjiXoMjI)!y?pVg4$`JQ5FIH=cqhlsLQI&eFv3zM&X8yiNV?duU+_C@QcPCJMsSj z{3|^Q-|bq)pQsyKh}lVwIMs@@vUb`!*Xeh}pZF*D!-z+NJODeBR8-glMpF4JaE`B(9*DDH38DkdWptnLm=LgoD z;_$=v+i*V4NJ2^HcYZ7NSK?3X7ozw(!KFMUb#bd|a6}8F%nW%X26a)$K9%$L#t+## z_ruz6hHZWx+234WYk+;C+C#lnf`%=h;pZTViPxVjWmVe$0D*;06vU}DPAXP^dGO`c z&D?M(WdN}wdiBFphU-nWWCh+xKmoS!%zM|?Hb1dX#vL=n`dx>P;9DIxQnstDyL&MDT~h zhkJ>&Q!UzCl=&mKR#?@Gj(%U_&3Q+}PuWl6$AWF4xtb{MY?2Zqn@`6;_dE6>1nz0l$SCjZWXpDIUHbfTecEPmMV_^L{b&=Msj55Ad%X> z`P08)FBUJuYo8VPr2}fAtXjgt)w!82W<-3c+d8WJBXuL)@Y-~1_59=1bUup zpLL1t5;MG9hZ|g;4m($6<9~#|621{?jih+5M!K{FnIO55a*`sExs`_)&3W0knoHHV zx^@eY>IXu4@m}Q&N42FIk=g8go+AM)Jy}9-*8EMec<|e(`y#Ogm6^FLNdA>h*xfa| zD2VxCw+!$VkEKAf%PsGpJFr-X0Y|S&u?5r1B#}JWjo1KB^%bl=)^bV>CuEH~dr6_Q z3NmGomkpkSju-s*t597?(*nSK-m$GP5q`Ud);2kHZF|(QowVWtmn#-8#G}A;#q% z-(1ywLhr!3XX%_c5v5>q@yU|BiZN2?Ds`iVT&!{zMS+c~I z@LaS~UQA83s-GyJ0g3eDs7W-J(KI&Tv`wEWSn`1J{OaTRGT8Yn3=SOk`A-?+-l;vT zQAV=dIgMmf|RiDHXS^4$X52g17jJ)Or<$l@ru1QMh>Y(i< zB#JZLrMyvD&loPa1G&vuS)+#1ONo?cI}~^6-lX#U*(EdJ&Z-D(bga9dCgp@oeq`Z? z_m6W^8=a6*ot3=b`~%j1XPK=R<9CAYFW^hq7gd0~a&nm74^L6nzn~8n-1vjy$Hn^} zhk8xqnr@4v>8q(-+`w@oyrU7I9f0HWujkX@fwJ)b0E|38sc+1%-K2>SJhm8cee3tv z!oDQbyf5+NQNKDZ#-VSmXj*^S+Q@HZjKq#N4uEbLHS-ysEh_x7cKCJjZ&UR?4XUeQ z++nm^{Eupb;S5@FGHROS@}n@@y}4$`LQijM@~<5D8T2tF$A|UAlg~KvOo3TWGrO{gi@cHB!66}T7@%iXJ=7jCQT!9q zqtrYX;>}&ISS8Fh@S{ujknAOh`sS#9&>s`EzkuEg@V|rp9ce|T)+21(!iGX5K&<Y-bcg7X05-@j0 zJELy-vq$ZB@ln1X_#?uf4m=M8*8=AD(%@Y?Wtm_iXqac6rGUwPeFyt{#EN=w?D%RoqtEr{BfWJQ{mY#_TviDTi^|; z{_Z+gp-nN zB=et2`~vY_wj{dpZy%~0Rs{`l6mI2yNTkshCRxwAaEbhgrAq0l= zI-jL;*SBX=m77d$vZ2c<9Z2i=SEXy{pUn6gSah{}p2e%^mbTg!jpMudBl|V{vf4&s zUA{!{{{Rawp;MgKg!um6_Sz{g?F%3bvf+9@;s^f#UcCcO*G9Fc*=V}4F=>`Eoyf=c zYVr@BO}@N!uQu@}si=6C_Dw3-3wUAq5`K}0QNC6+!Cs4nIVroVJD*9A;p6rCb$c{^ z=Kj)NCTafwVLcmDx0zu#7P=vLhCXFhK+3)NBDs&+w_en4e_<~Rc!t*TTK@p-+APi) zfb%0eLk+#i|v8kZ2gu6xI*dWuVqe{^)@L|zX5zT)gFJu? z{k=R%CXeEu+5Z4ix42nW+r_cZJRyPJuL=c_f&3$$YvT{wH}+}PejI3D3G{3K02JP9 z*INFo{hM?4xeIK#aUL2K>J>pN_zLJhYj1#>m&5NJd>PWG@m>C|d_k$fq_bX0xLX+H zg-b@qrdC2f3h*javYK&sn%BzPT$o_#+7gV{#`Hdb{jq*0w4bz(!fy(Adst9no*Rpe ztgRFcJ<3L`amM1A#d>e;VdFi2!@so0z%3g}y4x0^s9iMZ*x4RRL*^0ND(nS(4f|<+ z&1vxS<2S>oycwxa5%IO$dUQf*SV-IBX}40PxuIsY(#^lbSX{Q0T1lsQN$c{r z+9aYIW+`QNDH{y&ykqmPihpQt*^2YSKeT_s{{RZu_^$r(weu1S%b82vTT2bfsgO2% zt7Uz~Wc+abm85UmJHWpV)5RB!9=l=vpxT7;7rQTN&AE4HflH1W@Ct?1)QyNpN*h?vUkP7eeftUYmG z7yjG61Ju4Q{@&gX@B{0KZELCNcGlp&5nxU7F;zdr%y|RWj~{#z)jTWvaeO`aeFuoH z*>(6Db=2BIZWm;7ZVE~2p+Fy6`RD!$9i{lc;7{!n;SF}nSGd$KZ1kIJyG=sn_ekYCiK}p!|2^bd~c>Loy74-tORCF+#liW57NCn9u^o36Hlu@7UH~G$76lkZZ0}r z);oPOSiG|E?Z$&R4X0g5Cs><;Cso+84}H9SE2sD|@P$4n{8iC>A9Xy-YpBc+wW9$f zYs)l;*;HiLnqI&!H9fIR5)#7&x+osi_7CiVsA=8`_~E7agT$9H*jiiK-J?q>z<)9# zsf_m|*NdFZ)uQRawSD~qhOkkVB~_`>pQ9Ha4*vjU--VY@cwbzz)1i{q$Xj{$@rk5z zIV~f%@wZy}L&LrS{h0nLd_>f|Eud+4b8A-W%V+khzbe-3i8+!bAKV8aM`2#;b@4ag ze!1e`9P0i$woO9Hbhn1a+Envh-XKfNq;$!^9>Tvm?Yw>9?}xv%*Mz)H4E{!i4e~U* zco2!)h289+jE_NIC-rH28G9`(e$jx!ds)ZbPp-%5oIe2m5BxgTw7&?Zg>T|d66@QH zP~5_Vym1V>25uEl1J8Qpd`qBN{0#9ezO#AY4Le8EFK!kX?xVz#sgU8ZoTwb02Ws^F zOT|&#{?@(#*Cc4B({FTn?ChN%{#r^-ThQ>ryC2)5#5Ni)!@UC1>JK^ZHTzJFjQL>! zE&l+0bj5Fl%Od@x`MwqUUgvDvbBtVWrQ~>*#GeOvW5IFS{7Jd+?Vt7rsh#lLDvg&c zJij+^7{Lc3zdZi{XwQj%3%q~gG}8PbrG1X$Pgvug?fzf3BBngN3=OH+_pjL>+G9$O zRsEYTW7F-B+e2d&&yyg*3mS&N^dQ&sx2Wkhy7kjfD-^pM^S6RjoG3qrE4qX^XsVNY z+wwEQWE63grwX46=u7bC%fi}kk1RDW6AEYD!uqe_c}jPP~3YhQdNHinaRu zkBl_G6nF<(gHhH`gci!#l-)yo)~gycY}+G9_~$*qt6vBFN$_LF{y3lEp10uZEk9Me zd14A-RZGZLHgHb>hZxAOeenMPf?~Y=nPmRUNSg61E^e&EgMP+WX%;elKp4e*6YvJ| z(^mbcJR5hct9`oqOM-U{GRlxS1L!e|^k-h2X-cOrT@RPVC1-K_H_R}KE8P9T|vG`Yk$*G%VYkAp3Z-sX2jiSGx--|!9=j{Eg z_{QVK{{R+bQ>p6s7UFAXp*pMHdM?MrCg_re&HUI)ZOHWX_O71q z#FmX9)22{X5F2ng0lh2rNA{@wm%bkSDEK)iiT)gDv02`;+S^`TEAK_MmQR_$Vn%rb zBDntm*^~Av(!68fTaSo-FS1#n)fQ#Z?a3f?Dwi+Z<@yeo=9NhPR*SYz%o)q{~`#yfpx;MsO6(m}f_le`Rxm$=J4$7BFnC;>R zrWYm8bDGC8#$&0~g(ybc-+|4NWEeRq)Tu9rbMU`egHzJ4KeKhHq_DV*;3KKt0qMuR zPpW8MCegIWXVt9kts}YSH=ZX*+!2)_*khjD*V|vV55=E`e-gY`qTl>E@cTyAWXY*t zO$dTZnDU`wMC_~1Th_lzJZq=;GvlZ1gQ))i!U3kitlocPL@lhDN1ttZ0x2>4z!h}> z;O7RqaW4|8)2A6NuFqEsl3}V*t5WaXdUijbD_dP*?53YfvXbrvR2ODAnn1brbq5ut zHNA`x#R-A!w++%3$dG#+^Iy?F?C)jpKg0h32Q*C&NrO&58@R5)C!;o;aMG!Mu2gE6n=E_xzww8)>@;_&oV_q z6sl2`?)A6z(D_2@7@0RDrIhg-ciq@EV#@bsKQHe}+;R1-OGtyOTgapULEYH>DmkrL zE`%_?-a*~%Tg_{9j{=oN2gu#}g{`@03i+EPPTt48ES8AukgEZO!65ZLX_DK^9qT%_ z_2>_8%A|_jV`IN8PdTYebj2xI^C@m?=z3*>YSF#Y?>i3D_+p{)35}$ZH7m+qS{A+V zt=J@OP(lwz9Q3aaQ=1M=t(87hUSuHF-0ToLOMI+kz6v|X79^J7*q3m z)WhMsCkq$ttMcqgHO#AWVz*tWt(^OJt+YkDh`WGG1GN7Djbq&I4;;V`DDX=98oLyU z1Wr+o8tf#liQ@NeuGE6I;!IlKS`bsu4U*BgDXlBP5Q7 zq_`86Qsk9f0mlNHYR2-Uo8^Dkp!thAxiYhm~t~zxi%NH41OHYvvl7Nh*fGf}*^*MNMQ7#!-{IF9C*RNWydo*go z-aZj{W&^cOet3eyLMKi;k6M`QT$620^%`ZB*X*#w%Dr%V{c4GxbS4J*Q#fOehqYNP zBybgZw>xk+XB~#;8UA022lY04Sv7 zvHPR3HFh}Uj?cc@ARV9$*rwOjFB9SLWbw?qh5rmsK!qE-%sngFt@mYUL`9k zC_9_m+Mvu**?ot0(5<*0ayrL-7m1OR5?N7zEwa1<^Q2@X_%?+Y9c5dx! zr*Ulov5>U51e_>8&Zde94bR$m^4NU-+3Uq;lv|5|_Lm_^%7gD$UO=!kLn9}X?MZeO zMDM2Wa+}=4_M&!fK-?(x?OG6--r%uZpdAk*=~V3?iPe<+u>+QJIpeUaHgd`Ki8eOa zT=GcZ9@JY;AgyaP9*eK_xg54^#z$;?`~~g@YL#t_LJ~oBQZhX~b4^(Dd$`D0X9c@s zyg@=&csyLzhG|aL~ zxd`w5D00R;MaXBt2e=*SM*TyPUfZHrE+Ux8brNlxfLEYAcBmkd^6u#v@gH&99Whcg zE*WOQ+L_$FI^z{A7Xo1-ZSvu66po`EYe>fIcS$)#f50ccl)-QoAU2Q!s{a5wY?gxS zdP(vz1eQE;RAbGy(7(NG;YswXmrHWnyxzNWfN{lYoODKft}Z;U=l=izs|Lu=*rt8a zNBl#n&uUllW1J%{JivB?-yZc%?R{)>! zlh~Z(eJf6TE0N22I8R0054wemh+hSi{LDQYpv_sl(_aNqlU{`DEKGsou0^Xd$Iprg_82I3TJcti;#=8uOL!G+-z((c0}(!z!G!jn-cy`y6!bpF1n5(v8gQSy z)~CWgCDnXKtm_w%Ud5MU!Nxek`cu3+@W%4~(WO;pzBy>(9D=?703ltEj!b$ti#I^t zWD??1df<0j^gj=LTkyArXOPAsiDiVU%-piE<7geT-o1>%wQ8@}!)spWuY&ea)~2ZQ zPlvy=wf_LbZBF9S7kHwO|_P-7->}%aCiiP*1H6R!!MNqR7GIQ2YTXsBdLA9 z!rDzt4?bAKGj;`rc&(jM_TE$xK{E-VINZE(pIRtemiIoQ?dW%~yv4vA?l{M#Y!t#i z?i_JhT7Ep3omY~eR8vu)=+ zK9t$ivaCwDz#~4ijCRB!=$v%pnwIWYE#qWXh%p#$2szD4HZvBB?|swnQw9;hNdN*5 zIjWEo5AT~FFM2%7PWLAS5LAB;UgtD`7=p^g5>8Z7g!3J)6a_3gdi&DGfD<+{K?w@CXAlf?&tNEx*?)J?edAT4Ce1QaBw&~U0lT#|ZGm#C z!=^y@sD{N}oRf_D)ZSdMkt*})>qk*N7UXOtks1~xbpYr2*XB2f?w&u0+Lfdba&Baf zMm&Zp*e)yd=6L{x#tD;YV!pn(ugv{&8580}vcSGwvm2Q)Ki+IM2_E^bxXbrV4u`~! z(@Kl%(xtGF1b&42j&u0etLS!#J+Awe{{U2ab~)}V%PkGH+)@}bKI{Fb@xEmiJap=@A!@mr3; zCRt_UcLUQWy>?Ps9P%|K^B@ayFzh)awhc+=8-fb@(CN2{vqvGM3Fk}P!Kmkp7l@naz(f~4V>{(Bw?{6gALo? z(vwyiA3QMz0yibQpL12E^X(>CV+4S_k^0ruM)}tqk<;gLvdRyEZ zS0^!n8ED8mj!J>jkj*C!!*SqJPa%i_7#Kf~6=h;zfZ&b8H2p-C^)Knp?%SN_kU&0_ z^Uv)o3AoVg_I#|SMjn|MucEBq%tcfHFgVY8`P=rM^KCQ>PxrMY(EfvUWc)Q^*>bgFAzMRvDxs(kYbi~fX2$eHf@q}314sr6@}vsUr5)# zW?uz(ej?FBZFg%45s~xxau9NR4E3+fE5C@oG_lh^u&lLdk{D2-iCg3d$8l5Z%|~P7 zZyH-^HkxPJ<@-EStmz)&Ramx31E(jN#B}vusz)9+?kT`09r&&TPVw)FwC8OPPQQ4gkQ~IWLIyd{ z9<-?Bsxe&8R8;uFpET7vd+dF;<81@L8rQ>3AHn_^`#t8jZ*&s z(%%#OYoL4s{hTcPAEY!k{vy=v(g%q!vMb2HynGIRQQp5H{0rh;Pr#lrZCg&emOH3} zZ(wo(IBfIYlj2v!j}-XJT!&xPB{$cQ7Ff)qBW^hPM-{akmXz^S>rH76{Y>$EVNu{E zf`*c*ZC6BolK#S93G7VThsM7d+zFJk%WW;!@H9k*kh69jsh_j=#3+1i`%(Df@5Buq z+!t=qY3Vj41IduA9$tD4xja|KdUx#!;|KUqpA9s*6{K9^lw{_yaWQWDRERx_P z_cF-L+lv-Gd!8$NFAr0N6spTsWsKtV>NP?<$#p+)z8U;n_;;ju0^ZM9xU{#vy7Sud z0#q3sXQ)1-7_OJ$9nZl301)UCXqr8xmAftUm$%l_ZQku87Rw&$c);i@@IwCpSHIO< z$#HWg>|=J~F}Au72zZBH((Ny<;nb#rLZuX}g-Z`}oSNFUMTM&rt*>*R4aE4E)V;2c z=%je7!*y9BdfiV;w8uUyt4b_$T5|8R~kzx#AmL4_I4!c5?!& z`Rs=ya2?M=Yx6XGLGc^|&xcWy&peroj81xS&2d+kdbQp3mwJuVlg&9hot&{C@(Im% zQQ@e?J5;c-JY!RtNmA85r}RoD zp z_I4;B{p2w=^5FE%URRM}uow?vT0HWTcl5pg03M`g9A{q>kM{JmQC3T%({uNF^Y*d$ zNjwv2SFdd`Yi4_Zz!=PjfV}lMu9x9=$Df1O-WW?*H61$E+TEER7=p$DCpbR!{QaqG znF?CS?h5X~Jn`E#J(LRyNhBAq0Vi>YHUVMK*3!zd`iWA7HoWd|(c>&tIXTtT{)Df; zYhQ$39D++JFJgiz=VV7LHW0s0>t180ZR0akhvtvDS zMnJE_mP4u+w7$AxN6dtrjpnIdTmJxOLa{8R+z>OKnDnismf+(U!;?f><6K0mb2Pb{ zwY9gY`)TmA_MrG*;vb9tB-6C>9lTnC!40k4$|4N0ZW|SjJDlRZm&bp!f5YF0w?0H# zoxHaOF&)5C8b)${UPgEo{P5VxeG;32AwU=qz~HGJ@+(U6;_6}N8^)e_Mh^BYa(nYl zVDQtNs#SyXHva%!aWv`1jAE{|Z&Ug%_H9=Sbh z=bbP1#`rVf`we44du7umI;G<^oI`h+8$U7(ah4VMuV=1nc9Fwux3j|eyS9?Gy-(iX*?Q;UcD3WL8GLi_!M3&2 zFKw;uN!k&kk~QBmIX%eRn)rvspS4_`C;hM_@ol~P4-VMeCYNEC*q>rUvR3E&?Up{( z@ehaoDtNm<@IB|l&ktU`m6hemy0{UsR*{%(#(fCJN40r9)rO;SdkkGJC3TJvKZ)Fc z2jWe7Sw>NS!@Nam-uJY}Nr0LcE5JW26~;dg<&H?8=R zZCc*qT|zC<=2dJiI0!S-Jn>!|rT*8S1vMWRcwbM`FGTY280`6^o?sA03F*nres61E z2X!4c!dBMaDAZN;%ScgH$IUJoWF+8wuNCvXu7=t!zZy=`Lvw2&aMAtvuyeS3gI;UP zFcYICO6{xabkyR!Qp6`+B)pI5Q{u;p{2}1~00&t3uU?iIMV0-uxS4?=UQzO|&~uM! z`AL6okA$|~Bk>LOyzX_JT65 z1`9Hb0^X!o&o>$*X){d$GAo>&#~7|!8Acuy;-##*oH&jxt17bP_hZuiPuo8W{@T6< z@h|NKrfT*o*8UxnOOr^F>g_^^-K6u8#(wB5Hk$Ttj6byp!7mPJ`aPZ2p>3!Oo9N$V zu~}L*5@n=jA9P^z?O)HWv&(52h_iW205~`tb4hbET$3Cvxj5$;$F^$e<(N)qIb6yd zVTz1>pDJs){S|okB1{ut|@ zv^1KYjj6%*d)*7{W_JNlTthbgSoAwsjQ6j@4NFticUs$B%<|j8_ah{bpyTe2)cyC? zmr_k*aM78UX~!HlQC!tBic^m%toWw1rl!(K>b^(om;4kT;?#Y3rn$HSR@`PIX_bj%>`rR_ zkBv6~F&BZm4{OlU%t6;2Cd=JPCEtIk#Az32B^$Vf7pZ=G|WIJVqlo;c>M zTUjK~wZ`3zyK8PgFHW^R-PRGWWW}8JNfEW9u_57)OJ8pRw zl%pv}O@F2GEFUV!tW#j*gP!BKtp+Ia4wQt3ae=C zJ-lo|LOOc|J?V0`thFPPnmqpidAS@W3%jTddhBzp|jsdE%7;kLV%lT4QyXyoSvf}g+B6Bv%88LVihD%LLSGt{OffdB}v%#7Z10XXas?a6byR z+f9O|u9RJ_^)e(SUgOH#koEG(&pqnIFZPQRH!frQKP$9#+-jBW+rkzPC^>VNBd7#b zON~N3KH1?{-5YHsyMyUmZd&Skv7tJVR$9HkBIMUH$R0>zA&AGW>dx3?!-mA`{63#r znoUJyf(5quNF1N7T8=ALompElKl;y4&Zbo?tZ1)EYa4&S?j8)*zp(ssq3YLheWmSh z;&Tx{DZ_X6ujx_#De0aG@m-d^DlNvJcj4zMx1?XM}@EwPWJTe8{(bAII;pCSm%6P!@P#>=tub9WiD!RV< zerM|Z4>c+>dnfv%3se69f`RyGYPS$rXlNJKWv*15_l&2O|sI8 zB=+Ya-?1E!J*)F_A)sAqe`B*j9J{%dkIYvjkaO=|gYg$t(--0G&Z@RQC8R!FOr1-g zl;<6)Xgk_*S1XL@JGPm{{5}1wJYRXKc%Q;nVZO*Li!_nBQWiWeann54(O&>R zY(Ey>+0Eg5{UQ88ai+pn%IVY2nlK3Dk`L20`DO4o?HZ@Xt6vRTF^(O5(XFi;Ht01k7L>s@(_>C#RLp8FnM zJhl{@=BU}?{{R*|Rr^T4#m!g6iKS|~9+Lk6W-&<^+XJj=xd|hJ4>_rO)g#fC((V{6&>xjj1ydr( z-M8lgxc>my55pcTviO00s9VOCmUCM@z2vO>i-^ucD*phq*uk$dwlzwREM290XlF+( zjH^qVWUsGNr1)>~i{f{~FNOaA@PapmuJ6}Dw`E(oA|X;Ya;_PFi)&`TFg3kiIDAd2 z-pa-a3$&fZ9DZ1qpEOiK(#tt=o&a5{oUYit#1;nd?VdzD{9<@k#Vu~g{R z+nKo7@ColM_Cu&lv8Z(nA+3{!o3CaHe2oJ+KymxEj(IK;o&D1UK z;tF_eq{Knwj==S=LkXHmK|Dn#8>_Cm9xLG4Cap8`pIGrxy|`7=p#c~JIa7deK5i?_ zd|9t*^1_RwG>s4>lNdev2Cv);pZF%X!%MG)7QQ65@g?M1gt}s0M=2<(@<|YVq-PjC zF<+3HUYQ?;UslnhK4JSqM#e?UhDIZS+PhxQq{FZC7DO`5r1pC#mjUTo5#}AF)4m3Z8UMGPCu9(m> zO9W+1!qq}VTy)6lPpxtj>bE*2z`cqE2tHSAf2>?{n#+U4Re7F|zm1Z_bj$ zNhKv4X>XDB^l;JT->v>w+;7vRc`C?`r0Rtcb z+OB+O{jB~fd~mh!R+Fp69khN8dwFcq1t2uG>Zl|>_(5JX1a1P7R^YoaEIBF<@U?8( zJ+s_2a*rStK2wYwFdujEq^m+Hw@&M0twBdryw7a?s6T5TiXXH_ui-m{vVAmoNxb+T z9Fi{|W@X*m?hFd!{yzTKpBH{N_>;lf=Dz}I_Z|vp=CqF7HYB=7Ml-lS#Ki`CSC>QL zJL_2_O@JhF10pE@09lUTTAoX_)Nk#tbm;(ocO<}WP)<&NrFYV+g^UzuPWlt|SZRCj zbJadNe%8MkzBhRD!QLeCCA3;|cxHHSwA*M@tYUR_b15gBor1RhGyc_|8U7)BQSg_= z3+drY-w2hu(^eJ4QBAlshX<~B2E1cdxP|X+q`4_|84Rm~f~9(6(z)$G+}%ckc|nRM zVT_H-pXXS@y*NoaRF$o5Oe*1``BWtAuKx4szZL%4e-V6Z@ngeZ75qb|K)2o+fn$a^ z_jzv0cZoUZOLL0jyjA;Bd|UBv$IB0n4SKL@x^#K78+Su(7SsEcQ`9z2eREzcbqmzJ zw~}c5`%s{T3&s>%yvvE4<|GT1Lyq*NnDXkW^h@Myu<1rAx9;BGJ@aM+;ZV&A}SHrSxv9#J!A1iADpD#_@i6*$8+E3#T#J`VzIfKN$Kebq{ z;fKh#iXLN*0L8)&a8EUjrfc{51g_UmER8loN;A&{1aiNT6kB) zws*Rkxr*{Hw5vYS4kQXa4RikhX|8pJX}fOyPn^f_#3vb4jFzqFe3y5vUER)t3BnfV zIpdF7_g{nF2=RBu{{S6nejXPW1}5dEhxigdz0$fi{gpIGw4EyBN%1|c?xA+knOUR@ z6kE7#?(fA}}zpNF3Y{yq3dz}^_tc39gNi&@li7+hUU0NOLsFnHp)^DJFzv1<<- zS$-W^wo8#xz|K7G?cT?;YJUQ6d^3Hh_*yBFOc0H6IsM|GK-v9k;17xa00`x~_{HI` zhB`&(+b^}3PmxYUPS1cC`r{|*U#B0nAHrXZx{r;uPZ#_*X>~0lRE8;DIAp^{$f~3- zu5q3#$bV(;+0)08{8)qH=fx1(O|FSKxH4Vv3%u*8GM&F+GMlZPa?M8GA0{cP@XH!JbQclMR?1> z^7ty_!**$9EK%&9NpivAA%VxPMn!kwrR?V6ed3k<4?312PE_R;tXqGL&q4jGeky3% zFTi;;Z9;U9RMm9UMv60ljOaIDj-2O;{2Enez;nkj~luBn}&BKhD3Q--xkM?>&u!K*the)TP3 zy0tOxEy**fCuga_ugyP%wi7SyZQyHr3xW1cF8=tX)Sr5Lca8Sv)r#?4yZ9USrTCfQ zT@57j4Tg^olNGe2M}EuPv!0>ola2p*vJufxq}#yane?Bburcdc?kA`&d8eieAD_IS|#7W`JYvxeux%q98b<-HevgAwBk;r|F9Pk+X^@)I%g;^Y_^H^s5V^t=z4#ytM-$=O2KrF&J3Znw+C{ zc({D-SY;?DzU}IMihND@x%)o+LHPCHjbl#J?FNIQ!*{0Y)~y=B6wyfA)?6+(Y#`$` z?;jKY0Bc`@UJ>|z;vWwDDw!nI^{Z)PRk9vh$0NcaMg#x`)#;B){9Dv?>8%sZfN)OT z$L|1mJkzZ#5<;$-D<9q?`qHJBQTE=}+MVx+u@4I>YHOBHw^Q~f_FVYg`#)$u4s`n| zNZw5xx%YBOD(_cLBZGc3j1?-F@XE8w5ntK)^A$Ipq0;vF*7$EInJ8%su${bZ@U zyE}Sh^c{tK&3|ho7b|mZ_c5Ff2flr1lJML}j;bS2yn)7j>uOZthf`7AoS2Nlz9O6# zH@d!7etpI%adyHHg&E_h%{kPVUTGULDeLX&Q+abDJ>{!{jrWJ46+(NqlwlVPK-_&z zdGd~`=Oh!8T))?;0)3>TKcV@1b*z@2`DA!!QVH+vTgdRWyEVYW#&V<9oR%Nj4JFN? zE&^r2Jw59hiRsF9!n|S3>PJrH-v&I^n$k%D60jWyILNK998YmOmYo(SkEt9JyVj`q zJ_ghEhLR!!vh7tooo7srpw1CAF=juF6YD#DyPI*1bc+cCpLhHJwyt12nDNjPy0;*DY$-69qYK z*-$yarAaA9(cKq?llJvuuWOPIEb z${l`RPJgXWXv-43Qjgyg!=8mm>G)HMw?me7A1jm9{{UZ*tciJ;Q0wJRb; zY(=vKkYu>-M@m^@XY!gk0<3Sz9sB+@UQ-OyJAhF~T=%BhGLE*_Lb|2YrCo;DSnUI* zaYe$~*hFMcnr+zxb{XcUlkFlFBMg!Nqy3TX^{NYMvf2y6c|(n-zh2*kB?G1nH+elT zvg_(=Lp8LrDZycoLwZ)hj@g39z%c~xQg|Q^4RUsc)nCcuZyfv5*=jPa;@RCWd2HFdCa#_Bri9BSvP7`SkQbk0^{6M6M9Z+BAbi8n z5$RJ)2bpYV02Bl%IrZ;XZEWI8b$Oe5AK@LAnx2{?7dS<8KfC(sQ$@GBZ#b(41P?*a zd{uC;`OHRSNc(!9e`>UshwXb6Ra9pf=)#$Ps$=r()Wprp=ea#;N!v@67Sxnpm;4IT z>@DXEZaLZqtw}r2D2sO0QZU}99<=YVJS8P!HmDsr#%g&I5Xm&75&QM}(q2V-NZngd z*$&Sx4geV-`qY+=Qr>lycq8Re*CV}01h%MI<6YciB=)AXiWR3^6(EhEwkKi;NEV|Vs-U~pLEulpU1Xvpn(3=L?R zkv>-*xE*Rzce%Xz6L!@%_ZU){46fx^;C1vKwWX#?kgwY1VCbiWJ$dh0v(IrDj-*tAOP7$tYP+Lc44(B?YiS~sg(^-sT<0Y96xvHmY*jV1WcA#1*73m_ zy2>1|1TUx3p!-d{?!RgTZ5SYT&q|KwDc&gee1IHabs43&c#JVZ1{h#u4!-o%osNjb zJ6+xDuVYRUOsaqejecKZr?qB3pKWwvi}zcz$EfuDYTCnbZkZ!;gOwim>-kkH$yVYo zwMN-^uoxbqu4_$6qP_QRQv{b#w2O3kj$PRUsVmcpd~#e^+^m?~WJN5ek&=4VyCs@y zqQDKVoNhd>27X$vZ3LnB2^K@hK;L_wgYcp})=arhKJsaK{{UV@_v-6#kRqHA8KghL zIV1|F90_wXO(EtCV{yPEpv6kBr?JTC_*DC%SwLum`9u?rgn`fHN>jTO zTZ9r_zpuQ_x1W9Qk#xZaK3K=c5a1P#}Q038@R7teqwYBnp*5hKbx58Y^`xk#+_!ZL&$dVxx2MAXy+@Gy- zNpLRrEQ1lp(~o|YVray0rOYfr&jYVCn@>_I)0Cv6H>s~Cqda4C97x0zDaJmv>mLub zi+S|zQXNH)+6LD^rz}(sN{_913^r5DQd@!qY*mQ$9r>ynCy1ku#BMA%vcz666c#0Y z!0lYv+T4j$lhHHN$}rU`P1B_tyK7^w*8D52__6glbXioGVl1m8ZET-I>t82dTkDp$ z6IoxYiDPUPJn{}J^>g4)!i_V>ej3*`ZAG1(BO=U3`%21Gle`Xk;}!64crV7S;mG^W2OE8bc77c2Plvoc6|Jhl6lk#` ze31dlj^B-X_JH~|oHIkQOu>dT)C&1rMl!Y{tZGSH$sb{bRN)?2?s`9i66LgLg62tB zV1PORI#*L`Wjtk`f0?)7aoF_F(y{yjb!%qxDVHJ6R2~35@N3k3y9s3TcL4o*@mt~I z)8su^%GSBFrY!#eWk+%gFvlL%c0m4Dm_|>fX2Mq}B9kB-oEp@7g;YQ}Vacstj>FfP zSZhjUj7H;T(Z{V)nI(O=`G;Ke=}ww*#1tJzKU$61nka~7C z5#IysbqtM+ReS^c!1-bGW7?$`1dKTf2>v0SXhX308~3gO!5reJ+baX)unXRzf-qg^;>;#Q3Oe=r z(3Re#JKp4531Hoj=N|QG?owt`5ym=$RlG^v-(Kc_oQd zBq_l zrw1u)@RWdi6!duHQyEt*eY$f~va4BTm-nhj#ye-OYFQ6Ks@C#I5-++tn!69rCfxk1 z*0Yu-*rW^`b@e{=BDCIN4gvxPPU4V_duUAai6J;uI49{}G=A4t2E)R34j9U^OvqO} zjHxyBm85KhKI|YpJxQ;ce`~W6ct1^^Ron%`aqL%`%C)&}`>)LT{v*HDDta^WM*i1- zpCshu76nhCJ*$wG2ei508Q&Qh803M9?C)+o{{XaDAlw<5v(=On$K_e}_J3uz6EvT^ zLx88a@0#`LR^?Wa_=BYCYH{By%q6%4M%=L-YqHZdi%V-~Scvm>ovubgRjl3iT3>7`<-=ZrDv zYW9b3jd5X|u$0J5j>nHA|R`OO=*6WNa(0;0OC%Dh)rw_t!UaTD%f@ppmqKxb@C`E2hyr zE3RsdEDLPxV+9Ey;A4SZk;FkO><Y*ergMU2q%-c@H&+zBgwY3cw#g=wa=oXT-!d)Gr7`Z6DPtzv3 zt5k%Y+tBL5;iV}kM{~~gOGsq6^0fIfs=zS*@f~TGcG3roz`Nr(SHb(Dy|2UHvfsu1 zT3Bb(wC9Q|qE#fBf~t&Qv9CB+;cbUS0gMyr%)%%C7e$F2Sejs>fPc5cF_E_OZhI?WaUNQmsits_N zGZmcUVTpwrQM_$?y)Jz|Tf?||m~UxO`^WR8;Ti2NwJ7vs5hhHqk=Fwtb~P`LbX)B= zQ&}x7&CR^%F@@kb;EdPSzZ|tMf?o@MHd^>YMzfA1$l;J~Zez&k*cJ8gS({$S}UbrXrm0%Fo5Ny`IX- zTX+!Lm;V4*j4{tn4Rh~u@Q_#MT=06D^(}YeSHrDyQ?a!B3)Ul4>qdMzl z-29G?b+?oQk|iQ$IaQ@}*Tat#{7cRA^G76&gjehnHTC}h?Iq!VfnT%FhwN=rOHU9> zsWTRiNk&me&Ae^;@m)~FSB@$)6fbmn48|Cs zS1~c%GKU$@y?%lI&pPMq>G23^YvRok*4){mrL=Q4aE+bEIpq3RNAW>E0(djSz^2~Y3$USN*;&BSIqM~+F;mJx0vwwm4tY#7E%N@yho%5W7$sUzj5UlZ} zWsHgg2IvVVfnTOy0`-sCuj4cq58|yV+S2mcQjkRC2+jZl817AU-yVJrd!f$56iS*+t**h)8NV&RCw9PXx#pO`wlcGg#Dvbo%<35;{|V>Gr& z9AoMkj?CKvObsA%9CMI6W74(3*T-WkPMU?S`W-d+Qx8TI`RZ!+N5i`9 zo!^IV%vznXfulQ5bF}{e^{S-)B8tZP!u}WuTUioSCpkM#LHz6XKjDY$;qYtXpTk%@ zW8w>ko@?S|X?G4boNdRxE9d_Jik}5O5;uzU9X`@{bbA{~KFM_xV7&I(&f)05^fX~J z97jCVp?JhqxH-|3A2sB7?dOcVN2*vq#YuFDTS|o^wTAV9XoQSDZO5i_Ups4h{l2%V z&w04Vb9BdZX9EYVe&zU6;V*+G{hWM5q+Oy%puLUEwwBxgyA+t^B>IpWHTeVKuZ5SN z7W_wL;a>&V0&LJV{qi5(##nbhg=LfG-rEUQtf6HVac3C^v&7TGbKX){x#a#5((Uf` znMuUyYi~5lzg(X79V$!ocA@nb9!znPAxNvTs>RcOPQx+ ze8lo?w^$1RugGrsJ*pdhLR}(5Z!{ayaez<1aBFYHz7f>?D|;@Jti>c3_EHsOWh0Ts zMtXzT)*SNc7xuA6P&8Kiit+Pv*1ZT#a;Yw-&Z<#f?3R~zzM7iNHKJNDo-pL{j(V@P zQENeWY}XLVF%SfU!94SdhYvJy+s3=t5s~$)^2_#TYaoQir)!L3=B;G(vCN|3@bCH* z#iLyQ^_y@QPkNT-Gp3X*#hHNly8+l!E$wEuStj4Wn<&hpUpg!>Kys3E$Q56Fw-emO90n#vE~In`oZ!?4I&@e){q_WuCu)!jc>o#B!@wc8!rfb2#N-%R=%!e17qa*V9zmDlyLX2xZ0vd*GbkLPeZ zfN|2O+$_<$>X9$*?x+N+r`RD8>1q`r{>yt0R0VD^Da?hgawi!56~Y& zRO8sqSxWHLe^*42&alf7ww)uEbs%TdoMYIUr*jM~bq&4zEYqZ$2RI>tQ62Qf9qRMX>B|)e`bKu1w$*p zL}T}bOl0qFhSegXqMg&(k=q%BqT*!|MUk)roM87JwAh5JcC4W zESEFe>C$b8b?gc6jQZ2;p|Y}qSHD8cbFqQ@t=N0})yjPgn^Anp>!yYrv$8l@NRo1X zW~ps0Y$Ux_y@{AGA0Yd`i1w>Cg1}uFE;30UDfcFjNIc2pkw{Pm-J=-5u1QJVrLofL zHFX=UKdTilbkhc=BUJ|7@KmRDJ*%d>_;2FfJqD)sawVLs6-noo8C?Dp%`(c~B}>bj zfKhx+u`WtY2?vcW;>P3G;Um89S_?J_$@ZW~CZ#v~H^lg)@MB{z5 zDx*HT*l}Nr-T~J+Lq` z`sc)PvRc|eyQjoR9ll6;S1#7D-kT4QE+=4o|Sjv_knz4;17p)_P!(1WU+^8!{xG*waNSYhZrWmPprRc zt4LsdPgd~$scUBVxcPmN!+{{V*mDri?;3e&tbdvcfiAzwBOmMq)_ z$m&l+SyRBc%}!OcN8RRGrAI}_*xN(!-{3x>H-`Qo>b@t67Lw;q((j~$10^LOD1XS; z>rd>Ns7diV_GkEwV$KGus7oX_0Wc+%ytQbC53PI;;Qs)JTFs`b@fG3I&Xq2QU?jUs zpjJqslIde8LGD4XyuaX|o&?c!{{Rd4r{a#gs5CkqsgWgvB^73Yf%6Yy0`XCg#d}w4 zX>^Ysgw);M=`wr$8}@+sOYla23h3SyNASJFz4oOp@Yf310IMkHZvfYs{0s3H$19H& z$F2Mpu#);qizc?zQHWb;l>N|4akw6x>+c_mAGFWI7VyTbwhMEvxI=EX)@aVIBwPaL zpdVWJBlcJLldSwc@s6*h>Uu@di(HkCpBiorYb!Z*1qa?zt@vWOYE$-cT(Z8U(5%~> zlsX�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%0AH?zcS$r6+B|ZdY!4uHWWCCH2j& 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ⅈSF)PM);J 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