feat: BluFi Wi-Fi configuration (#1321)
Some checks failed
Build Boards / Determine variants to build (push) Has been cancelled
Build Boards / Build ${{ matrix.name }} (push) Has been cancelled

Signed-off-by: WhereAreBugs <wherearebugs@icloud.com>
This commit is contained in:
神奇bug在哪里 2025-12-13 01:43:32 +08:00 committed by GitHub
parent 4b582f8074
commit 0ccdc082b5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 992 additions and 39 deletions

36
docs/blufi.md Normal file
View File

@ -0,0 +1,36 @@
# BluFi 配网(集成 esp-wifi-connect
本文档说明如何在小智固件中启用和使用 BluFiBLE WiFi 配网),并结合项目内置的 `esp-wifi-connect` 组件完成 WiFi 连接与存储。官方
BluFi
协议说明请参考 [Espressif 文档](https://docs.espressif.com/projects/esp-idf/zh_CN/stable/esp32/api-guides/ble/blufi.html)。
## 前置条件
- 需要支持 BLE 的芯片与固件配置。
- 在 `idf.py menuconfig` 中启用 `WiFi Configuration Method -> Esp Blufi``CONFIG_USE_ESP_BLUFI_WIFI_PROVISIONING=y`
)。如果只想用 BluFi可关闭同一菜单下的 Hotspot/Acoustic 选项。
- 保持默认的 NVS 与事件循环初始化(项目的 `app_main` 已处理)。
- CONFIG_BT_BLUEDROID_ENABLED、CONFIG_BT_NIMBLE_ENABLED这两个宏应二选一不能同时启用。
## 工作流程
1) 手机端通过 BluFi如官方 EspBlufi App 或自研客户端)连接设备,发送 WiFi SSID/密码。
2) 设备侧在 `ESP_BLUFI_EVENT_REQ_CONNECT_TO_AP` 中将凭据写入 `SsidManager`(存储到 NVS属于 `esp-wifi-connect` 组件)。
3) 随后启动 `WifiStation` 扫描并连接;状态通过 BluFi 返回。
4) 连接成功后当前固件会延时 1 秒并重启,使主应用在下次启动时直接使用新 WiFi失败则返回失败状态。
## 使用步骤
1. 配置:在 menuconfig 开启 `Esp Blufi`。编译并烧录固件。
2. 触发配网:设备首次启动且没有已保存的 WiFi 时会自动进入配网。
3. 手机端操作:打开 EspBlufi App或其他 BluFi 客户端),搜索并连接设备,按提示输入 WiFi SSID/密码并发送。
4. 观察结果:
- 成功BluFi 报告连接成功,设备自动使用新的 WiFi。
- 失败BluFi 返回失败状态,可重新发送或检查路由器。
## 注意事项
- BluFi 与 Hotspot/声波配网可以同时编译,但会同时启动,增加内存占用。建议在 menuconfig 中只保留一种方式。
- 若多次测试,建议清除或覆盖存储的 SSID`wifi` 命名空间),避免旧配置干扰。
- 如果使用自定义 BluFi 客户端,需遵循官方协议帧格式,参考上文官方文档链接。
- 官方文档中已提供EspBlufi APP下载地址

View File

@ -583,6 +583,10 @@ else()
list(APPEND SOURCES "audio/wake_words/esp_wake_word.cc")
endif()
# Auto Select Additional Sources
if (CONFIG_USE_ESP_BLUFI_WIFI_PROVISIONING)
list(APPEND SOURCES "boards/common/blufi.cpp")
endif ()
# Select language directory according to Kconfig
if(CONFIG_LANGUAGE_ZH_CN)
set(LANG_DIR "zh-CN")

View File

@ -671,6 +671,29 @@ config USE_AUDIO_DEBUGGER
help
Enable audio debugger, send audio data through UDP to the host machine
menu "WiFi Configuration Method"
help
WiFi Configuration Method Selection
config USE_HOTSPOT_WIFI_PROVISIONING
bool "Hotspot"
default y
help
Use WiFi Hotspot to transmit WiFi configuration data
config USE_ACOUSTIC_WIFI_PROVISIONING
bool "Acoustic"
help
Use audio signal to transmit WiFi configuration data
config USE_ESP_BLUFI_WIFI_PROVISIONING
bool "Esp Blufi"
help
Use esp blufi protocol to transmit WiFi configuration data
select BT_ENABLED
select BT_BLE_42_FEATURES_SUPPORTED
select BT_BLE_BLUFI_ENABLE
select MBEDTLS_DHM_C
endmenu
config AUDIO_DEBUG_UDP_SERVER
string "Audio Debug UDP Server Address"
default "192.168.2.100:8000"
@ -678,12 +701,6 @@ config AUDIO_DEBUG_UDP_SERVER
help
UDP server address, format: IP:PORT, used to receive audio debugging data
config USE_ACOUSTIC_WIFI_PROVISIONING
bool "Enable Acoustic WiFi Provisioning"
default n
help
Enable acoustic WiFi provisioning, use audio signal to transmit WiFi configuration data
config RECEIVE_CUSTOM_MESSAGE
bool "Enable Custom Message Reception"
default n
@ -694,7 +711,7 @@ menu "Camera Configuration"
depends on !IDF_TARGET_ESP32
comment "Warning: Please read the help text before modifying these settings."
config XIAOZHI_CAMERA_ALLOW_JPEG_INPUT
bool "Allow JPEG Input"
default n

View File

