diff --git a/Software/components/net/include/net/StaClient.hpp b/Software/components/net/include/net/StaClient.hpp index 7c5186d..4dadf3d 100644 --- a/Software/components/net/include/net/StaClient.hpp +++ b/Software/components/net/include/net/StaClient.hpp @@ -17,6 +17,8 @@ */ #pragma once +#include "core/DeviceIdentity.hpp" +#include "core/ISecureStore.hpp" #include "core/WifiCredentials.hpp" #include "net/NetError.hpp" @@ -94,8 +96,20 @@ public: * @brief connect — join the network described by creds. * * @dname connect - * @param creds Validated domain credentials from ISecureStore. - * @param hostname STA hostname / mDNS label (no .local suffix). + * @param creds Validated domain credentials from + * ISecureStore. + * @param hostname STA hostname / mDNS label (no .local + * suffix). + * @param bleFallbackStore When set alongside bleFallbackIdentity, + * a link lost for over a minute *after* + * this call already returned successfully + * starts BLE provisioning (additive, + * STA reconnect attempts keep running) so + * the app can reconfigure Wi-Fi without a + * power cycle. Null skips this behaviour. + * @param bleFallbackIdentity Device identity for the BLE fallback's + * advertising name; must outlive this + * StaClient when bleFallbackStore is set. * @return Ok on success, or NetError::StaConnectTimeout / * NetError::StaConnectFailed. * @pubstate writes connected_ on success; uses creds via Secret. @@ -105,7 +119,9 @@ public: */ [[nodiscard]] std::expected connect(const core::WifiCredentials& creds, - std::string_view hostname = {}); + std::string_view hostname = {}, + core::ISecureStore* bleFallbackStore = nullptr, + const core::DeviceIdentity* bleFallbackIdentity = nullptr); private: bool connected_; diff --git a/Software/components/net/src/NetBootstrap.cpp b/Software/components/net/src/NetBootstrap.cpp index 7cdcda1..fa97efa 100644 --- a/Software/components/net/src/NetBootstrap.cpp +++ b/Software/components/net/src/NetBootstrap.cpp @@ -189,7 +189,8 @@ startStaMode(core::ISecureStore& store, tuner::TunerService& tuner, StaClient sta; if (auto staResult = - sta.connect(creds, deviceIdentity.hostname()); + sta.connect(creds, deviceIdentity.hostname(), &store, + &deviceIdentity); !staResult) { ESP_LOGW(kTag, "STA connect failed for SSID %.*s — credentials kept in NVS", static_cast(creds.ssid().value().size()), diff --git a/Software/components/net/src/StaClient.cpp b/Software/components/net/src/StaClient.cpp index 079a6f5..d30ea04 100644 --- a/Software/components/net/src/StaClient.cpp +++ b/Software/components/net/src/StaClient.cpp @@ -18,6 +18,8 @@ #include "net/StaClient.hpp" +#include "net/BleProvisioning.hpp" + #include "esp_event.h" #include "esp_log.h" #include "esp_netif.h" @@ -39,10 +41,20 @@ constexpr int kFailedBit = BIT1; constexpr int kMaxConnectRetries = 10; constexpr TickType_t kConnectTimeout = pdMS_TO_TICKS(45000); constexpr TickType_t kRetryDelay = pdMS_TO_TICKS(800); +/** How long a post-boot link loss must persist before BLE fallback starts. */ +constexpr TickType_t kBleFallbackThreshold = pdMS_TO_TICKS(60000); EventGroupHandle_t s_wifiEventGroup = nullptr; int s_connectRetries = 0; +// Set once by connect() on success when the caller opted in; used only by +// the post-boot branch of wifiEventHandler() below (s_wifiEventGroup is +// null there, since the bounded connect() call has already returned). +core::ISecureStore* s_bleFallbackStore = nullptr; +const core::DeviceIdentity* s_bleFallbackIdentity = nullptr; +TickType_t s_disconnectedSinceTick = 0; +bool s_bleFallbackActive = false; + /** * @brief disconnectReasonString — map ESP-IDF Wi-Fi disconnect reason codes. * @@ -136,6 +148,32 @@ void wifiEventHandler(void* arg, return; } + // Post-boot link loss (the bounded connect() call above already + // succeeded once): esp_wifi_connect() below retries forever on its + // own, which used to be the whole story -- if the AP never comes + // back (moved, replaced, password changed), the device retried + // silently forever with no way for the app to reach it and no way + // for the user to reconfigure Wi-Fi short of a power cycle. After + // a sustained ~1 minute outage, start BLE provisioning alongside + // the ongoing reconnect attempts (does not stop them) so the app + // can push new credentials over Bluetooth. + if (s_disconnectedSinceTick == 0) { + s_disconnectedSinceTick = xTaskGetTickCount(); + } else if (!s_bleFallbackActive && s_bleFallbackStore != nullptr + && s_bleFallbackIdentity != nullptr + && (xTaskGetTickCount() - s_disconnectedSinceTick) + >= kBleFallbackThreshold) { + ESP_LOGW(kTag, "STA link lost for over a minute — starting BLE " + "provisioning fallback"); + if (auto bleResult = ble_provisioning::start( + *s_bleFallbackStore, *s_bleFallbackIdentity); + !bleResult) { + ESP_LOGW(kTag, "BLE fallback provisioning failed to start"); + } else { + s_bleFallbackActive = true; + } + } + ESP_LOGW(kTag, "STA link lost — reconnecting"); esp_wifi_connect(); } else if (eventBase == IP_EVENT && eventId == IP_EVENT_STA_GOT_IP) { @@ -145,6 +183,7 @@ void wifiEventHandler(void* arg, ESP_LOGI(kTag, "STA IP " IPSTR, IP2STR(&event->ip_info.ip)); } applyStaLinkTuning(); + s_disconnectedSinceTick = 0; if (s_wifiEventGroup != nullptr) { xEventGroupSetBits(s_wifiEventGroup, kConnectedBit); } @@ -212,12 +251,20 @@ StaClient& StaClient::operator=(StaClient&& other) noexcept } std::expected -StaClient::connect(const core::WifiCredentials& creds, std::string_view hostname) +StaClient::connect(const core::WifiCredentials& creds, + std::string_view hostname, + core::ISecureStore* bleFallbackStore, + const core::DeviceIdentity* bleFallbackIdentity) { if (connected_) { return {}; } + s_bleFallbackStore = bleFallbackStore; + s_bleFallbackIdentity = bleFallbackIdentity; + s_disconnectedSinceTick = 0; + s_bleFallbackActive = false; + std::string hostLabel; if (!hostname.empty()) { hostLabel.assign(hostname.begin(), hostname.end()); diff --git a/Software/docs/app-cursor-brief-2026-08-25.md b/Software/docs/app-cursor-brief-2026-08-25.md index c7ecc05..c852b39 100644 --- a/Software/docs/app-cursor-brief-2026-08-25.md +++ b/Software/docs/app-cursor-brief-2026-08-25.md @@ -5,9 +5,48 @@ un suggerimento. Se qualcosa non è chiaro o sembra in conflitto con codice esistente, chiedi prima di improvvisare una soluzione diversa — non inventare comportamenti non specificati qui. -Dispositivo di test reale: `http://192.168.1.62` (mDNS `digiradio-CC4DB4.local`). -Verifica ogni funzionalità contro il dispositivo vero prima di considerarla -finita, non solo con dati mock. +Dispositivo di test reale: `http://192.168.1.62` (mDNS **`igiradio-CC4DB4.local`** +— vedi §0bis, è cambiato oggi). Verifica ogni funzionalità contro il +dispositivo vero prima di considerarla finita, non solo con dati mock. + +--- + +## 0bis. Identità dispositivo rinominata "DigiRadio" → "igiRadio" — OBBLIGATORIO + +Oggi il firmware è stato rinominato per coerenza con il nome dell'app. Tre +stringhe sono cambiate: + +| Cosa | Prima | Ora | +|---|---|---| +| SSID WiFi di setup | `DigiRadio-` | `igiRadio-` | +| Nome Bluetooth | `DigiRadio` | `igiRadio` | +| Hostname mDNS | `digiradio-.local` | `igiradio-.local` | + +**Cerca in tutto il progetto app ogni occorrenza letterale di `"DigiRadio"` o +`"digiradio"`** usata per riconoscere/filtrare il dispositivo (discovery WiFi +locale, scan Bluetooth, matching SSID durante il provisioning) — non solo +testo di UI. Candidati sospetti da controllare: `DigiRadioDiscoveryService`, +`BLEProvisioningService`, `LocalNetworkScanner`. Se il filtro cerca ancora +"DigiRadio", l'app smette di trovare il dispositivo reale. + +--- + +## 0ter. Nuovo: fallback Bluetooth se il WiFi cade durante l'uso normale + +Prima, se il WiFi si perdeva DOPO essersi già connesso una volta (router +riavviato, password cambiata, spostato di posto), il firmware ritentava +all'infinito senza mai avvisare né dare un modo per riconfigurare — bisognava +staccare la corrente. Ora, dopo **circa un minuto** di disconnessione +continua, il dispositivo attiva automaticamente il provisioning Bluetooth +(stesso protocollo BLE già usato al primo setup) **senza smettere di +ritentare il WiFi in background**. + +Non serve un nuovo endpoint HTTP (il dispositivo potrebbe non essere +raggiungibile via WiFi in quel momento, è proprio il punto). **Azione +consigliata**: se l'app ha già una schermata "Cambia rete WiFi" che passa da +Bluetooth (dovrebbe già esistere per il primo setup), verifica che scatti +anche quando l'app perde la connessione HTTP al dispositivo per un po' — è +il segnale che potrebbe essere entrato in questa modalità. ---