重构蓝牙配网: 替换BluFi为自定义GATT Server,修复手机蓝牙不可见问题

核心改动:
- bluetooth_provisioning: 使用 esp_ble_gap_config_adv_data_raw() 原始广播
  替代 BluFi API,采用自定义 GATT Server (Service 0xABF0, Write 0xABF1,
  Notify 0xABF2) 实现二进制配网协议,保留全部WiFi配网业务逻辑
- ble_service: 广播包名称移至 Scan Response,避免超31字节限制;
  GAP事件改用位掩码确保 adv_data 和 scan_rsp 都完成后再启动广播
- application: BLE JSON 服务从 Application 移至 WifiBoard 管理,
  HandleBleJsonCommand 改为接收 BleJsonService 引用参数
- wifi_board: 新增 StartBleJsonProvisioning(),配网入口切换回
  StartBluFiProvisioning() 使用重构后的 GATT Server
- 蓝牙设备名统一为 "Airhub_Ble"
- 配网模式下跳过电量上报,避免无WiFi时HTTP请求失败
- 新增 tests/ble_provision_test.py 配网协议测试脚本

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Rdzleo 2026-02-10 14:32:55 +08:00
parent 02ae116488
commit 77c7283d09
13 changed files with 1318 additions and 867 deletions

View File

@ -349,7 +349,7 @@ menu "蓝牙配网 (Bluetooth Provisioning)"
config BLUETOOTH_PROVISIONING_DEVICE_NAME
string "默认设备名称"
depends on BLUETOOTH_PROVISIONING_ENABLE
default "BLUFI_Airhub"
default "Airhub_Ble"
help
蓝牙配网时显示的默认设备名称。
可以在运行时通过API修改。

View File