@ -0,0 +1,721 @@
#include "blufi.h"
#include <string>
#include <cstring>
#include <cassert>
#include <algorithm>
#include "application.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#include "esp_system.h"
#include "esp_wifi.h"
#include "esp_mac.h"
#include "esp_bt.h"
#include "wifi_manager.h"
// Bluedroid specific
#ifdef CONFIG_BT_BLUEDROID_ENABLED
#include "esp_bt_main.h"
#include "esp_bt_device.h"
#include "esp_gap_ble_api.h"
#endif
// NimBLE specific
#ifdef CONFIG_BT_NIMBLE_ENABLED
#include "nimble/nimble_port.h"
#include "nimble/nimble_port_freertos.h"
#include "host/ble_hs.h"
#include "services/gap/ble_svc_gap.h"
#include "console/console.h"
extern void esp_blufi_gatt_svr_register_cb(struct ble_gatt_register_ctxt *ctxt, void *arg);
extern int esp_blufi_gatt_svr_init(void);
extern void esp_blufi_gatt_svr_deinit(void);
extern void esp_blufi_btc_init(void);
extern void esp_blufi_btc_deinit(void);
#endif
extern "C" {
// Blufi Advertising & Connection
void esp_blufi_adv_start(void);
void esp_blufi_adv_stop(void);
void esp_blufi_disconnect(void);
// Internal BTC layer functions needed for error reporting
void btc_blufi_report_error(esp_blufi_error_state_t state);
// Bluedroid specific GAP event handler
#ifdef CONFIG_BT_BLUEDROID_ENABLED
void esp_blufi_gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param);
#endif
// NimBLE specific internal functions
#ifdef CONFIG_BT_NIMBLE_ENABLED
void esp_blufi_gatt_svr_register_cb(struct ble_gatt_register_ctxt *ctxt, void *arg);
int esp_blufi_gatt_svr_init(void);
void esp_blufi_gatt_svr_deinit(void);
void esp_blufi_btc_init(void);
void esp_blufi_btc_deinit(void);
#endif
}
// mbedTLS for security
#include "mbedtls/md5.h"
#include "esp_crc.h"
#include "esp_random.h"
#include "ssid_manager.h"
#include <wifi_station.h>
// Logging Tag
static const char *BLUFI_TAG = "BLUFI_CLASS";
static wifi_mode_t GetWifiModeWithFallback(const WifiManager &wifi) {
if (wifi.IsConfigMode()) {
return WIFI_MODE_AP;
}
if (wifi.IsInitialized() && wifi.IsConnected()) {
return WIFI_MODE_STA;
}
wifi_mode_t mode = WIFI_MODE_STA;
esp_wifi_get_mode(&mode);
return mode;
}
Blufi &Blufi::GetInstance() {
static Blufi instance;
return instance;
}
Blufi::Blufi() : m_sec(nullptr),
m_ble_is_connected(false),
m_sta_connected(false),
m_sta_got_ip(false),
m_provisioned(false),
m_deinited(false),
m_sta_ssid_len(0),
m_sta_is_connecting(false) {
// Initialize member variables
memset(&m_sta_config, 0, sizeof(m_sta_config));
memset(&m_ap_config, 0, sizeof(m_ap_config));
memset(m_sta_bssid, 0, sizeof(m_sta_bssid));
memset(m_sta_ssid, 0, sizeof(m_sta_ssid));
memset(&m_sta_conn_info, 0, sizeof(m_sta_conn_info));
}
Blufi::~Blufi() {
if (m_sec) {
_security_deinit();
}
}
esp_err_t Blufi::init() {
esp_err_t ret;
m_provisioned = false;
m_deinited = false;
#if CONFIG_BT_CONTROLLER_ENABLED || !CONFIG_BT_NIMBLE_ENABLED
ret = _controller_init();
if (ret) {
ESP_LOGE(BLUFI_TAG, "BLUFI controller init failed: %s", esp_err_to_name(ret));
return ret;
}
#endif
ret = _host_and_cb_init();
if (ret) {
ESP_LOGE(BLUFI_TAG, "BLUFI host and cb init failed: %s", esp_err_to_name(ret));
return ret;
}
ESP_LOGI(BLUFI_TAG, "BLUFI VERSION %04x", esp_blufi_get_version());
return ESP_OK;
}
esp_err_t Blufi::deinit() {
if (m_deinited) {
return ESP_OK;
}
m_deinited = true;
esp_err_t ret;
ret = _host_deinit();
if (ret) {
ESP_LOGE(BLUFI_TAG, "Host deinit failed: %s", esp_err_to_name(ret));
}
#if CONFIG_BT_CONTROLLER_ENABLED || !CONFIG_BT_NIMBLE_ENABLED
ret = _controller_deinit();
if (ret) {
ESP_LOGE(BLUFI_TAG, "Controller deinit failed: %s", esp_err_to_name(ret));
}
#endif
return ret;
}
#ifdef CONFIG_BT_BLUEDROID_ENABLED
esp_err_t Blufi::_host_init() {
esp_err_t ret = esp_bluedroid_init();
if (ret) {
ESP_LOGE(BLUFI_TAG, "%s init bluedroid failed: %s", __func__, esp_err_to_name(ret));
return ESP_FAIL;
}
ret = esp_bluedroid_enable();
if (ret) {
ESP_LOGE(BLUFI_TAG, "%s enable bluedroid failed: %s", __func__, esp_err_to_name(ret));
return ESP_FAIL;
}
ESP_LOGI(BLUFI_TAG, "BD ADDR: " ESP_BD_ADDR_STR, ESP_BD_ADDR_HEX(esp_bt_dev_get_address()));
return ESP_OK;
}
esp_err_t Blufi::_host_deinit() {
esp_err_t ret = esp_blufi_profile_deinit();
if (ret != ESP_OK) return ret;
ret = esp_bluedroid_disable();
if (ret) {
ESP_LOGE(BLUFI_TAG, "%s disable bluedroid failed: %s", __func__, esp_err_to_name(ret));
return ESP_FAIL;
}
ret = esp_bluedroid_deinit();
if (ret) {
ESP_LOGE(BLUFI_TAG, "%s deinit bluedroid failed: %s", __func__, esp_err_to_name(ret));
return ESP_FAIL;
}
return ESP_OK;
}
esp_err_t Blufi::_gap_register_callback() {
esp_err_t rc = esp_ble_gap_register_callback(esp_blufi_gap_event_handler);
if (rc) {
return rc;
}
return esp_blufi_profile_init();
}
esp_err_t Blufi::_host_and_cb_init() {
esp_blufi_callbacks_t blufi_callbacks = {
.event_cb = &_event_callback_trampoline,
.negotiate_data_handler = &_negotiate_data_handler_trampoline,
.encrypt_func = &_encrypt_func_trampoline,
.decrypt_func = &_decrypt_func_trampoline,
.checksum_func = &_checksum_func_trampoline,
};
esp_err_t ret = _host_init();
if (ret) {
ESP_LOGE(BLUFI_TAG, "%s initialise host failed: %s", __func__, esp_err_to_name(ret));
return ret;
}
ret = esp_blufi_register_callbacks(&blufi_callbacks);
if (ret) {
ESP_LOGE(BLUFI_TAG, "%s blufi register failed, error code = %x", __func__, ret);
return ret;
}
ret = _gap_register_callback();
if (ret) {
ESP_LOGE(BLUFI_TAG, "%s gap register failed, error code = %x", __func__, ret);
return ret;
}
return ESP_OK;
}
#endif /* CONFIG_BT_BLUEDROID_ENABLED */
#ifdef CONFIG_BT_NIMBLE_ENABLED
// Stubs for NimBLE specific store functionality
void ble_store_config_init();
void Blufi::_nimble_on_reset(int reason) {
ESP_LOGE(BLUFI_TAG, "NimBLE Resetting state; reason=%d", reason);
}
void Blufi::_nimble_on_sync() {
// This is called when the host and controller are synced.
// It's a good place to initialize the Blufi profile.
esp_blufi_profile_init();
}
void Blufi::_nimble_host_task(void *param) {
ESP_LOGI(BLUFI_TAG, "BLE Host Task Started");
nimble_port_run(); // This function will return only when nimble_port_stop() is executed
nimble_port_freertos_deinit();
}
esp_err_t Blufi::_host_init() {
// esp_nimble_init() is called by controller_init for NimBLE
ble_hs_cfg.reset_cb = _nimble_on_reset;
ble_hs_cfg.sync_cb = _nimble_on_sync;
ble_hs_cfg.gatts_register_cb = esp_blufi_gatt_svr_register_cb;
// Security Manager settings (can be customized)
ble_hs_cfg.sm_io_cap = 4; // IO capability: No Input, No Output
#ifdef CONFIG_EXAMPLE_BONDING
ble_hs_cfg.sm_bonding=1;
#endif
int rc = esp_blufi_gatt_svr_init();
assert (rc== 0);
ble_store_config_init(); // Configure the BLE storage
esp_blufi_btc_init();
esp_err_t err = esp_nimble_enable(_nimble_host_task);
if (err) {
ESP_LOGE(BLUFI_TAG, "%s failed: %s", __func__, esp_err_to_name(err));
return ESP_FAIL;
}
return ESP_OK;
}
esp_err_t Blufi::_host_deinit(void) {
esp_err_t ret = nimble_port_stop();
if (ret == ESP_OK) {
esp_nimble_deinit();
}
esp_blufi_gatt_svr_deinit();
ret = esp_blufi_profile_deinit();
esp_blufi_btc_deinit();
return ret;
}
esp_err_t Blufi::_gap_register_callback(void) {
return ESP_OK; // For NimBLE, GAP callbacks are handled differently
}
esp_err_t Blufi::_host_and_cb_init() {
esp_blufi_callbacks_t blufi_callbacks = {
.event_cb = &_event_callback_trampoline,
.negotiate_data_handler = &_negotiate_data_handler_trampoline,
.encrypt_func = &_encrypt_func_trampoline,
.decrypt_func = &_decrypt_func_trampoline,
.checksum_func = &_checksum_func_trampoline,
};
esp_err_t ret = esp_blufi_register_callbacks(&blufi_callbacks);
if (ret) {
ESP_LOGE(BLUFI_TAG, "%s blufi register failed, error code = %x", __func__, ret);
return ret;
}
// Host init must be called after registering callbacks for NimBLE
ret = _host_init();
if (ret) {
ESP_LOGE(BLUFI_TAG, "%s initialise host failed: %s", __func__, esp_err_to_name(ret));
return ret;
}
return ESP_OK;
}
#endif /* CONFIG_BT_NIMBLE_ENABLED */
#if CONFIG_BT_CONTROLLER_ENABLED || !CONFIG_BT_NIMBLE_ENABLED
esp_err_t Blufi::_controller_init() {
esp_bt_controller_config_t bt_cfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT();
esp_err_t ret = esp_bt_controller_init(&bt_cfg);
if (ret) {
ESP_LOGE(BLUFI_TAG, "%s initialize controller failed: %s", __func__, esp_err_to_name(ret));
return ret;
}
ret = esp_bt_controller_enable(ESP_BT_MODE_BLE);
if (ret) {
ESP_LOGE(BLUFI_TAG, "%s enable controller failed: %s", __func__, esp_err_to_name(ret));
return ret;
}
#ifdef CONFIG_BT_NIMBLE_ENABLED
// For NimBLE, host init needs to be done after controller init
ret = esp_nimble_init();
if (ret) {
ESP_LOGE(BLUFI_TAG, "esp_nimble_init() failed: %s", esp_err_to_name(ret));
return ret;
}
#endif
return ESP_OK;
}
esp_err_t Blufi::_controller_deinit() {
esp_err_t ret = esp_bt_controller_disable();
if (ret) {
ESP_LOGE(BLUFI_TAG, "%s disable controller failed: %s", __func__, esp_err_to_name(ret));
}
ret = esp_bt_controller_deinit();
if (ret) {
ESP_LOGE(BLUFI_TAG, "%s deinit controller failed: %s", __func__, esp_err_to_name(ret));
}
return ret;
}
#endif // Generic controller init
static int myrand(void *rng_state, unsigned char *output, size_t len) {
esp_fill_random(output, len);
return 0;
}
void Blufi::_security_init() {
m_sec = new BlufiSecurity();
if (m_sec == nullptr) {
ESP_LOGE(BLUFI_TAG, "Failed to allocate security context");
return;
}
memset(m_sec, 0, sizeof(BlufiSecurity));
m_sec->dhm = new mbedtls_dhm_context();
m_sec->aes = new mbedtls_aes_context();
mbedtls_dhm_init(m_sec->dhm);
mbedtls_aes_init(m_sec->aes);
memset(m_sec->iv, 0x0, sizeof(m_sec->iv));
}
void Blufi::_security_deinit() {
if (m_sec == nullptr) return;
if (m_sec->dh_param) {
free(m_sec->dh_param);
}
mbedtls_dhm_free(m_sec->dhm);
mbedtls_aes_free(m_sec->aes);
delete m_sec->dhm;
delete m_sec->aes;
delete m_sec;
m_sec = nullptr;
}
void Blufi::_dh_negotiate_data_handler(uint8_t *data, int len, uint8_t **output_data, int *output_len,
bool *need_free) {
if (m_sec == nullptr) {
ESP_LOGE(BLUFI_TAG, "Security not initialized in DH handler");
btc_blufi_report_error(ESP_BLUFI_INIT_SECURITY_ERROR);
return;
}
uint8_t type = data[0];
switch (type) {
case 0x00: /* DH_PARAM_LEN */
m_sec->dh_param_len = (data[1] << 8) | data[2];
if (m_sec->dh_param) {
free(m_sec->dh_param);
m_sec->dh_param = nullptr;
}
m_sec->dh_param = (uint8_t *) malloc(m_sec->dh_param_len);
if (m_sec->dh_param == nullptr) {
ESP_LOGE(BLUFI_TAG, "DH malloc failed");
btc_blufi_report_error(ESP_BLUFI_DH_MALLOC_ERROR);
}
break;
case 0x01: /* DH_PARAM_DATA */ {
if (m_sec->dh_param == nullptr) {
ESP_LOGE(BLUFI_TAG, "DH param not allocated");
btc_blufi_report_error(ESP_BLUFI_DH_PARAM_ERROR);
return;
}
uint8_t *param = m_sec->dh_param;
memcpy(m_sec->dh_param, &data[1], m_sec->dh_param_len);
int ret = mbedtls_dhm_read_params(m_sec->dhm, &param, &param[m_sec->dh_param_len]);
if (ret) {
ESP_LOGE(BLUFI_TAG, "mbedtls_dhm_read_params failed %d", ret);
btc_blufi_report_error(ESP_BLUFI_READ_PARAM_ERROR);
return;
}
const int dhm_len = mbedtls_dhm_get_len(m_sec->dhm);
ret = mbedtls_dhm_make_public(m_sec->dhm, dhm_len, m_sec->self_public_key, DH_SELF_PUB_KEY_LEN, myrand,
NULL);
if (ret) {
ESP_LOGE(BLUFI_TAG, "mbedtls_dhm_make_public failed %d", ret);
btc_blufi_report_error(ESP_BLUFI_MAKE_PUBLIC_ERROR);
return;
}
ret = mbedtls_dhm_calc_secret(m_sec->dhm, m_sec->share_key, SHARE_KEY_LEN, &m_sec->share_len, myrand, NULL);
if (ret) {
ESP_LOGE(BLUFI_TAG, "mbedtls_dhm_calc_secret failed %d", ret);
btc_blufi_report_error(ESP_BLUFI_ENCRYPT_ERROR);
return;
}
ret = mbedtls_md5(m_sec->share_key, m_sec->share_len, m_sec->psk);
if (ret) {
ESP_LOGE(BLUFI_TAG, "mbedtls_md5 failed %d", ret);
btc_blufi_report_error(ESP_BLUFI_CALC_MD5_ERROR);
return;
}
mbedtls_aes_setkey_enc(m_sec->aes, m_sec->psk, PSK_LEN * 8);
*output_data = &m_sec->self_public_key[0];
*output_len = dhm_len;
*need_free = false;
free(m_sec->dh_param);
m_sec->dh_param = NULL;
}
break;
default:
ESP_LOGE(BLUFI_TAG, "DH handler unknown type: %d", type);
}
}
int Blufi::_aes_encrypt(uint8_t iv8, uint8_t *crypt_data, int crypt_len) {
if (!m_sec) return -1;
size_t iv_offset = 0;
uint8_t iv0[16];
memcpy(iv0, m_sec->iv, 16);
iv0[0] = iv8;
return mbedtls_aes_crypt_cfb128(m_sec->aes, MBEDTLS_AES_ENCRYPT, crypt_len, &iv_offset, iv0, crypt_data,
crypt_data);
}
int Blufi::_aes_decrypt(uint8_t iv8, uint8_t *crypt_data, int crypt_len) {
if (!m_sec) return -1;
size_t iv_offset = 0;
uint8_t iv0[16];
memcpy(iv0, m_sec->iv, 16);
iv0[0] = iv8;
return mbedtls_aes_crypt_cfb128(m_sec->aes, MBEDTLS_AES_DECRYPT, crypt_len, &iv_offset, iv0, crypt_data,
crypt_data);
}
uint16_t Blufi::_crc_checksum(uint8_t iv8, uint8_t *data, int len) {
return esp_crc16_be(0, data, len);
}
int Blufi::_get_softap_conn_num() {
auto &wifi = WifiManager::GetInstance();
if (!wifi.IsInitialized() || !wifi.IsConfigMode()) {
return 0;
}
wifi_sta_list_t sta_list{};
if (esp_wifi_ap_get_sta_list(&sta_list) == ESP_OK) {
return sta_list.num;
}
return 0;
}
void Blufi::_handle_event(esp_blufi_cb_event_t event, esp_blufi_cb_param_t *param) {
switch (event) {
case ESP_BLUFI_EVENT_INIT_FINISH:
ESP_LOGI(BLUFI_TAG, "BLUFI init finish");
esp_blufi_adv_start();
break;
case ESP_BLUFI_EVENT_BLE_CONNECT:
ESP_LOGI(BLUFI_TAG, "BLUFI ble connect");
m_ble_is_connected = true;
esp_blufi_adv_stop();
_security_init();
break;
case ESP_BLUFI_EVENT_BLE_DISCONNECT:
ESP_LOGI(BLUFI_TAG, "BLUFI ble disconnect");
m_ble_is_connected = false;
_security_deinit();
if (!m_provisioned) {
esp_blufi_adv_start();
} else {
esp_blufi_adv_stop();
if (!m_deinited) {
// Deinit BLE stack after provisioning completes to free resources.
xTaskCreate([](void *ctx) {
static_cast<Blufi *>(ctx)->deinit();
vTaskDelete(nullptr);
}, "blufi_deinit", 4096, this, 5, nullptr);
}
}
break;
case ESP_BLUFI_EVENT_SET_WIFI_OPMODE: {
ESP_LOGI(BLUFI_TAG, "BLUFI Set WIFI opmode %d", param->wifi_mode.op_mode);
auto &wifi_manager = WifiManager::GetInstance();
if (!wifi_manager.IsInitialized() && !wifi_manager.Initialize()) {
ESP_LOGE(BLUFI_TAG, "Failed to initialize WifiManager for opmode change");
break;
}
switch (param->wifi_mode.op_mode) {
case WIFI_MODE_STA:
wifi_manager.StartStation();
break;
case WIFI_MODE_AP:
wifi_manager.StartConfigAp();
break;
case WIFI_MODE_APSTA:
ESP_LOGW(BLUFI_TAG, "APSTA mode not supported, starting station only");
wifi_manager.StartStation();
break;
default:
wifi_manager.StopStation();
wifi_manager.StopConfigAp();
break;
}
break;
}
case ESP_BLUFI_EVENT_REQ_CONNECT_TO_AP: {
ESP_LOGI(BLUFI_TAG, "BLUFI request wifi connect to AP via esp-wifi-connect");
std::string ssid(reinterpret_cast<const char *>(m_sta_config.sta.ssid));
std::string password(reinterpret_cast<const char *>(m_sta_config.sta.password));
// Save credentials through SsidManager
SsidManager::GetInstance().AddSsid(ssid, password);
auto &wifi_manager = WifiManager::GetInstance();
if (!wifi_manager.IsInitialized() && !wifi_manager.Initialize()) {
ESP_LOGE(BLUFI_TAG, "Failed to initialize WifiManager");
break;
}
// Track SSID for BLUFI status reporting.
m_sta_ssid_len = static_cast<int>(std::min(ssid.size(), sizeof(m_sta_ssid)));
memcpy(m_sta_ssid, ssid.c_str(), m_sta_ssid_len);
memset(m_sta_bssid, 0, sizeof(m_sta_bssid));
m_sta_connected = false;
m_sta_got_ip = false;
m_sta_is_connecting = true;
m_sta_conn_info = {}; // Reset connection info
m_sta_conn_info.sta_ssid = m_sta_ssid;
m_sta_conn_info.sta_ssid_len = m_sta_ssid_len;
wifi_manager.StartStation();
// Wait for connection in a separate task to avoid blocking the BLUFI handler.
xTaskCreate([](void *ctx) {
auto *self = static_cast<Blufi *>(ctx);
auto &wifi = WifiManager::GetInstance();
constexpr int kConnectTimeoutMs = 10000; // 10s
constexpr TickType_t kDelayTick = pdMS_TO_TICKS(200);
int waited_ms = 0;
while (waited_ms < kConnectTimeoutMs && !wifi.IsConnected()) {
vTaskDelay(kDelayTick);
waited_ms += 200;
}
wifi_mode_t mode = GetWifiModeWithFallback(wifi);
const int softap_conn_num = _get_softap_conn_num();
if (wifi.IsConnected()) {
self->m_sta_is_connecting = false;
self->m_sta_connected = true;
self->m_sta_got_ip = true;
self->m_provisioned = true;
auto current_ssid = wifi.GetSsid();
if (!current_ssid.empty()) {
self->m_sta_ssid_len = static_cast<int>(
std::min(current_ssid.size(), sizeof(self->m_sta_ssid)));
memcpy(self->m_sta_ssid, current_ssid.c_str(), self->m_sta_ssid_len);
}
wifi_ap_record_t ap_info{};
if (esp_wifi_sta_get_ap_info(&ap_info) == ESP_OK) {
memcpy(self->m_sta_bssid, ap_info.bssid, sizeof(self->m_sta_bssid));
}
esp_blufi_extra_info_t info = {};
memcpy(info.sta_bssid, self->m_sta_bssid, sizeof(self->m_sta_bssid));
info.sta_bssid_set = true;
info.sta_ssid = self->m_sta_ssid;
info.sta_ssid_len = self->m_sta_ssid_len;
esp_blufi_send_wifi_conn_report(mode, ESP_BLUFI_STA_CONN_SUCCESS, softap_conn_num, &info);
ESP_LOGI(BLUFI_TAG, "connected to WiFi");
// Close BluFi session after successful provisioning to free resources.
if (self->m_ble_is_connected) {
esp_blufi_disconnect();
}
} else {
self->m_sta_is_connecting = false;
self->m_sta_connected = false;
self->m_sta_got_ip = false;
esp_blufi_extra_info_t info = {};
info.sta_ssid = self->m_sta_ssid;
info.sta_ssid_len = self->m_sta_ssid_len;
esp_blufi_send_wifi_conn_report(mode, ESP_BLUFI_STA_CONN_FAIL, softap_conn_num, &info);
ESP_LOGE(BLUFI_TAG, "Failed to connect to WiFi via esp-wifi-connect");
}
vTaskDelete(nullptr);
}, "blufi_wifi_conn", 4096, this, 5, nullptr);
break;
}
case ESP_BLUFI_EVENT_REQ_DISCONNECT_FROM_AP:
ESP_LOGI(BLUFI_TAG, "BLUFI request wifi disconnect from AP");
if (WifiManager::GetInstance().IsInitialized()) {
WifiManager::GetInstance().StopStation();
}
m_sta_is_connecting = false;
m_sta_connected = false;
m_sta_got_ip = false;
break;
case ESP_BLUFI_EVENT_GET_WIFI_STATUS: {
auto &wifi = WifiManager::GetInstance();
wifi_mode_t mode = GetWifiModeWithFallback(wifi);
const int softap_conn_num = _get_softap_conn_num();
if (wifi.IsInitialized() && wifi.IsConnected()) {
m_sta_connected = true;
m_sta_got_ip = true;
auto current_ssid = wifi.GetSsid();
if (!current_ssid.empty()) {
m_sta_ssid_len = static_cast<int>(std::min(current_ssid.size(), sizeof(m_sta_ssid)));
memcpy(m_sta_ssid, current_ssid.c_str(), m_sta_ssid_len);
}
esp_blufi_extra_info_t info;
memset(&info, 0, sizeof(esp_blufi_extra_info_t));
memcpy(info.sta_bssid, m_sta_bssid, 6);
info.sta_ssid = m_sta_ssid;
info.sta_ssid_len = m_sta_ssid_len;
esp_blufi_send_wifi_conn_report(mode, ESP_BLUFI_STA_CONN_SUCCESS, softap_conn_num, &info);
} else if (m_sta_is_connecting) {
esp_blufi_send_wifi_conn_report(mode, ESP_BLUFI_STA_CONNECTING, softap_conn_num, &m_sta_conn_info);
} else {
esp_blufi_send_wifi_conn_report(mode, ESP_BLUFI_STA_CONN_FAIL, softap_conn_num, &m_sta_conn_info);
}
ESP_LOGI(BLUFI_TAG, "BLUFI get wifi status");
break;
}
case ESP_BLUFI_EVENT_RECV_STA_BSSID:
memcpy(m_sta_config.sta.bssid, param->sta_bssid.bssid, 6);
m_sta_config.sta.bssid_set = true;
ESP_LOGI(BLUFI_TAG, "Recv STA BSSID");
break;
case ESP_BLUFI_EVENT_RECV_STA_SSID:
strncpy((char *) m_sta_config.sta.ssid, (char *) param->sta_ssid.ssid, param->sta_ssid.ssid_len);
m_sta_config.sta.ssid[param->sta_ssid.ssid_len] = '\0';
ESP_LOGI(BLUFI_TAG, "Recv STA SSID: %s", m_sta_config.sta.ssid);
break;
case ESP_BLUFI_EVENT_RECV_STA_PASSWD:
strncpy((char *) m_sta_config.sta.password, (char *) param->sta_passwd.passwd,
param->sta_passwd.passwd_len);
m_sta_config.sta.password[param->sta_passwd.passwd_len] = '\0';
ESP_LOGI(BLUFI_TAG, "Recv STA PASSWORD");
break;
default:
ESP_LOGW(BLUFI_TAG, "Unhandled event: %d", event);
break;
}
}
void Blufi::_event_callback_trampoline(esp_blufi_cb_event_t event, esp_blufi_cb_param_t *param) {
GetInstance()._handle_event(event, param);
}
void Blufi::_negotiate_data_handler_trampoline(uint8_t *data, int len, uint8_t **output_data, int *output_len,
bool *need_free) {
GetInstance()._dh_negotiate_data_handler(data, len, output_data, output_len, need_free);
}
int Blufi::_encrypt_func_trampoline(uint8_t iv8, uint8_t *crypt_data, int crypt_len) {
return GetInstance()._aes_encrypt(iv8, crypt_data, crypt_len);
}
int Blufi::_decrypt_func_trampoline(uint8_t iv8, uint8_t *crypt_data, int crypt_len) {
return GetInstance()._aes_decrypt(iv8, crypt_data, crypt_len);
}
uint16_t Blufi::_checksum_func_trampoline(uint8_t iv8, uint8_t *data, int len) {
return _crc_checksum(iv8, data, len);
}

