mirror of
https://github.com/78/xiaozhi-esp32.git
synced 2026-01-14 09:17:20 +08:00
Merge branch '78:main' into main
This commit is contained in:
commit
976dba60fe
36
docs/blufi.md
Normal file
36
docs/blufi.md
Normal file
@ -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下载地址
|
||||
@ -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")
|
||||
|
||||
@ -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
|
||||
|
||||
@ -780,9 +780,10 @@ void Application::HandleWakeWordDetectedEvent() {
|
||||
protocol_->SendWakeWordDetected(wake_word);
|
||||
SetListeningMode(aec_mode_ == kAecOff ? kListeningModeAutoStop : kListeningModeRealtime);
|
||||
#else
|
||||
// Set flag to play popup sound after state changes to listening
|
||||
// (PlaySound here would be cleared by ResetDecoder in EnableVoiceProcessing)
|
||||
play_popup_on_listening_ = true;
|
||||
SetListeningMode(aec_mode_ == kAecOff ? kListeningModeAutoStop : kListeningModeRealtime);
|
||||
// Play the pop up sound to indicate the wake word is detected
|
||||
audio_service_.PlaySound(Lang::Sounds::OGG_POPUP);
|
||||
#endif
|
||||
} else if (state == kDeviceStateSpeaking) {
|
||||
AbortSpeaking(kAbortReasonWakeWordDetected);
|
||||
@ -825,6 +826,12 @@ void Application::HandleStateChangedEvent() {
|
||||
audio_service_.EnableVoiceProcessing(true);
|
||||
audio_service_.EnableWakeWordDetection(false);
|
||||
}
|
||||
|
||||
// Play popup sound after ResetDecoder (in EnableVoiceProcessing) has been called
|
||||
if (play_popup_on_listening_) {
|
||||
play_popup_on_listening_ = false;
|
||||
audio_service_.PlaySound(Lang::Sounds::OGG_POPUP);
|
||||
}
|
||||
break;
|
||||
case kDeviceStateSpeaking:
|
||||
display->SetStatus(Lang::Strings::SPEAKING);
|
||||
@ -846,7 +853,7 @@ void Application::HandleStateChangedEvent() {
|
||||
}
|
||||
}
|
||||
|
||||
void Application::Schedule(std::function<void()> callback) {
|
||||
void Application::Schedule(std::function<void()>&& callback) {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
main_tasks_.push_back(std::move(callback));
|
||||
@ -960,9 +967,10 @@ void Application::WakeWordInvoke(const std::string& wake_word) {
|
||||
protocol_->SendWakeWordDetected(wake_word);
|
||||
SetListeningMode(aec_mode_ == kAecOff ? kListeningModeAutoStop : kListeningModeRealtime);
|
||||
#else
|
||||
// Set flag to play popup sound after state changes to listening
|
||||
// (PlaySound here would be cleared by ResetDecoder in EnableVoiceProcessing)
|
||||
play_popup_on_listening_ = true;
|
||||
SetListeningMode(aec_mode_ == kAecOff ? kListeningModeAutoStop : kListeningModeRealtime);
|
||||
// Play the pop up sound to indicate the wake word is detected
|
||||
audio_service_.PlaySound(Lang::Sounds::OGG_POPUP);
|
||||
#endif
|
||||
} else if (state == kDeviceStateSpeaking) {
|
||||
Schedule([this]() {
|
||||
|
||||
@ -75,7 +75,7 @@ public:
|
||||
/**
|
||||
* Schedule a callback to be executed in the main task
|
||||
*/
|
||||
void Schedule(std::function<void()> callback);
|
||||
void Schedule(std::function<void()>&& callback);
|
||||
|
||||
/**
|
||||
* Alert with status, message, emotion and optional sound
|
||||
@ -139,6 +139,7 @@ private:
|
||||
bool has_server_time_ = false;
|
||||
bool aborted_ = false;
|
||||
bool assets_version_checked_ = false;
|
||||
bool play_popup_on_listening_ = false; // Flag to play popup sound after state changes to listening
|
||||
int clock_ticks_ = 0;
|
||||
TaskHandle_t activation_task_handle_ = nullptr;
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
725
main/boards/common/blufi.cpp
Normal file
725
main/boards/common/blufi.cpp
Normal file
@ -0,0 +1,725 @@
|
||||
#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;
|
||||
inited_ = true;
|
||||
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() {
|
||||
esp_err_t ret = ESP_OK;
|
||||
|
||||
if (inited_) {
|
||||
if (m_deinited) {
|
||||
return ESP_OK;
|
||||
}
|
||||
m_deinited = true;
|
||||
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<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);
|
||||
}
|
||||
123
main/boards/common/blufi.h
Normal file
123
main/boards/common/blufi.h
Normal file
@ -0,0 +1,123 @@
|
||||
#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:
|
||||
bool inited_ = false;
|
||||
|
||||
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{};
|
||||
};
|
||||
@ -155,13 +155,13 @@ const char* Ml307Board::GetNetworkStateIcon() {
|
||||
int csq = modem_->GetCsq();
|
||||
if (csq == -1) {
|
||||
return FONT_AWESOME_SIGNAL_OFF;
|
||||
} else if (csq >= 0 && csq <= 14) {
|
||||
} else if (csq >= 0 && csq <= 9) {
|
||||
return FONT_AWESOME_SIGNAL_WEAK;
|
||||
} else if (csq >= 15 && csq <= 19) {
|
||||
} else if (csq >= 10 && csq <= 14) {
|
||||
return FONT_AWESOME_SIGNAL_FAIR;
|
||||
} else if (csq >= 20 && csq <= 24) {
|
||||
} else if (csq >= 15 && csq <= 19) {
|
||||
return FONT_AWESOME_SIGNAL_GOOD;
|
||||
} else if (csq >= 25 && csq <= 31) {
|
||||
} else if (csq >= 20 && csq <= 31) {
|
||||
return FONT_AWESOME_SIGNAL_STRONG;
|
||||
}
|
||||
|
||||
|
||||
@ -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,18 +250,18 @@ 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) {
|
||||
if (rssi >= -65) {
|
||||
return FONT_AWESOME_WIFI;
|
||||
} else if (rssi >= -70) {
|
||||
} else if (rssi >= -75) {
|
||||
return FONT_AWESOME_WIFI_FAIR;
|
||||
}
|
||||
return FONT_AWESOME_WIFI_WEAK;
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -3,7 +3,15 @@
|
||||
"builds": [
|
||||
{
|
||||
"name": "esp32s3-korvo2-v3",
|
||||
"sdkconfig_append": []
|
||||
"sdkconfig_append": [
|
||||
"CONFIG_CAMERA_OV2640=y",
|
||||
"CONFIG_CAMERA_OV3660=y",
|
||||
"CONFIG_CAMERA_OV3660_AUTO_DETECT_DVP_INTERFACE_SENSOR=y",
|
||||
"CONFIG_CAMERA_OV3660_DVP_RGB565_240X240_24FPS=y",
|
||||
"CONFIG_CAMERA_OV2640_AUTO_DETECT_DVP_INTERFACE_SENSOR=y",
|
||||
"CONFIG_CAMERA_OV2640_DVP_RGB565_240X240_25FPS=y",
|
||||
"CONFIG_XIAOZHI_ENABLE_CAMERA_ENDIANNESS_SWAP=y"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -230,12 +230,12 @@ private:
|
||||
|
||||
auto set_button = adc_button_[BSP_ADC_BUTTON_SET];
|
||||
set_button->OnClick([this]() {
|
||||
ESP_LOGI(TAG, "TODO %s:%d\n", __func__, __LINE__);
|
||||
EnterWifiConfigMode();
|
||||
});
|
||||
|
||||
auto rec_button = adc_button_[BSP_ADC_BUTTON_REC];
|
||||
rec_button->OnClick([this]() {
|
||||
ESP_LOGI(TAG, "TODO %s:%d\n", __func__, __LINE__);
|
||||
Application::GetInstance().ToggleChatState();
|
||||
});
|
||||
boot_button_.OnClick([this]() {});
|
||||
boot_button_.OnClick([this]() {
|
||||
|
||||
@ -3,7 +3,15 @@
|
||||
"builds": [
|
||||
{
|
||||
"name": "kevin-sp-v4-dev",
|
||||
"sdkconfig_append": []
|
||||
"sdkconfig_append": [
|
||||
"CONFIG_CAMERA_OV2640=y",
|
||||
"CONFIG_CAMERA_OV3660=y",
|
||||
"CONFIG_CAMERA_OV3660_AUTO_DETECT_DVP_INTERFACE_SENSOR=y",
|
||||
"CONFIG_CAMERA_OV3660_DVP_RGB565_240X240_24FPS=y",
|
||||
"CONFIG_CAMERA_OV2640_AUTO_DETECT_DVP_INTERFACE_SENSOR=y",
|
||||
"CONFIG_CAMERA_OV2640_DVP_RGB565_240X240_25FPS=y",
|
||||
"CONFIG_XIAOZHI_ENABLE_CAMERA_ENDIANNESS_SWAP=y"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -16,10 +16,9 @@
|
||||
|
||||
class KEVIN_SP_V4Board : public WifiBoard {
|
||||
private:
|
||||
i2c_master_bus_handle_t display_i2c_bus_;
|
||||
Button boot_button_;
|
||||
LcdDisplay* display_;
|
||||
i2c_master_bus_handle_t codec_i2c_bus_;
|
||||
i2c_master_bus_handle_t i2c_bus_;
|
||||
Esp32Camera* camera_;
|
||||
|
||||
void InitializeCodecI2c() {
|
||||
@ -36,7 +35,7 @@ private:
|
||||
.enable_internal_pullup = 1,
|
||||
},
|
||||
};
|
||||
ESP_ERROR_CHECK(i2c_new_master_bus(&i2c_bus_cfg, &codec_i2c_bus_));
|
||||
ESP_ERROR_CHECK(i2c_new_master_bus(&i2c_bus_cfg, &i2c_bus_));
|
||||
}
|
||||
|
||||
void InitializeSpi() {
|
||||
@ -118,12 +117,8 @@ private:
|
||||
};
|
||||
|
||||
esp_video_init_sccb_config_t sccb_config = {
|
||||
.init_sccb = true,
|
||||
.i2c_config = {
|
||||
.port = 1,
|
||||
.scl_pin = CAMERA_PIN_SIOC,
|
||||
.sda_pin = GPIO_NUM_NC,
|
||||
},
|
||||
.init_sccb = false,
|
||||
.i2c_handle = i2c_bus_,
|
||||
.freq = 100000,
|
||||
};
|
||||
|
||||
@ -159,7 +154,7 @@ public:
|
||||
}
|
||||
|
||||
virtual AudioCodec* GetAudioCodec() override {
|
||||
static Es8311AudioCodec audio_codec(codec_i2c_bus_, I2C_NUM_0, AUDIO_INPUT_SAMPLE_RATE, AUDIO_OUTPUT_SAMPLE_RATE,
|
||||
static Es8311AudioCodec audio_codec(i2c_bus_, I2C_NUM_0, AUDIO_INPUT_SAMPLE_RATE, AUDIO_OUTPUT_SAMPLE_RATE,
|
||||
AUDIO_I2S_GPIO_MCLK, AUDIO_I2S_GPIO_BCLK, AUDIO_I2S_GPIO_WS, AUDIO_I2S_GPIO_DOUT, AUDIO_I2S_GPIO_DIN,
|
||||
AUDIO_CODEC_PA_PIN, AUDIO_CODEC_ES8311_ADDR);
|
||||
return &audio_codec;
|
||||
|
||||
@ -369,7 +369,11 @@ bool EmoteEngine::OnFlushIoReady(const esp_lcd_panel_io_handle_t panel_io,
|
||||
esp_lcd_panel_io_event_data_t* const edata,
|
||||
void* const user_ctx)
|
||||
{
|
||||
return true;
|
||||
gfx_handle_t handle = static_cast<gfx_handle_t>(user_ctx);
|
||||
if (handle) {
|
||||
gfx_emote_flush_ready(handle, true);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void EmoteEngine::OnFlush(const gfx_handle_t handle, const int x_start, const int y_start,
|
||||
@ -379,7 +383,6 @@ void EmoteEngine::OnFlush(const gfx_handle_t handle, const int x_start, const in
|
||||
if (panel) {
|
||||
esp_lcd_panel_draw_bitmap(panel, x_start, y_start, x_end, y_end, color_data);
|
||||
}
|
||||
gfx_emote_flush_ready(handle, true);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
||||
@ -18,9 +18,9 @@ dependencies:
|
||||
espressif/esp_io_expander_tca9554: ==2.0.0
|
||||
espressif/esp_lcd_panel_io_additions: ^1.0.1
|
||||
78/esp_lcd_nv3023: ~1.0.0
|
||||
78/esp-wifi-connect: ~3.0.1
|
||||
78/esp-wifi-connect: ~3.0.2
|
||||
78/esp-opus-encoder: ~2.4.1
|
||||
78/esp-ml307: ~3.5.2
|
||||
78/esp-ml307: ~3.5.3
|
||||
78/xiaozhi-fonts: ~1.5.5
|
||||
espressif/led_strip: ~3.0.1
|
||||
espressif/esp_codec_dev: ~1.5
|
||||
@ -44,7 +44,7 @@ dependencies:
|
||||
esp_lvgl_port: ~2.6.0
|
||||
espressif/esp_io_expander_tca95xx_16bit: ^2.0.0
|
||||
espressif2022/image_player: ^1.1.1
|
||||
espressif2022/esp_emote_gfx: ^1.1.2
|
||||
espressif2022/esp_emote_gfx: ==2.0.0
|
||||
espressif/adc_mic: ^0.2.1
|
||||
espressif/esp_mmap_assets: '>=1.2'
|
||||
espressif/bq27220: ^0.1.1
|
||||
|
||||
@ -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}")
|
||||
|
||||
Loading…
Reference in New Issue
Block a user