@ -1574,8 +1574,7 @@ void Application::Start() {
SetDeviceState(kDeviceStateIdle);
// 初始化 BLE JSON 通讯服务
InitBleJsonService();
// BLE JSON 通讯服务已移至 WifiBoard 中,仅在配网模式下启动
// 每次设备开机后idle状态下测试 自动检测并设置当前位置打印
//此逻辑为冗余操作当前NVS中没有城市信息时会自动调用 位置查询API
@ -3052,33 +3051,12 @@ const char* Application::DeviceStateToString(DeviceState state) {
return "unknown";
}
void Application::InitBleJsonService() {
ESP_LOGI(TAG, "初始化 BLE JSON 通讯服务...");
if (!ble_json_service_.Initialize()) {
ESP_LOGE(TAG, "BLE JSON 服务初始化失败");
return;
}
ble_json_service_.SetCommandCallback(
[this](const std::string& cmd, int msg_id, cJSON* data) {
HandleBleJsonCommand(cmd, msg_id, data);
});
if (!ble_json_service_.Start()) {
ESP_LOGE(TAG, "BLE JSON 服务启动失败");
return;
}
ESP_LOGI(TAG, "BLE JSON 通讯服务启动成功");
}
void Application::HandleBleJsonCommand(const std::string& cmd, int msg_id, cJSON* data) {
void Application::HandleBleJsonCommand(const std::string& cmd, int msg_id, cJSON* data, BleJsonService& service) {
auto& board = Board::GetInstance();
// ---- ping ----
if (cmd == "ping") {
ble_json_service_.SendResponse(cmd, msg_id, 0, "pong");
service.SendResponse(cmd, msg_id, 0, "pong");
return;
}
@ -3106,7 +3084,7 @@ void Application::HandleBleJsonCommand(const std::string& cmd, int msg_id, cJSON
cJSON_AddNumberToObject(resp, "rssi", ap.rssi);
}
ble_json_service_.SendResponse(cmd, msg_id, 0, "ok", resp);
service.SendResponse(cmd, msg_id, 0, "ok", resp);
cJSON_Delete(resp);
return;
}
@ -3121,7 +3099,7 @@ void Application::HandleBleJsonCommand(const std::string& cmd, int msg_id, cJSON
cJSON_AddStringToObject(resp, "chip", SystemInfo::GetChipModelName().c_str());
cJSON_AddStringToObject(resp, "idf", app_desc->idf_ver);
ble_json_service_.SendResponse(cmd, msg_id, 0, "ok", resp);
service.SendResponse(cmd, msg_id, 0, "ok", resp);
cJSON_Delete(resp);
return;
}
@ -3131,7 +3109,7 @@ void Application::HandleBleJsonCommand(const std::string& cmd, int msg_id, cJSON
cJSON* ssid_item = cJSON_GetObjectItem(data, "ssid");
cJSON* pwd_item = cJSON_GetObjectItem(data, "pwd");
if (!ssid_item || !cJSON_IsString(ssid_item) || strlen(ssid_item->valuestring) == 0) {
ble_json_service_.SendResponse(cmd, msg_id, -1, "missing ssid");
service.SendResponse(cmd, msg_id, -1, "missing ssid");
return;
}
@ -3145,14 +3123,14 @@ void Application::HandleBleJsonCommand(const std::string& cmd, int msg_id, cJSON
esp_err_t ret = esp_wifi_set_config(WIFI_IF_STA, &wifi_config);
if (ret != ESP_OK) {
ble_json_service_.SendResponse(cmd, msg_id, -2, "set config failed");
service.SendResponse(cmd, msg_id, -2, "set config failed");
return;
}
// 断开当前连接并重新连接
esp_wifi_disconnect();
ret = esp_wifi_connect();
ble_json_service_.SendResponse(cmd, msg_id, 0,
service.SendResponse(cmd, msg_id, 0,
ret == ESP_OK ? "connecting" : "connect failed");
return;
}
@ -3163,7 +3141,7 @@ void Application::HandleBleJsonCommand(const std::string& cmd, int msg_id, cJSON
scan_config.show_hidden = false;
esp_err_t ret = esp_wifi_scan_start(&scan_config, true); // 阻塞扫描
if (ret != ESP_OK) {
ble_json_service_.SendResponse(cmd, msg_id, -1, "scan failed");
service.SendResponse(cmd, msg_id, -1, "scan failed");
return;
}
@ -3192,7 +3170,7 @@ void Application::HandleBleJsonCommand(const std::string& cmd, int msg_id, cJSON
free(ap_list);
}
ble_json_service_.SendResponse(cmd, msg_id, 0, "ok", resp);
service.SendResponse(cmd, msg_id, 0, "ok", resp);
cJSON_Delete(resp);
return;
}
@ -3201,7 +3179,7 @@ void Application::HandleBleJsonCommand(const std::string& cmd, int msg_id, cJSON
if (cmd == "set_vol") {
cJSON* vol_item = cJSON_GetObjectItem(data, "vol");
if (!vol_item || !cJSON_IsNumber(vol_item)) {
ble_json_service_.SendResponse(cmd, msg_id, -1, "missing vol");
service.SendResponse(cmd, msg_id, -1, "missing vol");
return;
}
int vol = vol_item->valueint;
@ -3216,13 +3194,13 @@ void Application::HandleBleJsonCommand(const std::string& cmd, int msg_id, cJSON
s.SetInt("output_volume", vol);
}
ble_json_service_.SendResponse(cmd, msg_id, 0, "ok");
service.SendResponse(cmd, msg_id, 0, "ok");
return;
}
// ---- reboot: 重启设备 ----
if (cmd == "reboot") {
ble_json_service_.SendResponse(cmd, msg_id, 0, "rebooting");
service.SendResponse(cmd, msg_id, 0, "rebooting");
vTaskDelay(pdMS_TO_TICKS(500)); // 等待响应发出
Reboot();
return;
@ -3231,10 +3209,10 @@ void Application::HandleBleJsonCommand(const std::string& cmd, int msg_id, cJSON
// ---- ota: 触发 OTA 升级 ----
if (cmd == "ota") {
if (device_state_ == kDeviceStateUpgrading) {
ble_json_service_.SendResponse(cmd, msg_id, -1, "already upgrading");
service.SendResponse(cmd, msg_id, -1, "already upgrading");
return;
}
ble_json_service_.SendResponse(cmd, msg_id, 0, "start ota");
service.SendResponse(cmd, msg_id, 0, "start ota");
Schedule([this]() {
CheckNewVersion();
});
@ -3247,14 +3225,14 @@ void Application::HandleBleJsonCommand(const std::string& cmd, int msg_id, cJSON
std::string states;
if (thing_manager.GetStatesJson(states, true)) {
cJSON* resp = cJSON_Parse(states.c_str());
ble_json_service_.SendResponse(cmd, msg_id, 0, "ok", resp);
service.SendResponse(cmd, msg_id, 0, "ok", resp);
if (resp) cJSON_Delete(resp);
} else {
ble_json_service_.SendResponse(cmd, msg_id, 0, "ok");
service.SendResponse(cmd, msg_id, 0, "ok");
}
return;
}
// ---- 未知命令 ----
ble_json_service_.SendResponse(cmd, msg_id, -99, "unknown cmd");
service.SendResponse(cmd, msg_id, -99, "unknown cmd");
}

View File

@ -109,6 +109,9 @@ public:
bool IsDialogUploadEnabled() const { return dialog_upload_enabled_; }// 是否启用对话上传
void SetDialogUploadEnabled(bool enabled);// 设置对话上传状态
// BLE JSON 命令处理 (由 WifiBoard 中的 BleJsonService 回调)
void HandleBleJsonCommand(const std::string& cmd, int msg_id, cJSON* data, BleJsonService& service);
private:
Application();// 构造函数
~Application();// 析构函数
@ -193,10 +196,6 @@ private:
void StartDialogWatchdog();// 启动对话看门狗
void StopDialogWatchdog(); // 停止对话看门狗
// BLE JSON 通讯服务
BleJsonService ble_json_service_;
void InitBleJsonService(); // 初始化 BLE JSON 通讯
void HandleBleJsonCommand(const std::string& cmd, int msg_id, cJSON* data); // 处理 BLE 命令
const char* DeviceStateToString(DeviceState state); // 状态枚举转字符串
};

View File

@ -16,7 +16,7 @@ public:
AudioCodec();
virtual ~AudioCodec();
static constexpr int kDefaultOutputVolume = 30; // 默认输出音量 系统默认音量设置为100最大音量原来为70 产测固件使用
static constexpr int kDefaultOutputVolume = 40; // 默认输出音量 系统默认音量设置为100最大音量原来为70 产测固件使用
inline static int default_output_volume() { return kDefaultOutputVolume; }
virtual void SetOutputVolume(int volume);

View File

@ -61,19 +61,19 @@ static uint8_t cccd_val[2] = {0x00, 0x00};
static esp_attr_value_t write_char_attr = {
.attr_max_len = BLE_JSON_CHAR_VAL_MAX_LEN,
.attr_len = 0,
.attr_len = 1,
.attr_value = write_char_val,
};
static esp_attr_value_t notify_char_attr = {
.attr_max_len = BLE_JSON_CHAR_VAL_MAX_LEN,
.attr_len = 0,
.attr_len = 1,
.attr_value = notify_char_val,
};
static esp_attr_value_t status_char_attr = {
.attr_max_len = BLE_JSON_CHAR_VAL_MAX_LEN,
.attr_len = 0,
.attr_len = 1,
.attr_value = status_char_val,
};
@ -354,10 +354,10 @@ bool BleJsonService::SendNotify(const char* json_str, uint16_t len) {
// ============================================================
void BleJsonService::StartAdvertising() {
// 构建广播数据
// 广播包不放名称避免超过31字节导致手机系统蓝牙搜索不到
esp_ble_adv_data_t adv_data = {};
adv_data.set_scan_rsp = false;
adv_data.include_name = true;
adv_data.include_name = false; // 名称放在 Scan Response 中
adv_data.include_txpower = true;
adv_data.min_interval = 0x0006;
adv_data.max_interval = 0x0010;
@ -372,7 +372,7 @@ void BleJsonService::StartAdvertising() {
esp_ble_gap_config_adv_data(&adv_data);
// Scan response 中也加设备名
// Scan Response放完整设备名称手机扫描时会主动请求
esp_ble_adv_data_t scan_rsp = {};
scan_rsp.set_scan_rsp = true;
scan_rsp.include_name = true;
@ -388,12 +388,20 @@ void BleJsonService::GapEventHandler(esp_gap_ble_cb_event_t event,
esp_ble_gap_cb_param_t* param) {
switch (event) {
case ESP_GAP_BLE_ADV_DATA_SET_COMPLETE_EVT:
// 广播数据设置完成,等扫描应答数据也完成后再启动广播
break;
case ESP_GAP_BLE_SCAN_RSP_DATA_SET_COMPLETE_EVT:
// 扫描应答数据设置完成,启动广播
case ESP_GAP_BLE_SCAN_RSP_DATA_SET_COMPLETE_EVT: {
// 两个事件都到达后才启动广播
static uint8_t adv_config_done = 0;
if (event == ESP_GAP_BLE_ADV_DATA_SET_COMPLETE_EVT) {
adv_config_done |= 0x01;
} else {
adv_config_done |= 0x02;
}
if (adv_config_done == 0x03) {
adv_config_done = 0; // 重置,供下次重新广播使用
esp_ble_gap_start_advertising(&ble_json_adv_params);
}
break;
}
case ESP_GAP_BLE_ADV_START_COMPLETE_EVT:
if (param->adv_start_cmpl.status != ESP_BT_STATUS_SUCCESS) {
ESP_LOGE(TAG, "Advertising start failed: %d", param->adv_start_cmpl.status);
@ -531,7 +539,7 @@ void BleJsonService::HandleGattsEvent(esp_gatts_cb_event_t event,
break;
// ---- 客户端连接 ----
case ESP_GATTS_CONNECT_EVT:
case ESP_GATTS_CONNECT_EVT: {
conn_id_ = param->connect.conn_id;
connected_ = true;
notify_enabled_ = false;
@ -555,6 +563,7 @@ void BleJsonService::HandleGattsEvent(esp_gatts_cb_event_t event,
// 连接后停止广播 (BLE 4.2 单连接时自动停止,但显式调用更安全)
esp_ble_gap_stop_advertising();
break;
}
// ---- 客户端断开 ----
case ESP_GATTS_DISCONNECT_EVT:

View File

@ -6,6 +6,7 @@
#ifdef ESP_PLATFORM
#include <esp_gatts_api.h>
#include <esp_gap_ble_api.h>
#include <freertos/FreeRTOS.h>
#include <freertos/queue.h>
#endif

View File

@ -29,7 +29,7 @@ extern "C" {
#define BLE_JSON_CHAR_VAL_MAX_LEN 512
// 广播设备名称
#define BLE_JSON_DEVICE_NAME "Kapi_BLE"
#define BLE_JSON_DEVICE_NAME "Airhub_Ble"
// 广播参数
#define BLE_JSON_ADV_INT_MIN 0x40 // 40ms

File diff suppressed because it is too large Load Diff

View File

@ -2,11 +2,11 @@
/**
* @file bluetooth_provisioning.h
* @brief BluFi蓝牙配网模块头文件
* @brief GATT Server
*
* BluFi蓝牙配网的相关接口
* WiFi凭据传输等功能C++
* ESP-IDF的BLUFI功能WiFi配网操作
* 使 BLE GATT Server BluFi API
* 广raw advertising
* WiFi
*/
#include <functional>
@ -17,25 +17,55 @@
// 使用条件编译避免IDE环境中的头文件错误
#ifdef ESP_PLATFORM
#include "esp_blufi_api.h"
#include "esp_gap_ble_api.h"
#include "esp_gatts_api.h"
#include "esp_gatt_common_api.h"
#include "esp_wifi.h"
#include "esp_event.h"
#else
// 在非ESP环境中定义必要的类型和常量
typedef int esp_blufi_cb_event_t;
typedef void* esp_blufi_cb_param_t;
typedef int esp_event_base_t;
typedef void* wifi_ap_record_t;
typedef void* esp_event_base_t;
#endif
// ============================================================
// 配网协议命令定义 (手机 → 设备, WRITE 特征)
// ============================================================
#define PROV_CMD_SET_SSID 0x01 // 设置 WiFi SSID
#define PROV_CMD_SET_PASSWORD 0x02 // 设置 WiFi 密码
#define PROV_CMD_SET_BSSID 0x03 // 设置 BSSID (6字节)
#define PROV_CMD_CONNECT_AP 0x04 // 请求连接 WiFi
#define PROV_CMD_DISCONNECT_AP 0x05 // 请求断开 WiFi
#define PROV_CMD_GET_WIFI_LIST 0x06 // 请求 WiFi 列表
#define PROV_CMD_DISCONNECT_BLE 0x07 // 请求断开 BLE
#define PROV_CMD_SET_WIFI_MODE 0x08 // 设置 WiFi 模式
#define PROV_CMD_GET_WIFI_STATUS 0x09 // 获取 WiFi 状态
#define PROV_CMD_CUSTOM_DATA 0x10 // 自定义数据
// ============================================================
// 配网协议响应定义 (设备 → 手机, NOTIFY 特征)
// ============================================================
#define PROV_RESP_WIFI_STATUS 0x81 // WiFi 状态: [success(1)][reason(1)]
#define PROV_RESP_WIFI_LIST 0x82 // WiFi 列表: [rssi(1)][ssid_len(1)][ssid...]
#define PROV_RESP_WIFI_LIST_END 0x83 // WiFi 列表结束标记
#define PROV_RESP_CUSTOM_DATA 0x84 // 自定义数据
// ============================================================
// GATT 服务配置
// ============================================================
#define PROV_SERVICE_UUID 0xABF0 // 配网服务 UUID
#define PROV_CHAR_WRITE_UUID 0xABF1 // 写入特征 UUID (手机→设备)
#define PROV_CHAR_NOTIFY_UUID 0xABF2 // 通知特征 UUID (设备→手机)
#define PROV_APP_ID 2 // GATTS App ID
#define PROV_HANDLE_NUM 8 // Service handle 数量
#define PROV_LOCAL_MTU 512 // 本地 MTU
/**
* @brief
*
* BluFi配网过程中的各种状态
*/
enum class BluetoothProvisioningState {
IDLE, //< 空闲状态,未启动配网
INITIALIZING, //< 初始化中正在初始化蓝牙和BluFi服务
INITIALIZING, //< 初始化中,正在初始化蓝牙和服务
ADVERTISING, //< 广播中,等待手机客户端连接
CONNECTED, //< 已连接,手机客户端已连接到设备
PROVISIONING, //< 配网中正在接收和处理WiFi凭据
@ -46,255 +76,100 @@ enum class BluetoothProvisioningState {
/**
* @brief
*
*
*/
enum class BluetoothProvisioningEvent {
STATE_CHANGED, //< 状态改变事件,配网状态发生变化
WIFI_CREDENTIALS, //< 收到WiFi凭据事件从手机接收到WiFi信息
WIFI_CONNECTED, //< WiFi连接成功事件设备成功连接到WiFi网络
WIFI_FAILED, //< WiFi连接失败事件设备连接WiFi失败
CLIENT_CONNECTED, //< 客户端连接事件,手机客户端连接到设备
CLIENT_DISCONNECTED //< 客户端断开事件,手机客户端断开连接
STATE_CHANGED, //< 状态改变事件
WIFI_CREDENTIALS, //< 收到WiFi凭据事件
WIFI_CONNECTED, //< WiFi连接成功事件
WIFI_FAILED, //< WiFi连接失败事件
CLIENT_CONNECTED, //< 客户端连接事件
CLIENT_DISCONNECTED //< 客户端断开事件
};
/**
* @brief WiFi凭据结构体
*
* WiFi连接信息
*/
struct WiFiCredentials {
std::string ssid; //< WiFi网络名称SSID
std::string password; //< WiFi网络密码
uint8_t bssid[6]; //< WiFi接入点的MAC地址BSSID,可选
bool bssid_set; //< 是否设置了BSSID,用于指定特定的接入点
uint8_t bssid[6]; //< WiFi接入点的MAC地址BSSID
bool bssid_set; //< 是否设置了BSSID
};
/**
* @brief
* @param event
* @param data
*/
using BluetoothProvisioningCallback = std::function<void(BluetoothProvisioningEvent event, void* data)>;
/**
* @brief
* @brief GATT Server
*
* ESP-IDF的BLUFI功能C++
* WiFi配网操作WiFi凭据接收等功能
*
* 使
* 1. BluetoothProvisioning实例
* 2.
* 3. StartProvisioning()
* 4.
* 5. StopProvisioning()
* 使 BLE GATT Server + 广 BluFi API
* WiFi
*/
class BluetoothProvisioning {
public:
/**
* @brief
*
*
*/
BluetoothProvisioning();
/**
* @brief
*
*
*/
~BluetoothProvisioning();
/**
* @brief
*
* BluFi服务
*
* @return true false
*/
bool Initialize();
/**
* @brief
*
*
*
* @return true false
*/
bool Deinitialize();
/**
* @brief
*
* BluFi服务广
*
* @param device_name "BLUFI_Airhub"
* @return true false
*/
bool StartProvisioning(const char* device_name = BLU_NAME);
/**
* @brief
*
* BluFi服务广
*
* @return true false
*/
bool StopProvisioning();
/**
* @brief
* @return
*/
BluetoothProvisioningState GetState() const { return state_; }
/**
* @brief
*
*
*
* @param callback
*/
void SetCallback(BluetoothProvisioningCallback callback) { callback_ = callback; }
/**
* @brief WiFi凭据
*
* WiFi连接信息
*
* @return WiFi凭据结构体的常量引用
*/
const WiFiCredentials& GetWiFiCredentials() const { return wifi_credentials_; }
/**
* @brief
*
*
*
* @return true false
*/
bool IsClientConnected() const { return client_connected_; }
/**
* @brief
*
* 便
*
* @return
*/
std::string GetStateString() const;
/**
* @brief WiFi连接状态报告
*
* WiFi连接尝试的结果
*
* @param success
* @param reason
*/
void ReportWiFiStatus(bool success, uint8_t reason = 0);
/**
* @brief WiFi扫描结果
*
* WiFi接入点列表发送给手机客户端
*
* @param ap_list WiFi接入点记录数组
* @param ap_count
*/
void SendWiFiList(const wifi_ap_record_t* ap_list, uint16_t ap_count);
/**
* @brief MAC地址给手机客户端
*
* MAC地址发送机制
* -
* -
* - MAC地址
* -
*
* @return true false
*/
bool SendMacAddressReliably();
/**
* @brief MAC地址发送状态
*
*
* MAC地址
*/
void ResetMacSendingState();
private:
BluetoothProvisioningState state_; //< 当前配网状态
BluetoothProvisioningCallback callback_; //< 用户设置的事件回调函数
WiFiCredentials wifi_credentials_; //< 存储接收到的WiFi凭据信息
bool client_connected_; //< 客户端连接状态标志
bool initialized_; //< 蓝牙配网模块初始化状态标志
bool delayed_disconnect_; //< 延迟断开连接标志,用于优雅断开
bool wifi_connecting_; //< WiFi连接进行中标志
bool mac_address_sent_; //< MAC地址发送状态标志避免重复发送
BluetoothProvisioningState state_;
BluetoothProvisioningCallback callback_;
WiFiCredentials wifi_credentials_;
bool client_connected_;
bool initialized_;
bool delayed_disconnect_;
bool wifi_connecting_;
bool mac_address_sent_;
// 静态实例指针用于C回调函数访问
static BluetoothProvisioning* instance_; //< 单例实例指针,用于静态回调函数访问类成员
static BluetoothProvisioning* instance_;
/**
* @brief
*
*
*
* @param new_state
*/
void SetState(BluetoothProvisioningState new_state);
/**
* @brief
*
*
*
* @param event
* @param data
*/
void TriggerCallback(BluetoothProvisioningEvent event, void* data = nullptr);
public:
/**
* @brief BluFi事件回调函数
*
* ESP-IDF BluFi库的静态回调函数BluFi相关事件
*
* @param event BluFi事件类型
* @param param
*/
static void BlufiEventCallback(esp_blufi_cb_event_t event, esp_blufi_cb_param_t* param);
#ifdef ESP_PLATFORM
// GATT 相关成员
esp_gatt_if_t gatts_if_ = ESP_GATT_IF_NONE;
uint16_t service_handle_ = 0;
uint16_t write_char_handle_ = 0;
uint16_t notify_char_handle_ = 0;
uint16_t notify_cccd_handle_ = 0;
uint16_t conn_id_ = 0;
bool notify_enabled_ = false;
uint16_t mtu_ = 23;
private:
/**
* @brief WiFi事件处理函数
*
* WiFi连接
*
* @param arg
* @param event_base
* @param event_id ID
* @param event_data
*/
// BLE 回调
static void GattsEventHandler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t* param);
static void GapEventHandler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t* param);
void HandleGattsEvent(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t* param);
// GATT 辅助方法
void CreateService(esp_gatt_if_t gatts_if);
void StartAdvertising();
bool SendNotify(const uint8_t* data, uint16_t len);
void ProcessWriteData(const uint8_t* data, uint16_t len);
// WiFi 事件处理
static void WiFiEventHandler(void* arg, esp_event_base_t event_base, int32_t event_id, void* event_data);
/**
* @brief IP事件处理函数
*
* IP地址获取等网络相关事件
*
* @param arg
* @param event_base
* @param event_id ID
* @param event_data
*/
static void IPEventHandler(void* arg, esp_event_base_t event_base, int32_t event_id, void* event_data);
#endif
// 禁用拷贝构造和赋值操作
BluetoothProvisioning(const BluetoothProvisioning&) = delete;
BluetoothProvisioning& operator=(const BluetoothProvisioning&) = delete;
};

View File

@ -72,22 +72,21 @@ std::string WifiBoard::GetBoardType() {
void WifiBoard::EnterWifiConfigMode() {
ESP_LOGI(TAG, "🔵 进入配网模式 - 使用BluFi蓝牙配网");
// 直接启动BluFi配网不再回退到WiFi AP模式
bool blufi_success = StartBluFiProvisioning();
ESP_LOGI(TAG, "🔍 BluFi配网启动结果: %s", blufi_success ? "成功" : "失败");
// 使用 BluFi 蓝牙配网
bool success = StartBluFiProvisioning();
ESP_LOGI(TAG, "🔍 BluFi配网启动结果: %s", success ? "成功" : "失败");
if (blufi_success) {
if (success) {
ESP_LOGI(TAG, "✅ BluFi配网启动成功等待手机连接");
return;
}
ESP_LOGW(TAG, "⚠️ BluFi配网启动失败将持续重试BluFi配网不使用WiFi AP模式");
ESP_LOGI(TAG, "🔄 持续重试BluFi蓝牙配网...");
ESP_LOGW(TAG, "⚠️ BluFi配网启动失败将持续重试");
// 持续重试BluFi配网
// 持续重试
while (true) {
vTaskDelay(pdMS_TO_TICKS(5000)); // 等待5秒后重试
ESP_LOGI(TAG, "🔄 重试启动BluFi蓝牙配网...");
vTaskDelay(pdMS_TO_TICKS(5000));
ESP_LOGI(TAG, "🔄 重试启动BluFi配网...");
if (StartBluFiProvisioning()) {
ESP_LOGI(TAG, "✅ BluFi配网重试成功等待手机连接");
return;
@ -152,17 +151,14 @@ void WifiBoard::StartNetwork() {
auto& ssid_manager = SsidManager::GetInstance(); // 获取SSID管理器实例
auto ssid_list = ssid_manager.GetSsidList(); // 获取SSID列表
if (ssid_list.empty()) {
ESP_LOGI(TAG, "🔍 未找到WiFi凭据启动BluFi蓝牙配网...");
ESP_LOGI(TAG, "🔍 未找到WiFi凭据启动BluFi配网...");
if (StartBluFiProvisioning()) {
ESP_LOGI(TAG, "✅ BluFi蓝牙配网启动成功等待手机连接...");
// BluFi配网启动成功等待完成或超时
ESP_LOGI(TAG, "✅ BluFi配网启动成功等待手机连接...");
return;
} else {
// BluFi配网启动失败继续尝试重新启动BluFi配网
ESP_LOGW(TAG, "❌ BluFi蓝牙配网启动失败将持续重试BluFi配网");
// 延迟后重试BluFi配网
vTaskDelay(pdMS_TO_TICKS(5000)); // 等待5秒后重试
ESP_LOGI(TAG, "🔄 重试启动BluFi蓝牙配网...");
ESP_LOGW(TAG, "❌ BluFi配网启动失败将重试");
vTaskDelay(pdMS_TO_TICKS(5000));
ESP_LOGI(TAG, "🔄 重试启动BluFi配网...");
StartBluFiProvisioning();
return;
}
@ -418,6 +414,63 @@ bool WifiBoard::StartBluFiProvisioning() {
return true;// 启动成功返回true
}
// 使用 BLE JSON Service 进行配网 (替代 BluFi)
bool WifiBoard::StartBleJsonProvisioning() {
ESP_LOGI(TAG, "🔵 正在启动BLE JSON配网服务...");
Application::GetInstance().StopAudioProcessor();
Application::GetInstance().ClearAudioQueue();
// 初始化 BLE JSON 服务
if (!ble_json_service_.Initialize()) {
ESP_LOGE(TAG, "❌ BLE JSON服务初始化失败");
return false;
}
// 设置命令回调,转发给 Application 处理
ble_json_service_.SetCommandCallback(
[this](const std::string& cmd, int msg_id, cJSON* data) {
Application::GetInstance().HandleBleJsonCommand(cmd, msg_id, data, ble_json_service_);
});
// 使用与 BluFi 相同的设备名启动
std::string device_name = BLU_NAME;
if (!ble_json_service_.Start(device_name.c_str())) {
ESP_LOGE(TAG, "❌ BLE JSON服务启动失败");
return false;
}
ESP_LOGI(TAG, "✅ BLE JSON配网启动成功设备名称: %s", device_name.c_str());
blufi_provisioning_active_ = true;
blufi_start_time_ = xTaskGetTickCount();
// 显示配网通知
auto display = GetDisplay();
if (display) {
std::string notification = "BLE配网模式\n设备名: " + device_name;
display->ShowNotification(notification.c_str(), 30000);
}
// 播放配网提示音
auto& application = Application::GetInstance();
if (strcmp(CONFIG_DEVICE_ROLE, "KAKA") == 0) {
application.Alert("BLE配网模式", ("请使用手机APP连接设备: " + device_name).c_str(), "", Lang::Sounds::P3_KAKA_WIFICONFIG);
} else if (strcmp(CONFIG_DEVICE_ROLE, "RTC_Test") == 0) {
application.Alert("BLE配网模式", ("请使用手机APP连接设备: " + device_name).c_str(), "", Lang::Sounds::P3_LALA_WIFICONFIG);
}
// 配网状态等待循环
while (true) {
int free_sram = heap_caps_get_free_size(MALLOC_CAP_INTERNAL);
int min_free_sram = heap_caps_get_minimum_free_size(MALLOC_CAP_INTERNAL);
ESP_LOGI(TAG, "BLE配网等待中... Free internal: %u minimal internal: %u", free_sram, min_free_sram);
vTaskDelay(pdMS_TO_TICKS(10000));
}
return true;
}
// 监控BluFi配网状态
void WifiBoard::MonitorBluFiProvisioning() {
ESP_LOGI(TAG, "Starting BluFi provisioning monitor...");

View File

@ -12,6 +12,7 @@
#include "board.h"
#include "bluetooth_provisioning.h"
#include "ble_service.h"
#include <string>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
@ -33,7 +34,8 @@ protected:
bool blufi_provisioning_success_ = false; ///< BluFi配网成功状态标志
TickType_t blufi_start_time_ = 0; ///< BluFi配网开始时间戳
static const TickType_t BLUFI_TIMEOUT_MS = 300000; ///< BluFi配网超时时间5分钟避免过快重新进入配网
BluetoothProvisioning bluetooth_provisioning_; ///< BluFi蓝牙配网实例对象
BluetoothProvisioning bluetooth_provisioning_; ///< BluFi蓝牙配网实例对象暂停使用
BleJsonService ble_json_service_; ///< BLE JSON 配网服务实例
/**
* @brief
@ -63,6 +65,9 @@ protected:
*/
bool StartBluFiProvisioning();
// 使用 BLE JSON Service 替代 BluFi 进行配网
bool StartBleJsonProvisioning();
/**
* @brief BluFi配网进程
*

View File

@ -1124,13 +1124,16 @@ public:
});
}
// 电量上报逻辑每30秒上报一次启动3秒后
// 电量上报逻辑每30秒上报一次启动3秒后,配网模式下跳过
if (battery_report_enabled_) {
battery_report_ticks_++;
if (battery_report_ticks_ % kBatteryReportInterval == 0) {
auto& wifi_station = WifiStation::GetInstance();
if (wifi_station.IsConnected()) {
ReportBatteryToServer(battery_level_);
}
}
}
battery_alert_ticks_++;
auto& app = Application::GetInstance();

356
tests/ble_provision_test.py Normal file
View File

@ -0,0 +1,356 @@
#!/usr/bin/env python3
"""
BLE 配网协议测试脚本
用途: 模拟手机端通过 BLE ESP32 设备通信测试自定义 GATT 配网协议
依赖: pip install bleak
运行:
# 交互式配网(输入 SSID 和密码)
python tests/ble_provision_test.py --ssid "MyWiFi" --pwd "12345678"
# 扫描 WiFi 列表
python tests/ble_provision_test.py --scan-wifi
# 查询 WiFi 状态
python tests/ble_provision_test.py --get-status
# 指定设备名
python tests/ble_provision_test.py --device "Airhub_Ble" --ssid "MyWiFi" --pwd "12345678"
协议说明:
Service UUID: 0xABF0
Write Char: 0xABF1 (手机设备, 二进制命令)
Notify Char: 0xABF2 (设备手机, 二进制响应)
命令格式: [cmd(1字节)] + [payload...]
响应格式: [resp(1字节)] + [data...]
"""
import argparse
import asyncio
import sys
import time
import struct
try:
from bleak import BleakClient, BleakScanner
from bleak.backends.characteristic import BleakGATTCharacteristic
except ImportError:
print("错误: 缺少 bleak 库,请执行: pip install bleak")
sys.exit(1)
# ============================================================
# BLE 参数定义 (与 bluetooth_provisioning.h 一致)
# ============================================================
SERVICE_UUID = "0000abf0-0000-1000-8000-00805f9b34fb"
CHAR_WRITE_UUID = "0000abf1-0000-1000-8000-00805f9b34fb"
CHAR_NOTIFY_UUID = "0000abf2-0000-1000-8000-00805f9b34fb"
DEFAULT_DEVICE = "Airhub_Ble"
# 命令码 (手机→设备)
CMD_SET_SSID = 0x01
CMD_SET_PASSWORD = 0x02
CMD_SET_BSSID = 0x03
CMD_CONNECT_AP = 0x04
CMD_DISCONNECT_AP = 0x05
CMD_GET_WIFI_LIST = 0x06
CMD_DISCONNECT_BLE = 0x07
CMD_SET_WIFI_MODE = 0x08
CMD_GET_WIFI_STATUS = 0x09
CMD_CUSTOM_DATA = 0x10
# 响应码 (设备→手机)
RESP_WIFI_STATUS = 0x81
RESP_WIFI_LIST = 0x82
RESP_WIFI_LIST_END = 0x83
RESP_CUSTOM_DATA = 0x84
class BleProvisionTester:
"""BLE 配网协议测试器"""
def __init__(self, device_name: str, timeout: float = 10.0):
self.device_name = device_name
self.timeout = timeout
self.client = None
self.notifications = []
self._notify_event = asyncio.Event()
def _on_notify(self, sender: BleakGATTCharacteristic, data: bytearray):
"""NOTIFY 回调"""
self.notifications.append(bytes(data))
self._notify_event.set()
self._print_notification(data)
@staticmethod
def _print_notification(data: bytearray):
"""解析并打印 NOTIFY 数据"""
if len(data) < 1:
print(f" <- [空数据]")
return
resp_type = data[0]
hex_str = data.hex(" ")
if resp_type == RESP_WIFI_STATUS:
success = data[1] if len(data) > 1 else 0
reason = data[2] if len(data) > 2 else 0
status = "成功" if success == 1 else f"失败 (原因码: {reason})"
print(f" <- [WiFi状态] {status} (raw: {hex_str})")
elif resp_type == RESP_WIFI_LIST:
if len(data) >= 3:
rssi = struct.unpack("b", bytes([data[1]]))[0] # 有符号
ssid_len = data[2]
ssid = data[3:3 + ssid_len].decode("utf-8", errors="replace")
print(f" <- [WiFi] RSSI={rssi}dBm SSID=\"{ssid}\"")
else:
print(f" <- [WiFi列表] 数据不完整 (raw: {hex_str})")
elif resp_type == RESP_WIFI_LIST_END:
print(f" <- [WiFi列表结束]")
elif resp_type == RESP_CUSTOM_DATA:
payload = data[1:]
print(f" <- [自定义数据] {payload.hex(' ')} text=\"{payload.decode('utf-8', errors='replace')}\"")
else:
print(f" <- [未知响应 0x{resp_type:02x}] {hex_str}")
async def _wait_notifications(self, timeout: float = None, count: int = 1):
"""等待指定数量的通知"""
timeout = timeout or self.timeout
start = len(self.notifications)
deadline = time.monotonic() + timeout
while len(self.notifications) - start < count:
remaining = deadline - time.monotonic()
if remaining <= 0:
break
self._notify_event.clear()
try:
await asyncio.wait_for(self._notify_event.wait(), timeout=remaining)
except asyncio.TimeoutError:
break
return self.notifications[start:]
async def scan_and_connect(self):
"""扫描并连接设备"""
print(f"正在扫描设备 '{self.device_name}'...")
device = await BleakScanner.find_device_by_name(
self.device_name, timeout=10.0
)
if not device:
print(f"未找到设备 '{self.device_name}',请确认设备已开机且处于配网模式")
return False
print(f"找到设备: {device.address}")
print(f"正在连接...")
self.client = BleakClient(device, timeout=15.0)
await self.client.connect()
print(f"已连接, MTU={self.client.mtu_size}")
# 验证服务
svc = self.client.services.get_service(SERVICE_UUID)
if not svc:
print(f"错误: 未发现配网服务 (UUID: 0xABF0)")
return False
print(f"发现配网服务 0xABF0")
# 启用 NOTIFY
await self.client.start_notify(CHAR_NOTIFY_UUID, self._on_notify)
print(f"NOTIFY 已启用 (0xABF2)")
await asyncio.sleep(0.3)
return True
async def send_cmd(self, cmd: int, payload: bytes = b""):
"""发送二进制命令"""
data = bytes([cmd]) + payload
hex_str = data.hex(" ")
print(f" -> [0x{cmd:02x}] {hex_str}")
await self.client.write_gatt_char(CHAR_WRITE_UUID, data, response=True)
async def provision_wifi(self, ssid: str, password: str):
"""执行 WiFi 配网流程"""
print(f"\n{'='*50}")
print(f" 开始配网: SSID=\"{ssid}\"")
print(f"{'='*50}\n")
# 第1步: 设置 SSID
print("[1/3] 发送 SSID...")
await self.send_cmd(CMD_SET_SSID, ssid.encode("utf-8"))
await asyncio.sleep(0.3)
# 第2步: 设置密码(设置密码后设备会自动发起连接)
print("[2/3] 发送密码...")
await self.send_cmd(CMD_SET_PASSWORD, password.encode("utf-8"))
# 第3步: 等待连接结果
print("[3/3] 等待WiFi连接结果 (最长30秒)...")
result = await self._wait_wifi_result(timeout=30.0)
if result is None:
print("\n超时: 未收到WiFi连接结果")
# 可尝试显式发送连接命令
print("尝试发送显式连接命令...")
await self.send_cmd(CMD_CONNECT_AP)
result = await self._wait_wifi_result(timeout=30.0)
if result is None:
print("\n配网结果: 超时,未收到设备响应")
elif result:
print("\n配网结果: WiFi 连接成功!")
else:
print("\n配网结果: WiFi 连接失败")
return result
async def _wait_wifi_result(self, timeout: float = 30.0):
"""等待 WiFi 状态通知"""
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
remaining = deadline - time.monotonic()
if remaining <= 0:
break
self._notify_event.clear()
try:
await asyncio.wait_for(self._notify_event.wait(), timeout=remaining)
except asyncio.TimeoutError:
break
# 检查最新通知
if self.notifications:
last = self.notifications[-1]
if len(last) >= 2 and last[0] == RESP_WIFI_STATUS:
return last[1] == 1 # 1=成功, 0=失败
return None
async def scan_wifi_list(self):
"""请求 WiFi 扫描列表"""
print(f"\n{'='*50}")
print(f" 扫描 WiFi 列表")
print(f"{'='*50}\n")
self.notifications.clear()
await self.send_cmd(CMD_GET_WIFI_LIST)
# WiFi 扫描需要时间,等待列表结束标记
print("等待扫描结果 (最长15秒)...")
deadline = time.monotonic() + 15.0
wifi_list = []
got_end = False
while time.monotonic() < deadline and not got_end:
remaining = deadline - time.monotonic()
if remaining <= 0:
break
self._notify_event.clear()
try:
await asyncio.wait_for(self._notify_event.wait(), timeout=remaining)
except asyncio.TimeoutError:
break
# 只处理最新一条通知
if self.notifications:
n = self.notifications[-1]
if len(n) >= 3 and n[0] == RESP_WIFI_LIST:
rssi = struct.unpack("b", bytes([n[1]]))[0]
ssid_len = n[2]
ssid = n[3:3 + ssid_len].decode("utf-8", errors="replace")
wifi_list.append({"ssid": ssid, "rssi": rssi})
elif len(n) >= 1 and n[0] == RESP_WIFI_LIST_END:
got_end = True
if wifi_list:
print(f"\n扫描到 {len(wifi_list)} 个WiFi网络:")
print(f" {'序号':>4} {'RSSI':>6} {'SSID'}")
print(f" {''*4} {''*6} {''*30}")
for i, w in enumerate(wifi_list, 1):
print(f" {i:>4} {w['rssi']:>4}dBm {w['ssid']}")
else:
print("未扫描到WiFi网络")
return wifi_list
async def get_wifi_status(self):
"""查询 WiFi 状态"""
print(f"\n{'='*50}")
print(f" 查询 WiFi 状态")
print(f"{'='*50}\n")
self.notifications.clear()
await self.send_cmd(CMD_GET_WIFI_STATUS)
await self._wait_notifications(timeout=5.0, count=1)
async def disconnect(self):
"""断开连接"""
if self.client and self.client.is_connected:
try:
await self.client.stop_notify(CHAR_NOTIFY_UUID)
except Exception:
pass
await self.client.disconnect()
print("已断开BLE连接")
async def main():
parser = argparse.ArgumentParser(
description="BLE 配网协议测试脚本",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
使用示例:
# WiFi 配网
python tests/ble_provision_test.py --ssid "MyWiFi" --pwd "12345678"
# 扫描 WiFi 列表
python tests/ble_provision_test.py --scan-wifi
# 查询 WiFi 状态
python tests/ble_provision_test.py --get-status
"""
)
parser.add_argument("--device", default=DEFAULT_DEVICE,
help=f"BLE 设备名称 (默认: {DEFAULT_DEVICE})")
parser.add_argument("--ssid", help="要连接的 WiFi SSID")
parser.add_argument("--pwd", default="", help="WiFi 密码")
parser.add_argument("--scan-wifi", action="store_true",
help="扫描 WiFi 列表")
parser.add_argument("--get-status", action="store_true",
help="查询 WiFi 连接状态")
parser.add_argument("--timeout", type=float, default=10.0,
help="命令超时秒数 (默认: 10.0)")
args = parser.parse_args()
# 至少指定一个操作
if not args.ssid and not args.scan_wifi and not args.get_status:
parser.print_help()
print("\n请指定操作: --ssid/--scan-wifi/--get-status")
return 1
tester = BleProvisionTester(device_name=args.device, timeout=args.timeout)
# 扫描连接
if not await tester.scan_and_connect():
return 1
try:
# 执行操作
if args.scan_wifi:
await tester.scan_wifi_list()
if args.get_status:
await tester.get_wifi_status()
if args.ssid:
result = await tester.provision_wifi(args.ssid, args.pwd)
return 0 if result else 1
finally:
await tester.disconnect()
return 0
if __name__ == "__main__":
exit_code = asyncio.run(main())
sys.exit(exit_code or 0)