Add BLE Wi-Fi provisioning alongside the setup SoftAP
Wraps ESP-IDF's official wifi_provisioning manager (BLE transport, protocomm Security1, NimBLE host) so a phone can join the device to Wi-Fi over the ESP32-S3's own onboard BLE radio, without first connecting to the 192.168.4.1 SoftAP. Chosen over a custom GATT service specifically so the existing generic "ESP BLE Provisioning" iOS/Android apps work today, before the dedicated DigiRadio app exists — same standard protocol either app would speak. net::ble_provisioning::start() is additive, not a replacement: it runs next to the current SoftAP + POST /api/wifi HTTP route inside NetBootstrap's startSetupMode(), and failing to start it is non-fatal (same pattern already used there for the SigmaStudio TCP bridge) — SoftAP setup keeps working either way. Proof-of-possession is the device's own serial number (same source as the SoftAP SSID), so pairing requires reading it off the unit rather than being wide open. On WIFI_PROV_CRED_SUCCESS the received wifi_sta_config_t is converted to the same core::WifiCredentials type the HTTP handler uses and saved through the same ISecureStore, then the device reboots into STA mode — one persistence path regardless of which transport provisioned it. BT1035 is unaffected: it's a separate UART-attached classic Bluetooth module for A2DP output. This uses the ESP32-S3's independent internal BLE controller, switched to NimBLE (smaller footprint than Bluedroid, the only host stack needed for a single peripheral-role GATT service). App binary still has 33% free flash after pulling in wifi_provisioning/protocomm/NimBLE. Verified: idf.py build, doxygen (0 warnings), check-manual-sync, check_si4684_blobs, ctest (19/19) all green. Not yet tested with a real BLE provisioning app or on hardware — board is disconnected this session. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0178rASQ6ZETPMUamvpoR2KR
This commit is contained in:
@@ -7,9 +7,10 @@ idf_component_register(
|
||||
"src/SigmaStudioTcpServer.cpp"
|
||||
"src/WifiScanner.cpp"
|
||||
"src/NetBootstrap.cpp"
|
||||
"src/BleProvisioning.cpp"
|
||||
INCLUDE_DIRS "include"
|
||||
EMBED_FILES "www/index.html.gz"
|
||||
REQUIRES core esp_wifi esp_netif esp_event nvs_flash esp_http_server mdns lwip secure_store tuner audio bluetooth station integration ota bt1035 adau1701 webradio
|
||||
REQUIRES core esp_wifi esp_netif esp_event nvs_flash esp_http_server mdns lwip secure_store tuner audio bluetooth station integration ota bt1035 adau1701 webradio wifi_provisioning
|
||||
)
|
||||
|
||||
target_compile_features(${COMPONENT_LIB} PUBLIC cxx_std_23)
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* @file BleProvisioning.hpp
|
||||
* @brief BLE GATT Wi-Fi provisioning, additive alongside the setup SoftAP.
|
||||
*
|
||||
* DigiRadio firmware — https://github.com/manvalan/DigiRadio
|
||||
*
|
||||
* Copyright 2026 Michele Bigi
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-08-18
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "core/DeviceIdentity.hpp"
|
||||
#include "core/ISecureStore.hpp"
|
||||
#include "net/NetError.hpp"
|
||||
|
||||
#include <expected>
|
||||
|
||||
namespace net::ble_provisioning {
|
||||
|
||||
/**
|
||||
* @brief start — start the ESP-IDF wifi_provisioning manager over BLE.
|
||||
*
|
||||
* @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).
|
||||
* @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.
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-08-18
|
||||
*/
|
||||
[[nodiscard]] std::expected<void, NetError>
|
||||
start(core::ISecureStore& store, const core::DeviceIdentity& deviceIdentity);
|
||||
|
||||
} // namespace net::ble_provisioning
|
||||
@@ -43,6 +43,7 @@ enum class NetError {
|
||||
StoreSaveFailed,
|
||||
CredentialsNotFound,
|
||||
WifiScanFailed,
|
||||
BleProvisioningFailed,
|
||||
};
|
||||
|
||||
} // namespace net
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
/**
|
||||
* @file BleProvisioning.cpp
|
||||
* @brief BleProvisioning implementation.
|
||||
*
|
||||
* DigiRadio firmware — https://github.com/manvalan/DigiRadio
|
||||
*
|
||||
* Copyright 2026 Michele Bigi
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-08-18
|
||||
*/
|
||||
|
||||
#include "net/BleProvisioning.hpp"
|
||||
|
||||
#include "core/Secret.hpp"
|
||||
#include "core/WifiCredentials.hpp"
|
||||
#include "core/WifiSsid.hpp"
|
||||
|
||||
#include "esp_log.h"
|
||||
#include "esp_wifi_types.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "wifi_provisioning/manager.h"
|
||||
#include "wifi_provisioning/scheme_ble.h"
|
||||
|
||||
#include <cstring>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
namespace net::ble_provisioning {
|
||||
|
||||
namespace {
|
||||
constexpr char kTag[] = "BleProvisioning";
|
||||
constexpr unsigned kRebootDelaySec = 3;
|
||||
|
||||
// wifi_prov_mgr_init's callback is a plain C function pointer, so the store
|
||||
// and the credentials received mid-handshake are kept in module statics —
|
||||
// this mirrors the manager's own singleton lifetime (one instance for the
|
||||
// life of the process, same as NetBootstrap's other network resources).
|
||||
core::ISecureStore* gStore = nullptr;
|
||||
std::optional<wifi_sta_config_t> gPendingCreds;
|
||||
|
||||
void rebootTask(void* arg)
|
||||
{
|
||||
(void)arg;
|
||||
vTaskDelay(pdMS_TO_TICKS(kRebootDelaySec * 1000));
|
||||
esp_restart();
|
||||
}
|
||||
|
||||
/** wifi_sta_config_t::ssid/password are fixed-size byte arrays, zero-padded
|
||||
* but not guaranteed null-terminated when exactly full length. */
|
||||
[[nodiscard]] std::string_view boundedView(const std::uint8_t* bytes,
|
||||
std::size_t maxLen) noexcept
|
||||
{
|
||||
const auto* chars = reinterpret_cast<const char*>(bytes);
|
||||
return std::string_view(chars, strnlen(chars, maxLen));
|
||||
}
|
||||
|
||||
void saveAndReboot()
|
||||
{
|
||||
if (gStore == nullptr || !gPendingCreds.has_value()) {
|
||||
ESP_LOGW(kTag, "credentials success event with no pending creds — "
|
||||
"not saving");
|
||||
return;
|
||||
}
|
||||
|
||||
const auto& cfg = *gPendingCreds;
|
||||
const std::string_view ssidRaw =
|
||||
boundedView(cfg.ssid, sizeof(cfg.ssid));
|
||||
const std::string_view passwordRaw =
|
||||
boundedView(cfg.password, sizeof(cfg.password));
|
||||
|
||||
if (!core::WifiSsid::isValid(ssidRaw) ||
|
||||
!core::WifiCredentials::isPasswordValid(passwordRaw)) {
|
||||
ESP_LOGW(kTag, "BLE-provisioned credentials failed local validation "
|
||||
"— not saving");
|
||||
return;
|
||||
}
|
||||
|
||||
const core::WifiCredentials creds{core::WifiSsid(ssidRaw),
|
||||
core::Secret(std::string(passwordRaw))};
|
||||
if (!gStore->saveWifiCredentials(creds)) {
|
||||
ESP_LOGE(kTag, "failed to persist BLE-provisioned credentials");
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGI(kTag, "Wi-Fi joined via BLE provisioning — rebooting into STA");
|
||||
xTaskCreate(rebootTask, "ble_prov_reboot", 2048, nullptr, 5, nullptr);
|
||||
}
|
||||
|
||||
void provEventHandler(void* /*user_data*/, wifi_prov_cb_event_t event,
|
||||
void* data)
|
||||
{
|
||||
switch (event) {
|
||||
case WIFI_PROV_CRED_RECV:
|
||||
if (data != nullptr) {
|
||||
gPendingCreds = *static_cast<const wifi_sta_config_t*>(data);
|
||||
ESP_LOGI(kTag, "credentials received over BLE");
|
||||
}
|
||||
break;
|
||||
case WIFI_PROV_CRED_FAIL:
|
||||
ESP_LOGW(kTag, "BLE-provisioned Wi-Fi join failed — waiting for "
|
||||
"the app to retry");
|
||||
gPendingCreds.reset();
|
||||
break;
|
||||
case WIFI_PROV_CRED_SUCCESS:
|
||||
saveAndReboot();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::expected<void, NetError>
|
||||
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.
|
||||
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,
|
||||
.scheme_event_handler = WIFI_PROV_SCHEME_BLE_EVENT_HANDLER_FREE_BTDM,
|
||||
.app_event_handler = {.event_cb = provEventHandler,
|
||||
.user_data = nullptr},
|
||||
};
|
||||
if (wifi_prov_mgr_init(config) != ESP_OK) {
|
||||
ESP_LOGE(kTag, "wifi_prov_mgr_init failed");
|
||||
return std::unexpected(NetError::BleProvisioningFailed);
|
||||
}
|
||||
|
||||
if (wifi_prov_mgr_start_provisioning(WIFI_PROV_SECURITY_1, pop.c_str(),
|
||||
serviceName.c_str(),
|
||||
nullptr) != ESP_OK) {
|
||||
ESP_LOGE(kTag, "wifi_prov_mgr_start_provisioning failed");
|
||||
wifi_prov_mgr_deinit();
|
||||
return std::unexpected(NetError::BleProvisioningFailed);
|
||||
}
|
||||
|
||||
ESP_LOGI(kTag,
|
||||
"BLE provisioning advertising as %s (proof-of-possession: "
|
||||
"device serial number)",
|
||||
serviceName.c_str());
|
||||
return {};
|
||||
}
|
||||
|
||||
} // namespace net::ble_provisioning
|
||||
@@ -23,6 +23,7 @@
|
||||
#include "esp_netif.h"
|
||||
#include "esp_wifi.h"
|
||||
#include "audio/AudioService.hpp"
|
||||
#include "net/BleProvisioning.hpp"
|
||||
#include "bluetooth/BluetoothService.hpp"
|
||||
#include "secure_store/NvsPlatformInit.hpp"
|
||||
#include "station/StationService.hpp"
|
||||
@@ -131,6 +132,15 @@ startSetupMode(core::ISecureStore& store, tuner::TunerService& tuner,
|
||||
"without it");
|
||||
}
|
||||
|
||||
// Additive: a phone can provision over BLE (no need to first join the
|
||||
// SoftAP) while the HTTP route above keeps working as before. Non-fatal
|
||||
// — setup mode is still fully usable via SoftAP if this fails.
|
||||
if (auto bleResult = ble_provisioning::start(store, deviceIdentity);
|
||||
!bleResult) {
|
||||
ESP_LOGW(kTag, "BLE provisioning failed to start — SoftAP setup "
|
||||
"still available");
|
||||
}
|
||||
|
||||
ESP_LOGI(kTag, "setup mode ready — SSID %.*s",
|
||||
static_cast<int>(deviceIdentity.softApSsid().size()),
|
||||
deviceIdentity.softApSsid().data());
|
||||
|
||||
Reference in New Issue
Block a user