重构蓝牙配网: 替换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:
parent
02ae116488
commit
77c7283d09
@ -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修改。
|
||||
|
||||
@ -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");
|
||||
}
|
||||
|
||||
@ -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); // 状态枚举转字符串
|
||||
};
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
@ -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;
|
||||
};
|
||||
@ -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...");
|
||||
|
||||
@ -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配网进程
|
||||
* 监控配网状态变化,处理超时和异常情况
|
||||
|
||||
@ -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
356
tests/ble_provision_test.py
Normal 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)
|
||||
Loading…
x
Reference in New Issue
Block a user