From a8b62c75a8604c901787a4995353981859372930 Mon Sep 17 00:00:00 2001 From: WhereAreBugs Date: Sun, 7 Dec 2025 22:01:22 +0800 Subject: [PATCH] =?UTF-8?q?feat:=E6=96=B0=E5=A2=9Eblufi=E9=85=8D=E7=BD=91?= =?UTF-8?q?=E5=8D=8F=E8=AE=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: WhereAreBugs --- docs/blufi.md | 36 ++ main/CMakeLists.txt | 4 + main/Kconfig.projbuild | 31 +- main/boards/common/blufi.cpp | 721 +++++++++++++++++++++++++++++++ main/boards/common/blufi.h | 121 ++++++ main/boards/common/wifi_board.cc | 75 ++-- scripts/release.py | 43 ++ 7 files changed, 992 insertions(+), 39 deletions(-) create mode 100644 docs/blufi.md create mode 100644 main/boards/common/blufi.cpp create mode 100644 main/boards/common/blufi.h diff --git a/docs/blufi.md b/docs/blufi.md new file mode 100644 index 0000000000..d62aeebe7e --- /dev/null +++ b/docs/blufi.md @@ -0,0 +1,36 @@ +# BluFi 配网(集成 esp-wifi-connect) + +本文档说明如何在小智固件中启用和使用 BluFi(BLE Wi‑Fi 配网),并结合项目内置的 `esp-wifi-connect` 组件完成 Wi‑Fi 连接与存储。官方 +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 或自研客户端)连接设备,发送 Wi‑Fi SSID/密码。 +2) 设备侧在 `ESP_BLUFI_EVENT_REQ_CONNECT_TO_AP` 中将凭据写入 `SsidManager`(存储到 NVS,属于 `esp-wifi-connect` 组件)。 +3) 随后启动 `WifiStation` 扫描并连接;状态通过 BluFi 返回。 +4) 连接成功后当前固件会延时 1 秒并重启,使主应用在下次启动时直接使用新 Wi‑Fi;失败则返回失败状态。 + +## 使用步骤 + +1. 配置:在 menuconfig 开启 `Esp Blufi`。编译并烧录固件。 +2. 触发配网:设备首次启动且没有已保存的 Wi‑Fi 时会自动进入配网。 +3. 手机端操作:打开 EspBlufi App(或其他 BluFi 客户端),搜索并连接设备,按提示输入 Wi‑Fi SSID/密码并发送。 +4. 观察结果: + - 成功:BluFi 报告连接成功,设备自动使用新的 Wi‑Fi。 + - 失败:BluFi 返回失败状态,可重新发送或检查路由器。 + +## 注意事项 + +- BluFi 与 Hotspot/声波配网可以同时编译,但会同时启动,增加内存占用。建议在 menuconfig 中只保留一种方式。 +- 若多次测试,建议清除或覆盖存储的 SSID(`wifi` 命名空间),避免旧配置干扰。 +- 如果使用自定义 BluFi 客户端,需遵循官方协议帧格式,参考上文官方文档链接。 +- 官方文档中已提供EspBlufi APP下载地址 diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index 68d6004e48..df3e130bac 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -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") diff --git a/main/Kconfig.projbuild b/main/Kconfig.projbuild index 8c18ba575b..ace7e8f777 100644 --- a/main/Kconfig.projbuild +++ b/main/Kconfig.projbuild @@ -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 diff --git a/main/boards/common/blufi.cpp b/main/boards/common/blufi.cpp new file mode 100644 index 0000000000..bf4ab6f3f7 --- /dev/null +++ b/main/boards/common/blufi.cpp @@ -0,0 +1,721 @@ +#include "blufi.h" +#include +#include +#include +#include + +#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 + +// 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, ¶m, ¶m[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(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(m_sta_config.sta.ssid)); + std::string password(reinterpret_cast(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(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(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( + 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(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); +} diff --git a/main/boards/common/blufi.h b/main/boards/common/blufi.h new file mode 100644 index 0000000000..606769b5f4 --- /dev/null +++ b/main/boards/common/blufi.h @@ -0,0 +1,121 @@ +#pragma once + +#include +#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{}; +}; diff --git a/main/boards/common/wifi_board.cc b/main/boards/common/wifi_board.cc index 9edbcdcd4e..062362e587 100644 --- a/main/boards/common/wifi_board.cc +++ b/main/boards/common/wifi_board.cc @@ -17,6 +17,9 @@ #include #include #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::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_); +#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::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(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(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(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; } diff --git a/scripts/release.py b/scripts/release.py index d1a34475d1..35e99140ea 100755 --- a/scripts/release.py +++ b/scripts/release.py @@ -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}")