121
main/boards/common/blufi.h Normal file
View File

@ -0,0 +1,121 @@
#pragma once
#include <aes/esp_aes.h>
#include "mbedtls/dhm.h"
#include "mbedtls/aes.h"
#include "esp_err.h"
#include "esp_blufi_api.h"
#include "esp_wifi_types.h"
class Blufi {
public:
/**
* @brief Get the singleton instance of the Blufi class.
*/
static Blufi &GetInstance();
/**
* @brief Initializes the Bluetooth controller, host, and Blufi profile.
* This is the main entry point to start the Blufi process.
* @return ESP_OK on success, otherwise an error code.
*/
esp_err_t init();
/**
* @brief Deinitializes Blufi and the Bluetooth stack.
* @return ESP_OK on success, otherwise an error code.
*/
esp_err_t deinit();
// Delete copy constructor and assignment operator for singleton
Blufi(const Blufi &) = delete;
Blufi &operator=(const Blufi &) = delete;
private:
Blufi();
~Blufi();
// Initialization logic
static esp_err_t _controller_init();
static esp_err_t _controller_deinit();
static esp_err_t _host_init();
static esp_err_t _host_deinit();
static esp_err_t _gap_register_callback();
static esp_err_t _host_and_cb_init();
void _security_init();
void _security_deinit();
void _dh_negotiate_data_handler(uint8_t *data, int len, uint8_t **output_data, int *output_len, bool *need_free);
int _aes_encrypt(uint8_t iv8, uint8_t *crypt_data, int crypt_len);
int _aes_decrypt(uint8_t iv8, uint8_t *crypt_data, int crypt_len);
static uint16_t _crc_checksum(uint8_t iv8, uint8_t *data, int len);
void _handle_event(esp_blufi_cb_event_t event, esp_blufi_cb_param_t *param);
static int _get_softap_conn_num();
// These C-style functions are registered with ESP-IDF and call the corresponding instance methods.
static void _event_callback_trampoline(esp_blufi_cb_event_t event, esp_blufi_cb_param_t *param);
static void _negotiate_data_handler_trampoline(uint8_t *data, int len, uint8_t **output_data, int *output_len,
bool *need_free);
static int _encrypt_func_trampoline(uint8_t iv8, uint8_t *crypt_data, int crypt_len);
static int _decrypt_func_trampoline(uint8_t iv8, uint8_t *crypt_data, int crypt_len);
static uint16_t _checksum_func_trampoline(uint8_t iv8, uint8_t *data, int len);
#ifdef CONFIG_BT_NIMBLE_ENABLED
static void _nimble_on_reset(int reason);
static void _nimble_on_sync();
static void _nimble_host_task(void *param);
#endif
// Security context, formerly blufi_sec struct
struct BlufiSecurity {
#define DH_SELF_PUB_KEY_LEN 128
uint8_t self_public_key[DH_SELF_PUB_KEY_LEN];
#define SHARE_KEY_LEN 128
uint8_t share_key[SHARE_KEY_LEN];
size_t share_len;
#define PSK_LEN 16
uint8_t psk[PSK_LEN];
uint8_t *dh_param;
int dh_param_len;
uint8_t iv[16];
mbedtls_dhm_context *dhm;
esp_aes_context *aes;
};
BlufiSecurity *m_sec;
// State variables
wifi_config_t m_sta_config{};
wifi_config_t m_ap_config{};
bool m_ble_is_connected;
bool m_sta_connected;
bool m_sta_got_ip;
bool m_provisioned;
bool m_deinited;
uint8_t m_sta_bssid[6]{};
uint8_t m_sta_ssid[32]{};
int m_sta_ssid_len;
bool m_sta_is_connecting;
esp_blufi_extra_info_t m_sta_conn_info{};
};

View File

@ -17,6 +17,9 @@
#include <wifi_station.h>
#include <ssid_manager.h>
#include "afsk_demod.h"
#ifdef CONFIG_USE_ESP_BLUFI_WIFI_PROVISIONING
#include "blufi.h"
#endif
static const char *TAG = "WifiBoard";
@ -48,13 +51,13 @@ std::string WifiBoard::GetBoardType() {
void WifiBoard::StartNetwork() {
auto& wifi_manager = WifiManager::GetInstance();
// Initialize WiFi manager
WifiManagerConfig config;
config.ssid_prefix = "Xiaozhi";
config.language = Lang::CODE;
wifi_manager.Initialize(config);
// Set unified event callback - forward to NetworkEvent with SSID data
wifi_manager.SetEventCallback([this, &wifi_manager](WifiEvent event) {
std::string ssid = wifi_manager.GetSsid();
@ -79,7 +82,7 @@ void WifiBoard::StartNetwork() {
break;
}
});
// Try to connect or enter config mode
TryWifiConnect();
}
@ -87,7 +90,7 @@ void WifiBoard::StartNetwork() {
void WifiBoard::TryWifiConnect() {
auto& ssid_manager = SsidManager::GetInstance();
bool have_ssid = !ssid_manager.GetSsidList().empty();
if (have_ssid) {
// Start connection attempt with timeout
ESP_LOGI(TAG, "Starting WiFi connection attempt");
@ -103,18 +106,22 @@ void WifiBoard::TryWifiConnect() {
void WifiBoard::OnNetworkEvent(NetworkEvent event, const std::string& data) {
switch (event) {
case NetworkEvent::Connected:
// Stop timeout timer
esp_timer_stop(connect_timer_);
#ifdef CONFIG_USE_ESP_BLUFI_WIFI_PROVISIONING
// make sure blufi resources has been released
Blufi::GetInstance().deinit();
#endif
in_config_mode_ = false;
ESP_LOGI(TAG, "Connected to WiFi: %s", data.c_str());
break;
case NetworkEvent::Scanning:
ESP_LOGI(TAG, "WiFi scanning");
break;
case NetworkEvent::Connecting:
ESP_LOGI(TAG, "WiFi connecting to %s", data.c_str());
break;
case NetworkEvent::Connected:
// Stop timeout timer
esp_timer_stop(connect_timer_);
in_config_mode_ = false;
ESP_LOGI(TAG, "Connected to WiFi: %s", data.c_str());
break;
case NetworkEvent::Disconnected:
ESP_LOGW(TAG, "WiFi disconnected");
break;
@ -130,9 +137,8 @@ void WifiBoard::OnNetworkEvent(NetworkEvent event, const std::string& data) {
break;
default:
break;
}
// Notify external callback if set
if (network_event_callback_) {
network_event_callback_(event, data);
@ -146,36 +152,41 @@ void WifiBoard::SetNetworkEventCallback(NetworkEventCallback callback) {
void WifiBoard::OnWifiConnectTimeout(void* arg) {
auto* board = static_cast<WifiBoard*>(arg);
ESP_LOGW(TAG, "WiFi connection timeout, entering config mode");
WifiManager::GetInstance().StopStation();
board->StartWifiConfigMode();
}
void WifiBoard::StartWifiConfigMode() {
in_config_mode_ = true;
auto& wifi_manager = WifiManager::GetInstance();
// Transition to wifi configuring state
Application::GetInstance().SetDeviceState(kDeviceStateWifiConfiguring);
#ifdef CONFIG_USE_HOTSPOT_WIFI_PROVISIONING
auto& wifi_manager = WifiManager::GetInstance();
wifi_manager.StartConfigAp();
// Show config prompt after a short delay
Application::GetInstance().Schedule([this, &wifi_manager]() {
Application::GetInstance().Schedule([&wifi_manager]() {
std::string hint = Lang::Strings::CONNECT_TO_HOTSPOT;
hint += wifi_manager.GetApSsid();
hint += Lang::Strings::ACCESS_VIA_BROWSER;
hint += wifi_manager.GetApWebUrl();
Application::GetInstance().Alert(Lang::Strings::WIFI_CONFIG_MODE, hint.c_str(), "gear", Lang::Sounds::OGG_WIFICONFIG);
});
#endif
#if CONFIG_USE_ESP_BLUFI_WIFI_PROVISIONING
auto &blufi = Blufi::GetInstance();
// initialize esp-blufi protocol
blufi.init();
#endif
#if CONFIG_USE_ACOUSTIC_WIFI_PROVISIONING
// Start acoustic provisioning task
auto codec = Board::GetInstance().GetAudioCodec();
int channel = codec ? codec->input_channels() : 1;
ESP_LOGI(TAG, "Starting acoustic WiFi provisioning, channels: %d", channel);
xTaskCreate([](void* arg) {
auto ch = reinterpret_cast<intptr_t>(arg);
auto& app = Application::GetInstance();
@ -190,32 +201,32 @@ void WifiBoard::StartWifiConfigMode() {
void WifiBoard::EnterWifiConfigMode() {
ESP_LOGI(TAG, "EnterWifiConfigMode called");
GetDisplay()->ShowNotification(Lang::Strings::ENTERING_WIFI_CONFIG_MODE);
auto& app = Application::GetInstance();
auto state = app.GetDeviceState();
if (state == kDeviceStateSpeaking || state == kDeviceStateListening || state == kDeviceStateIdle) {
// Reset protocol (close audio channel, reset protocol)
Application::GetInstance().ResetProtocol();
xTaskCreate([](void* arg) {
auto* board = static_cast<WifiBoard*>(arg);
// Wait for 1 second to allow speaking to finish gracefully
vTaskDelay(pdMS_TO_TICKS(1000));
// Stop any ongoing connection attempt
esp_timer_stop(board->connect_timer_);
WifiManager::GetInstance().StopStation();
// Enter config mode
board->StartWifiConfigMode();
vTaskDelete(NULL);
}, "wifi_cfg_delay", 4096, this, 2, NULL);
return;
}
if (state != kDeviceStateStarting) {
ESP_LOGE(TAG, "EnterWifiConfigMode called but device state is not starting or speaking, device state: %d", state);
return;
@ -224,7 +235,7 @@ void WifiBoard::EnterWifiConfigMode() {
// Stop any ongoing connection attempt
esp_timer_stop(connect_timer_);
WifiManager::GetInstance().StopStation();
StartWifiConfigMode();
}
@ -239,14 +250,14 @@ NetworkInterface* WifiBoard::GetNetwork() {
const char* WifiBoard::GetNetworkStateIcon() {
auto& wifi = WifiManager::GetInstance();
if (wifi.IsConfigMode()) {
return FONT_AWESOME_WIFI;
}
if (!wifi.IsConnected()) {
return FONT_AWESOME_WIFI_SLASH;
}
int rssi = wifi.GetRssi();
if (rssi >= -60) {
return FONT_AWESOME_WIFI;
@ -260,14 +271,14 @@ std::string WifiBoard::GetBoardJson() {
auto& wifi = WifiManager::GetInstance();
std::string json = R"({"type":")" + std::string(BOARD_TYPE) + R"(",)";
json += R"("name":")" + std::string(BOARD_NAME) + R"(",)";
if (!wifi.IsConfigMode()) {
json += R"("ssid":")" + wifi.GetSsid() + R"(",)";
json += R"("rssi":)" + std::to_string(wifi.GetRssi()) + R"(,)";
json += R"("channel":)" + std::to_string(wifi.GetChannel()) + R"(,)";
json += R"("ip":")" + wifi.GetIpAddress() + R"(",)";
}
json += R"("mac":")" + SystemInfo::GetMacAddress() + R"("})";
return json;
}

View File

@ -113,6 +113,48 @@ def _find_board_config(board_type: str) -> Optional[str]:
return config
return None
# Kconfig "select" entries are not automatically applied when we simply append
# sdkconfig lines from config.json, so add the required dependencies here to
# mimic menuconfig behaviour.
_AUTO_SELECT_RULES: dict[str, list[str]] = {
"CONFIG_USE_ESP_BLUFI_WIFI_PROVISIONING": [
"CONFIG_BT_ENABLED=y",
"CONFIG_BT_BLUEDROID_ENABLED=y",
"CONFIG_BT_BLE_42_FEATURES_SUPPORTED=y",
"CONFIG_BT_BLE_50_FEATURES_SUPPORTED=n",
"CONFIG_BT_BLE_BLUFI_ENABLE=y",
"CONFIG_MBEDTLS_DHM_C=y",
],
}
def _apply_auto_selects(sdkconfig_append: list[str]) -> list[str]:
"""Apply hardcoded auto-select rules to sdkconfig_append."""
items: list[str] = []
existing_keys: set[str] = set()
def _append_if_missing(entry: str) -> None:
key = entry.split("=", 1)[0]
if key not in existing_keys:
items.append(entry)
existing_keys.add(key)
# Preserve original order while tracking keys
for entry in sdkconfig_append:
_append_if_missing(entry)
# Apply auto-select rules
for key, deps in _AUTO_SELECT_RULES.items():
for entry in sdkconfig_append:
name, _, value = entry.partition("=")
if name == key and value.lower().startswith("y"):
for dep in deps:
_append_if_missing(dep)
break
return items
################################################################################
# Check board_type in CMakeLists
################################################################################
@ -167,6 +209,7 @@ def release(board_type: str, config_filename: str = "config.json", *, filter_nam
board_type_config = _find_board_config(board_type)
sdkconfig_append = [f"{board_type_config}=y"]
sdkconfig_append.extend(build.get("sdkconfig_append", []))
sdkconfig_append = _apply_auto_selects(sdkconfig_append)
print("-" * 80)
print(f"name: {name}")