2623 lines
75 KiB
Markdown
2623 lines
75 KiB
Markdown
# BluFi蓝牙配网小程序开发需求说明书
|
||
|
||
## 1. 项目概述
|
||
|
||
### 1.1 项目背景
|
||
本项目需要开发一个微信小程序,用于与ESP32设备进行BluFi蓝牙配网。该小程序需要完全兼容ESP官方的espblufi应用程序功能,能够成功进行WiFi配网并返回配网成功报告。
|
||
|
||
### 1.2 项目目标
|
||
- 开发微信小程序,实现BluFi蓝牙配网功能
|
||
- 与ESP32设备建立稳定的蓝牙连接
|
||
- 完成WiFi网络配置和连接验证
|
||
- 提供用户友好的配网界面和状态反馈
|
||
- 确保与官方espblufi应用程序的完全兼容性
|
||
|
||
### 1.3 设备端配置信息
|
||
基于项目代码分析,设备端配置如下:
|
||
|
||
```javascript
|
||
// 设备端配置参数(来自bluetooth_provisioning_config.h)
|
||
const DEVICE_CONFIG = {
|
||
// 设备名称配置
|
||
DEFAULT_DEVICE_NAME: "Airhub_Ble",
|
||
MAX_DEVICE_NAME_LEN: 32,
|
||
|
||
// 超时配置
|
||
ADV_TIMEOUT_MS: 0, // 永不超时
|
||
CLIENT_TIMEOUT_MS: 5 * 60 * 1000, // 5分钟
|
||
WIFI_TIMEOUT_MS: 100 * 1000, // 100秒
|
||
WIFI_MAX_RETRY: 5,
|
||
|
||
// 安全配置
|
||
SECURITY_ENABLED: false,
|
||
REQUIRE_PAIRING: false,
|
||
PSK: "Airhub2025",
|
||
|
||
// 功能开关
|
||
ENABLE_WIFI_SCAN: true,
|
||
AUTO_REPORT_STATUS: true,
|
||
AUTO_STOP_ON_SUCCESS: true,
|
||
AUTO_STOP_DELAY_MS: 5000
|
||
};
|
||
```
|
||
|
||
## 2. 技术架构
|
||
|
||
### 2.1 系统架构图
|
||
```
|
||
微信小程序 <---> 蓝牙BLE <---> ESP32设备 <---> WiFi网络
|
||
↓ ↓ ↓
|
||
用户界面 BluFi协议 WiFi连接
|
||
状态管理 数据加密 网络验证
|
||
```
|
||
|
||
### 2.2 设备端架构
|
||
基于`bluetooth_provisioning.h`和`bluetooth_provisioning.cc`分析:
|
||
|
||
```javascript
|
||
// 设备端状态枚举(对应C++代码)
|
||
const BluetoothProvisioningState = {
|
||
IDLE: 0, // 空闲状态,未启动配网
|
||
INITIALIZING: 1, // 初始化中,正在初始化蓝牙和BluFi服务
|
||
ADVERTISING: 2, // 广播中,等待手机客户端连接
|
||
CONNECTED: 3, // 已连接,手机客户端已连接到设备
|
||
PROVISIONING: 4, // 配网中,正在接收和处理WiFi凭据
|
||
SUCCESS: 5, // 配网成功,WiFi连接建立成功
|
||
FAILED: 6, // 配网失败,WiFi连接失败或其他错误
|
||
STOPPED: 7 // 已停止,配网服务已停止
|
||
};
|
||
|
||
// 设备端事件类型(对应C++代码)
|
||
const BluetoothProvisioningEvent = {
|
||
STATE_CHANGED: 0, // 状态改变事件,配网状态发生变化
|
||
WIFI_CREDENTIALS: 1, // 收到WiFi凭据事件,从手机接收到WiFi信息
|
||
WIFI_CONNECTED: 2, // WiFi连接成功事件,设备成功连接到WiFi网络
|
||
WIFI_FAILED: 3, // WiFi连接失败事件,设备连接WiFi失败
|
||
CLIENT_CONNECTED: 4, // 客户端连接事件,手机客户端连接到设备
|
||
CLIENT_DISCONNECTED: 5 // 客户端断开事件,手机客户端断开连接
|
||
};
|
||
```
|
||
|
||
### 2.3 技术栈
|
||
- **前端**: 微信小程序框架
|
||
- **通讯协议**: BluFi (基于BLE)
|
||
- **设备端**: ESP-IDF BluFi组件
|
||
- **加密**: 可选AES加密(当前项目未启用)
|
||
|
||
## 3. BluFi协议详解
|
||
|
||
### 3.1 协议概述
|
||
BluFi是乐鑫开发的基于蓝牙通道的WiFi网络配置协议,通过安全的蓝牙连接传输WiFi凭据。
|
||
|
||
### 3.2 GATT服务和特征值
|
||
|
||
#### 3.2.1 BluFi服务UUID(ESP32标准)
|
||
```javascript
|
||
// BluFi GATT服务和特征值UUID
|
||
const BLUFI_SERVICE_UUID = "0000FFFF-0000-1000-8000-00805F9B34FB";
|
||
const BLUFI_CHAR_P2E_UUID = "0000FF01-0000-1000-8000-00805F9B34FB"; // 手机到设备(写)
|
||
const BLUFI_CHAR_E2P_UUID = "0000FF02-0000-1000-8000-00805F9B34FB"; // 设备到手机(通知)
|
||
```
|
||
|
||
#### 3.2.2 设备发现和命名规则
|
||
```javascript
|
||
// 设备名称识别(基于项目配置)
|
||
function isValidBluFiDevice(device) {
|
||
// 检查设备名称是否符合项目规范
|
||
const validNames = [
|
||
"Airhub_Ble", // 默认名称
|
||
"XiaoZhi-AI" // 备用名称
|
||
];
|
||
|
||
return device.name && (
|
||
validNames.includes(device.name) ||
|
||
device.name.startsWith("Airhub-") ||
|
||
device.name.startsWith("XiaoZhi-")
|
||
);
|
||
}
|
||
```
|
||
|
||
### 3.3 数据包格式
|
||
|
||
#### 3.3.1 BluFi数据包结构
|
||
```javascript
|
||
// BluFi数据包格式(基于ESP-IDF实现)
|
||
class BluFiPacket {
|
||
constructor() {
|
||
this.type = 0x00; // 数据包类型 (1字节)
|
||
this.fc = 0x00; // 帧控制 (1字节)
|
||
this.sequence = 0x0000; // 序列号 (2字节)
|
||
this.length = 0x0000; // 数据长度 (2字节)
|
||
this.data = []; // 数据内容 (变长)
|
||
this.checksum = 0x0000; // 校验和 (2字节)
|
||
}
|
||
|
||
// 构建数据包
|
||
build(type, subtype, data = null) {
|
||
const dataLength = data ? data.length : 0;
|
||
const totalLength = 8 + dataLength;
|
||
const buffer = new ArrayBuffer(totalLength);
|
||
const view = new DataView(buffer);
|
||
|
||
// 设置包头
|
||
view.setUint8(0, type); // 类型
|
||
view.setUint8(1, subtype); // 子类型
|
||
view.setUint16(2, this.sequence, true); // 序列号(小端)
|
||
view.setUint16(4, dataLength, true); // 数据长度(小端)
|
||
|
||
// 设置数据
|
||
if (data && dataLength > 0) {
|
||
const dataView = new Uint8Array(buffer, 6);
|
||
dataView.set(new Uint8Array(data));
|
||
}
|
||
|
||
// 计算并设置校验和
|
||
const checksum = this.calculateChecksum(buffer, totalLength - 2);
|
||
view.setUint16(totalLength - 2, checksum, true);
|
||
|
||
this.sequence++;
|
||
return buffer;
|
||
}
|
||
|
||
// 计算校验和
|
||
calculateChecksum(buffer, length) {
|
||
let checksum = 0;
|
||
const view = new Uint8Array(buffer);
|
||
|
||
for (let i = 0; i < length; i++) {
|
||
checksum += view[i];
|
||
}
|
||
|
||
return checksum & 0xFFFF;
|
||
}
|
||
|
||
// 解析数据包
|
||
parse(buffer) {
|
||
const view = new DataView(buffer);
|
||
|
||
return {
|
||
type: view.getUint8(0),
|
||
subtype: view.getUint8(1),
|
||
sequence: view.getUint16(2, true),
|
||
length: view.getUint16(4, true),
|
||
data: buffer.slice(6, 6 + view.getUint16(4, true)),
|
||
checksum: view.getUint16(buffer.byteLength - 2, true)
|
||
};
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 3.3.2 数据包类型定义
|
||
```javascript
|
||
// 控制包类型(基于ESP-IDF BluFi实现)
|
||
const BLUFI_TYPE_CTRL = {
|
||
ACK: 0x00, // 确认包
|
||
SET_SEC_MODE: 0x01, // 设置安全模式
|
||
SET_WIFI_OPMODE: 0x02, // 设置WiFi操作模式
|
||
CONNECT_WIFI: 0x03, // 连接WiFi
|
||
DISCONNECT_WIFI: 0x04, // 断开WiFi
|
||
GET_WIFI_STATUS: 0x05, // 获取WiFi状态
|
||
DEAUTHENTICATE: 0x06, // 取消认证
|
||
GET_VERSION: 0x07, // 获取版本
|
||
CLOSE_CONNECTION: 0x08, // 关闭连接
|
||
GET_WIFI_LIST: 0x09 // 获取WiFi列表
|
||
};
|
||
|
||
// 数据包类型(基于ESP-IDF BluFi实现)
|
||
const BLUFI_TYPE_DATA = {
|
||
NEG: 0x00, // 协商数据
|
||
STA_BSSID: 0x01, // STA BSSID
|
||
STA_SSID: 0x02, // STA SSID
|
||
STA_PASSWD: 0x03, // STA 密码
|
||
SOFTAP_SSID: 0x04, // SoftAP SSID
|
||
SOFTAP_PASSWD: 0x05, // SoftAP 密码
|
||
SOFTAP_MAX_CONN: 0x06, // SoftAP最大连接数
|
||
SOFTAP_AUTH_MODE: 0x07, // SoftAP认证模式
|
||
SOFTAP_CHANNEL: 0x08, // SoftAP信道
|
||
USERNAME: 0x09, // 用户名
|
||
CA_CERT: 0x0A, // CA证书
|
||
CLIENT_CERT: 0x0B, // 客户端证书
|
||
SERVER_CERT: 0x0C, // 服务器证书
|
||
CLIENT_PRIV_KEY: 0x0D, // 客户端私钥
|
||
SERVER_PRIV_KEY: 0x0E, // 服务器私钥
|
||
WIFI_REP: 0x0F, // WiFi报告
|
||
WIFI_LIST: 0x10 // WiFi列表
|
||
};
|
||
|
||
// 包类型标识
|
||
const BLUFI_FC_ENC = 0x01; // 加密标志
|
||
const BLUFI_FC_CHECK = 0x02; // 校验标志
|
||
const BLUFI_FC_DATA_DIR = 0x04; // 数据方向标志
|
||
const BLUFI_FC_REQUIRE_ACK = 0x08; // 需要确认标志
|
||
```
|
||
|
||
## 4. 配网流程详细实现
|
||
|
||
### 4.1 第一阶段:蓝牙初始化和设备扫描
|
||
|
||
#### 4.1.1 小程序端实现
|
||
```javascript
|
||
// 蓝牙配网管理类
|
||
class BluFiProvisioning {
|
||
constructor() {
|
||
this.deviceId = null;
|
||
this.serviceId = null;
|
||
this.writeCharacteristicId = null;
|
||
this.notifyCharacteristicId = null;
|
||
this.sequenceNumber = 0;
|
||
this.isConnected = false;
|
||
this.provisioningState = 'idle';
|
||
this.packet = new BluFiPacket();
|
||
}
|
||
|
||
// 初始化蓝牙适配器
|
||
async initBluetooth() {
|
||
try {
|
||
console.log('初始化蓝牙适配器...');
|
||
|
||
await new Promise((resolve, reject) => {
|
||
wx.openBluetoothAdapter({
|
||
success: (res) => {
|
||
console.log('蓝牙适配器初始化成功:', res);
|
||
resolve(res);
|
||
},
|
||
fail: (err) => {
|
||
console.error('蓝牙适配器初始化失败:', err);
|
||
reject(new Error(`蓝牙初始化失败: ${err.errMsg}`));
|
||
}
|
||
});
|
||
});
|
||
|
||
// 检查蓝牙状态
|
||
await this.checkBluetoothState();
|
||
|
||
return true;
|
||
} catch (error) {
|
||
console.error('蓝牙初始化异常:', error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
// 检查蓝牙状态
|
||
async checkBluetoothState() {
|
||
return new Promise((resolve, reject) => {
|
||
wx.getBluetoothAdapterState({
|
||
success: (res) => {
|
||
console.log('蓝牙状态:', res);
|
||
if (!res.available) {
|
||
reject(new Error('蓝牙不可用'));
|
||
} else if (!res.discovering) {
|
||
console.log('蓝牙可用,准备扫描设备');
|
||
resolve(res);
|
||
} else {
|
||
resolve(res);
|
||
}
|
||
},
|
||
fail: (err) => {
|
||
reject(new Error(`获取蓝牙状态失败: ${err.errMsg}`));
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
// 扫描BluFi设备
|
||
async startScan(onDeviceFound) {
|
||
try {
|
||
console.log('开始扫描BluFi设备...');
|
||
|
||
// 监听设备发现事件
|
||
wx.onBluetoothDeviceFound((res) => {
|
||
res.devices.forEach(device => {
|
||
console.log('发现设备:', device);
|
||
|
||
// 检查是否为BluFi设备
|
||
if (this.isValidBluFiDevice(device)) {
|
||
console.log('发现BluFi设备:', device.name, device.deviceId);
|
||
onDeviceFound && onDeviceFound(device);
|
||
}
|
||
});
|
||
});
|
||
|
||
// 开始扫描
|
||
await new Promise((resolve, reject) => {
|
||
wx.startBluetoothDevicesDiscovery({
|
||
services: [BLUFI_SERVICE_UUID],
|
||
allowDuplicatesKey: false,
|
||
interval: 0,
|
||
success: (res) => {
|
||
console.log('开始扫描设备成功:', res);
|
||
resolve(res);
|
||
},
|
||
fail: (err) => {
|
||
console.error('开始扫描设备失败:', err);
|
||
reject(new Error(`扫描失败: ${err.errMsg}`));
|
||
}
|
||
});
|
||
});
|
||
|
||
return true;
|
||
} catch (error) {
|
||
console.error('扫描设备异常:', error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
// 停止扫描
|
||
async stopScan() {
|
||
return new Promise((resolve) => {
|
||
wx.stopBluetoothDevicesDiscovery({
|
||
success: (res) => {
|
||
console.log('停止扫描成功:', res);
|
||
resolve(res);
|
||
},
|
||
fail: (err) => {
|
||
console.warn('停止扫描失败:', err);
|
||
resolve(); // 即使失败也继续
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
// 验证是否为有效的BluFi设备
|
||
isValidBluFiDevice(device) {
|
||
if (!device.name) return false;
|
||
|
||
const validNames = [
|
||
"Airhub_Ble", // 项目默认名称
|
||
"XiaoZhi-AI" // 备用名称
|
||
];
|
||
|
||
return validNames.includes(device.name) ||
|
||
device.name.startsWith("Airhub-") ||
|
||
device.name.startsWith("XiaoZhi-");
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 4.1.2 设备扫描页面实现
|
||
```xml
|
||
<!-- pages/scan/scan.wxml -->
|
||
<view class="scan-container">
|
||
<view class="scan-header">
|
||
<text class="title">扫描BluFi设备</text>
|
||
<text class="subtitle">请确保设备处于配网模式</text>
|
||
</view>
|
||
|
||
<view class="scan-status">
|
||
<view wx:if="{{scanning}}" class="scanning">
|
||
<view class="loading-icon"></view>
|
||
<text>正在扫描设备...</text>
|
||
</view>
|
||
<button wx:else class="scan-btn" bindtap="startScan">开始扫描</button>
|
||
</view>
|
||
|
||
<view class="device-list">
|
||
<view class="device-item" wx:for="{{devices}}" wx:key="deviceId" bindtap="selectDevice" data-device="{{item}}">
|
||
<view class="device-info">
|
||
<view class="device-name">{{item.name}}</view>
|
||
<view class="device-id">{{item.deviceId}}</view>
|
||
<view class="device-signal">信号强度: {{item.RSSI}}dBm</view>
|
||
</view>
|
||
<view class="device-action">
|
||
<text class="connect-btn">连接</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view wx:if="{{devices.length === 0 && !scanning}}" class="empty-state">
|
||
<text>未发现设备,请检查设备是否开启配网模式</text>
|
||
</view>
|
||
</view>
|
||
```
|
||
|
||
```javascript
|
||
// pages/scan/scan.js
|
||
Page({
|
||
data: {
|
||
scanning: false,
|
||
devices: []
|
||
},
|
||
|
||
onLoad() {
|
||
this.blufi = new BluFiProvisioning();
|
||
this.initBluetooth();
|
||
},
|
||
|
||
async initBluetooth() {
|
||
try {
|
||
await this.blufi.initBluetooth();
|
||
console.log('蓝牙初始化完成');
|
||
} catch (error) {
|
||
wx.showToast({
|
||
title: '蓝牙初始化失败',
|
||
icon: 'error'
|
||
});
|
||
console.error('蓝牙初始化失败:', error);
|
||
}
|
||
},
|
||
|
||
async startScan() {
|
||
if (this.data.scanning) return;
|
||
|
||
this.setData({
|
||
scanning: true,
|
||
devices: []
|
||
});
|
||
|
||
try {
|
||
await this.blufi.startScan((device) => {
|
||
// 检查设备是否已存在
|
||
const exists = this.data.devices.find(d => d.deviceId === device.deviceId);
|
||
if (!exists) {
|
||
this.setData({
|
||
devices: [...this.data.devices, device]
|
||
});
|
||
}
|
||
});
|
||
|
||
// 30秒后自动停止扫描
|
||
setTimeout(() => {
|
||
this.stopScan();
|
||
}, 30000);
|
||
|
||
} catch (error) {
|
||
this.setData({ scanning: false });
|
||
wx.showToast({
|
||
title: '扫描失败',
|
||
icon: 'error'
|
||
});
|
||
console.error('扫描失败:', error);
|
||
}
|
||
},
|
||
|
||
async stopScan() {
|
||
if (!this.data.scanning) return;
|
||
|
||
try {
|
||
await this.blufi.stopScan();
|
||
this.setData({ scanning: false });
|
||
} catch (error) {
|
||
console.error('停止扫描失败:', error);
|
||
}
|
||
},
|
||
|
||
selectDevice(e) {
|
||
const device = e.currentTarget.dataset.device;
|
||
console.log('选择设备:', device);
|
||
|
||
// 停止扫描
|
||
this.stopScan();
|
||
|
||
// 跳转到连接页面
|
||
wx.navigateTo({
|
||
url: `/pages/connect/connect?deviceId=${device.deviceId}&deviceName=${device.name}`
|
||
});
|
||
},
|
||
|
||
onUnload() {
|
||
this.stopScan();
|
||
}
|
||
});
|
||
```
|
||
|
||
### 4.2 第二阶段:设备连接和GATT服务发现
|
||
|
||
#### 4.2.1 连接设备实现
|
||
```javascript
|
||
// 在BluFiProvisioning类中添加连接方法
|
||
class BluFiProvisioning {
|
||
// ... 前面的代码 ...
|
||
|
||
// 连接设备
|
||
async connectDevice(deviceId) {
|
||
try {
|
||
console.log('连接设备:', deviceId);
|
||
this.deviceId = deviceId;
|
||
|
||
// 建立BLE连接
|
||
await new Promise((resolve, reject) => {
|
||
wx.createBLEConnection({
|
||
deviceId: deviceId,
|
||
success: (res) => {
|
||
console.log('设备连接成功:', res);
|
||
this.isConnected = true;
|
||
resolve(res);
|
||
},
|
||
fail: (err) => {
|
||
console.error('设备连接失败:', err);
|
||
reject(new Error(`连接失败: ${err.errMsg}`));
|
||
}
|
||
});
|
||
});
|
||
|
||
// 发现服务
|
||
await this.discoverServices();
|
||
|
||
// 发现特征值
|
||
await this.discoverCharacteristics();
|
||
|
||
// 启用通知
|
||
await this.enableNotifications();
|
||
|
||
console.log('设备连接和初始化完成');
|
||
return true;
|
||
|
||
} catch (error) {
|
||
console.error('连接设备异常:', error);
|
||
this.isConnected = false;
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
// 自动发现GATT服务
|
||
async discoverServices() {
|
||
return new Promise((resolve, reject) => {
|
||
wx.getBLEDeviceServices({
|
||
deviceId: this.deviceId,
|
||
success: (res) => {
|
||
console.log('发现服务:', res.services);
|
||
|
||
// 查找BluFi服务
|
||
const blufiService = res.services.find(service =>
|
||
service.uuid.toUpperCase() === BLUFI_SERVICE_UUID.toUpperCase()
|
||
);
|
||
|
||
if (blufiService) {
|
||
this.serviceId = blufiService.uuid;
|
||
console.log('找到BluFi服务:', this.serviceId);
|
||
resolve(blufiService);
|
||
} else {
|
||
reject(new Error('未找到BluFi服务'));
|
||
}
|
||
},
|
||
fail: (err) => {
|
||
console.error('发现服务失败:', err);
|
||
reject(new Error(`发现服务失败: ${err.errMsg}`));
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
// 自动发现特征值
|
||
async discoverCharacteristics() {
|
||
return new Promise((resolve, reject) => {
|
||
wx.getBLEDeviceCharacteristics({
|
||
deviceId: this.deviceId,
|
||
serviceId: this.serviceId,
|
||
success: (res) => {
|
||
console.log('发现特征值:', res.characteristics);
|
||
|
||
// 查找写特征值(手机到设备)
|
||
const writeChar = res.characteristics.find(char =>
|
||
char.uuid.toUpperCase() === BLUFI_CHAR_P2E_UUID.toUpperCase()
|
||
);
|
||
|
||
// 查找通知特征值(设备到手机)
|
||
const notifyChar = res.characteristics.find(char =>
|
||
char.uuid.toUpperCase() === BLUFI_CHAR_E2P_UUID.toUpperCase()
|
||
);
|
||
|
||
if (writeChar && notifyChar) {
|
||
this.writeCharacteristicId = writeChar.uuid;
|
||
this.notifyCharacteristicId = notifyChar.uuid;
|
||
console.log('找到BluFi特征值:');
|
||
console.log('写特征值:', this.writeCharacteristicId);
|
||
console.log('通知特征值:', this.notifyCharacteristicId);
|
||
resolve({ writeChar, notifyChar });
|
||
} else {
|
||
reject(new Error('未找到BluFi特征值'));
|
||
}
|
||
},
|
||
fail: (err) => {
|
||
console.error('发现特征值失败:', err);
|
||
reject(new Error(`发现特征值失败: ${err.errMsg}`));
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
// 启用通知
|
||
async enableNotifications() {
|
||
return new Promise((resolve, reject) => {
|
||
// 监听特征值变化
|
||
wx.onBLECharacteristicValueChange((res) => {
|
||
console.log('收到设备数据:', res);
|
||
this.handleDeviceData(res.value);
|
||
});
|
||
|
||
// 启用通知
|
||
wx.notifyBLECharacteristicValueChange({
|
||
deviceId: this.deviceId,
|
||
serviceId: this.serviceId,
|
||
characteristicId: this.notifyCharacteristicId,
|
||
state: true,
|
||
success: (res) => {
|
||
console.log('启用通知成功:', res);
|
||
resolve(res);
|
||
},
|
||
fail: (err) => {
|
||
console.error('启用通知失败:', err);
|
||
reject(new Error(`启用通知失败: ${err.errMsg}`));
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
// 处理设备数据
|
||
handleDeviceData(buffer) {
|
||
try {
|
||
const packet = this.packet.parse(buffer);
|
||
console.log('解析数据包:', packet);
|
||
|
||
// 根据数据包类型处理
|
||
switch (packet.type) {
|
||
case 0x00: // 控制包
|
||
this.handleControlPacket(packet);
|
||
break;
|
||
case 0x01: // 数据包
|
||
this.handleDataPacket(packet);
|
||
break;
|
||
default:
|
||
console.warn('未知数据包类型:', packet.type);
|
||
}
|
||
} catch (error) {
|
||
console.error('处理设备数据失败:', error);
|
||
}
|
||
}
|
||
|
||
// 处理控制包
|
||
handleControlPacket(packet) {
|
||
switch (packet.subtype) {
|
||
case BLUFI_TYPE_CTRL.ACK:
|
||
console.log('收到确认包');
|
||
break;
|
||
case BLUFI_TYPE_CTRL.GET_WIFI_STATUS:
|
||
console.log('设备请求WiFi状态');
|
||
break;
|
||
default:
|
||
console.log('收到控制包:', packet.subtype);
|
||
}
|
||
}
|
||
|
||
// 处理数据包
|
||
handleDataPacket(packet) {
|
||
switch (packet.subtype) {
|
||
case BLUFI_TYPE_DATA.WIFI_REP:
|
||
this.handleWiFiReport(packet.data);
|
||
break;
|
||
case BLUFI_TYPE_DATA.WIFI_LIST:
|
||
this.handleWiFiList(packet.data);
|
||
break;
|
||
default:
|
||
console.log('收到数据包:', packet.subtype);
|
||
}
|
||
}
|
||
|
||
// 处理WiFi连接报告
|
||
handleWiFiReport(data) {
|
||
if (data.byteLength >= 2) {
|
||
const view = new DataView(data);
|
||
const status = view.getUint8(0);
|
||
const reason = view.getUint8(1);
|
||
|
||
console.log('WiFi连接报告 - 状态:', status, '原因:', reason);
|
||
|
||
if (status === 0) {
|
||
// 连接成功
|
||
this.provisioningState = 'success';
|
||
this.onProvisioningSuccess && this.onProvisioningSuccess();
|
||
} else {
|
||
// 连接失败
|
||
this.provisioningState = 'failed';
|
||
this.onProvisioningFailed && this.onProvisioningFailed(reason);
|
||
}
|
||
}
|
||
}
|
||
|
||
// 断开连接
|
||
async disconnect() {
|
||
if (!this.isConnected || !this.deviceId) return;
|
||
|
||
try {
|
||
await new Promise((resolve) => {
|
||
wx.closeBLEConnection({
|
||
deviceId: this.deviceId,
|
||
success: (res) => {
|
||
console.log('断开连接成功:', res);
|
||
resolve(res);
|
||
},
|
||
fail: (err) => {
|
||
console.warn('断开连接失败:', err);
|
||
resolve(); // 即使失败也继续
|
||
}
|
||
});
|
||
});
|
||
|
||
this.isConnected = false;
|
||
this.deviceId = null;
|
||
this.serviceId = null;
|
||
this.writeCharacteristicId = null;
|
||
this.notifyCharacteristicId = null;
|
||
|
||
} catch (error) {
|
||
console.error('断开连接异常:', error);
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 4.2.2 连接页面实现
|
||
```xml
|
||
<!-- pages/connect/connect.wxml -->
|
||
<view class="connect-container">
|
||
<view class="connect-header">
|
||
<text class="title">连接设备</text>
|
||
<view class="device-info">
|
||
<text class="device-name">{{deviceName}}</text>
|
||
<text class="device-id">{{deviceId}}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="connect-status">
|
||
<view wx:if="{{connecting}}" class="connecting">
|
||
<view class="loading-icon"></view>
|
||
<text>正在连接设备...</text>
|
||
</view>
|
||
|
||
<view wx:elif="{{connected}}" class="connected">
|
||
<view class="success-icon">✓</view>
|
||
<text>设备连接成功</text>
|
||
<button class="next-btn" bindtap="goToConfig">开始配网</button>
|
||
</view>
|
||
|
||
<view wx:else class="disconnected">
|
||
<text>设备未连接</text>
|
||
<button class="retry-btn" bindtap="connectDevice">重新连接</button>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="connect-steps">
|
||
<view class="step {{step >= 1 ? 'active' : ''}}">
|
||
<view class="step-number">1</view>
|
||
<text>建立BLE连接</text>
|
||
</view>
|
||
<view class="step {{step >= 2 ? 'active' : ''}}">
|
||
<view class="step-number">2</view>
|
||
<text>发现GATT服务</text>
|
||
</view>
|
||
<view class="step {{step >= 3 ? 'active' : ''}}">
|
||
<view class="step-number">3</view>
|
||
<text>初始化特征值</text>
|
||
</view>
|
||
<view class="step {{step >= 4 ? 'active' : ''}}">
|
||
<view class="step-number">4</view>
|
||
<text>启用数据通知</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
```
|
||
|
||
```javascript
|
||
// pages/connect/connect.js
|
||
Page({
|
||
data: {
|
||
deviceId: '',
|
||
deviceName: '',
|
||
connecting: false,
|
||
connected: false,
|
||
step: 0
|
||
},
|
||
|
||
onLoad(options) {
|
||
this.setData({
|
||
deviceId: options.deviceId,
|
||
deviceName: options.deviceName
|
||
});
|
||
|
||
this.blufi = new BluFiProvisioning();
|
||
this.connectDevice();
|
||
},
|
||
|
||
async connectDevice() {
|
||
if (this.data.connecting) return;
|
||
|
||
this.setData({
|
||
connecting: true,
|
||
connected: false,
|
||
step: 0
|
||
});
|
||
|
||
try {
|
||
// 步骤1:建立BLE连接
|
||
this.setData({ step: 1 });
|
||
await new Promise(resolve => setTimeout(resolve, 500)); // 显示进度
|
||
|
||
// 步骤2:发现GATT服务
|
||
this.setData({ step: 2 });
|
||
await new Promise(resolve => setTimeout(resolve, 500));
|
||
|
||
// 步骤3:初始化特征值
|
||
this.setData({ step: 3 });
|
||
await new Promise(resolve => setTimeout(resolve, 500));
|
||
|
||
// 步骤4:启用数据通知
|
||
this.setData({ step: 4 });
|
||
|
||
// 执行实际连接
|
||
await this.blufi.connectDevice(this.data.deviceId);
|
||
|
||
this.setData({
|
||
connecting: false,
|
||
connected: true
|
||
});
|
||
|
||
wx.showToast({
|
||
title: '连接成功',
|
||
icon: 'success'
|
||
});
|
||
|
||
} catch (error) {
|
||
this.setData({
|
||
connecting: false,
|
||
connected: false,
|
||
step: 0
|
||
});
|
||
|
||
wx.showToast({
|
||
title: '连接失败',
|
||
icon: 'error'
|
||
});
|
||
|
||
console.error('连接设备失败:', error);
|
||
}
|
||
},
|
||
|
||
goToConfig() {
|
||
if (!this.data.connected) return;
|
||
|
||
// 跳转到WiFi配置页面
|
||
wx.navigateTo({
|
||
url: '/pages/config/config'
|
||
});
|
||
},
|
||
|
||
onUnload() {
|
||
// 页面卸载时断开连接
|
||
if (this.blufi) {
|
||
this.blufi.disconnect();
|
||
}
|
||
}
|
||
});
|
||
```
|
||
|
||
### 4.3 第三阶段:WiFi扫描和网络选择
|
||
|
||
#### 4.3.1 WiFi扫描实现
|
||
```javascript
|
||
// 在BluFiProvisioning类中添加WiFi扫描方法
|
||
class BluFiProvisioning {
|
||
// ... 前面的代码 ...
|
||
|
||
// 请求WiFi扫描
|
||
async requestWiFiScan() {
|
||
try {
|
||
console.log('请求设备扫描WiFi...');
|
||
|
||
// 构建获取WiFi列表的控制包
|
||
const packet = this.packet.build(0x00, BLUFI_TYPE_CTRL.GET_WIFI_LIST);
|
||
|
||
// 发送数据包
|
||
await this.sendData(packet);
|
||
|
||
console.log('WiFi扫描请求已发送');
|
||
return true;
|
||
|
||
} catch (error) {
|
||
console.error('请求WiFi扫描失败:', error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
// 发送数据到设备
|
||
async sendData(buffer) {
|
||
if (!this.isConnected || !this.deviceId || !this.writeCharacteristicId) {
|
||
throw new Error('设备未连接或特征值未初始化');
|
||
}
|
||
|
||
return new Promise((resolve, reject) => {
|
||
wx.writeBLECharacteristicValue({
|
||
deviceId: this.deviceId,
|
||
serviceId: this.serviceId,
|
||
characteristicId: this.writeCharacteristicId,
|
||
value: buffer,
|
||
success: (res) => {
|
||
console.log('数据发送成功:', res);
|
||
resolve(res);
|
||
},
|
||
fail: (err) => {
|
||
console.error('数据发送失败:', err);
|
||
reject(new Error(`发送失败: ${err.errMsg}`));
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
// 处理WiFi列表
|
||
handleWiFiList(data) {
|
||
try {
|
||
console.log('收到WiFi列表数据:', data);
|
||
|
||
// 解析WiFi列表数据
|
||
const wifiList = this.parseWiFiList(data);
|
||
console.log('解析的WiFi列表:', wifiList);
|
||
|
||
// 触发回调
|
||
this.onWiFiListReceived && this.onWiFiListReceived(wifiList);
|
||
|
||
} catch (error) {
|
||
console.error('处理WiFi列表失败:', error);
|
||
}
|
||
}
|
||
|
||
// 解析WiFi列表数据
|
||
parseWiFiList(data) {
|
||
const wifiList = [];
|
||
const view = new DataView(data);
|
||
let offset = 0;
|
||
|
||
try {
|
||
while (offset < data.byteLength) {
|
||
// 读取SSID长度
|
||
if (offset >= data.byteLength) break;
|
||
const ssidLength = view.getUint8(offset);
|
||
offset += 1;
|
||
|
||
if (ssidLength === 0 || offset + ssidLength > data.byteLength) break;
|
||
|
||
// 读取SSID
|
||
const ssidBytes = new Uint8Array(data, offset, ssidLength);
|
||
const ssid = new TextDecoder('utf-8').decode(ssidBytes);
|
||
offset += ssidLength;
|
||
|
||
// 读取RSSI(信号强度)
|
||
if (offset >= data.byteLength) break;
|
||
const rssi = view.getInt8(offset);
|
||
offset += 1;
|
||
|
||
// 读取认证模式
|
||
if (offset >= data.byteLength) break;
|
||
const authMode = view.getUint8(offset);
|
||
offset += 1;
|
||
|
||
wifiList.push({
|
||
ssid: ssid,
|
||
rssi: rssi,
|
||
authMode: authMode,
|
||
security: this.getSecurityType(authMode)
|
||
});
|
||
}
|
||
} catch (error) {
|
||
console.error('解析WiFi列表数据异常:', error);
|
||
}
|
||
|
||
return wifiList;
|
||
}
|
||
|
||
// 获取安全类型描述
|
||
getSecurityType(authMode) {
|
||
const securityTypes = {
|
||
0: 'OPEN',
|
||
1: 'WEP',
|
||
2: 'WPA_PSK',
|
||
3: 'WPA2_PSK',
|
||
4: 'WPA_WPA2_PSK',
|
||
5: 'WPA2_ENTERPRISE',
|
||
6: 'WPA3_PSK',
|
||
7: 'WPA2_WPA3_PSK'
|
||
};
|
||
|
||
return securityTypes[authMode] || 'UNKNOWN';
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 4.3.2 WiFi配置页面实现
|
||
```xml
|
||
<!-- pages/config/config.wxml -->
|
||
<view class="config-container">
|
||
<view class="config-header">
|
||
<text class="title">WiFi配置</text>
|
||
<text class="subtitle">选择要连接的WiFi网络</text>
|
||
</view>
|
||
|
||
<!-- WiFi扫描区域 -->
|
||
<view class="scan-section">
|
||
<view class="scan-header">
|
||
<text>可用网络</text>
|
||
<button class="scan-btn" bindtap="scanWiFi" disabled="{{scanning}}">
|
||
{{scanning ? '扫描中...' : '刷新'}}
|
||
</button>
|
||
</view>
|
||
|
||
<view class="wifi-list">
|
||
<view class="wifi-item" wx:for="{{wifiList}}" wx:key="ssid" bindtap="selectWiFi" data-wifi="{{item}}">
|
||
<view class="wifi-info">
|
||
<view class="wifi-name">{{item.ssid}}</view>
|
||
<view class="wifi-details">
|
||
<text class="wifi-signal">{{item.rssi}}dBm</text>
|
||
<text class="wifi-security">{{item.security}}</text>
|
||
</view>
|
||
</view>
|
||
<view class="wifi-icon">
|
||
<text class="signal-icon">📶</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view wx:if="{{wifiList.length === 0 && !scanning}}" class="empty-wifi">
|
||
<text>未发现WiFi网络,请点击刷新重新扫描</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 手动输入区域 -->
|
||
<view class="manual-section">
|
||
<view class="section-title">或手动输入WiFi信息</view>
|
||
|
||
<form bindsubmit="submitWiFiConfig">
|
||
<view class="form-group">
|
||
<label>WiFi名称 (SSID)</label>
|
||
<input name="ssid" placeholder="请输入WiFi名称" value="{{selectedSSID}}" bindinput="onSSIDInput"/>
|
||
</view>
|
||
|
||
<view class="form-group">
|
||
<label>WiFi密码</label>
|
||
<input name="password" type="password" placeholder="请输入WiFi密码" value="{{wifiPassword}}" bindinput="onPasswordInput"/>
|
||
</view>
|
||
|
||
<view class="form-actions">
|
||
<button form-type="submit" class="config-btn" disabled="{{!canSubmit}}">
|
||
开始配网
|
||
</button>
|
||
</view>
|
||
</form>
|
||
</view>
|
||
</view>
|
||
```
|
||
|
||
```javascript
|
||
// pages/config/config.js
|
||
Page({
|
||
data: {
|
||
scanning: false,
|
||
wifiList: [],
|
||
selectedSSID: '',
|
||
wifiPassword: '',
|
||
canSubmit: false
|
||
},
|
||
|
||
onLoad() {
|
||
// 获取全局的BluFi实例
|
||
const app = getApp();
|
||
this.blufi = app.globalData.blufi;
|
||
|
||
if (!this.blufi || !this.blufi.isConnected) {
|
||
wx.showToast({
|
||
title: '设备未连接',
|
||
icon: 'error'
|
||
});
|
||
wx.navigateBack();
|
||
return;
|
||
}
|
||
|
||
// 设置WiFi列表接收回调
|
||
this.blufi.onWiFiListReceived = (wifiList) => {
|
||
console.log('收到WiFi列表:', wifiList);
|
||
this.setData({
|
||
wifiList: wifiList,
|
||
scanning: false
|
||
});
|
||
};
|
||
|
||
// 自动扫描WiFi
|
||
this.scanWiFi();
|
||
},
|
||
|
||
async scanWiFi() {
|
||
if (this.data.scanning) return;
|
||
|
||
this.setData({
|
||
scanning: true,
|
||
wifiList: []
|
||
});
|
||
|
||
try {
|
||
await this.blufi.requestWiFiScan();
|
||
|
||
// 设置超时
|
||
setTimeout(() => {
|
||
if (this.data.scanning) {
|
||
this.setData({ scanning: false });
|
||
wx.showToast({
|
||
title: '扫描超时',
|
||
icon: 'none'
|
||
});
|
||
}
|
||
}, 15000); // 15秒超时
|
||
|
||
} catch (error) {
|
||
this.setData({ scanning: false });
|
||
wx.showToast({
|
||
title: '扫描失败',
|
||
icon: 'error'
|
||
});
|
||
console.error('WiFi扫描失败:', error);
|
||
}
|
||
},
|
||
|
||
selectWiFi(e) {
|
||
const wifi = e.currentTarget.dataset.wifi;
|
||
console.log('选择WiFi:', wifi);
|
||
|
||
this.setData({
|
||
selectedSSID: wifi.ssid,
|
||
wifiPassword: '' // 清空密码
|
||
});
|
||
|
||
this.checkCanSubmit();
|
||
},
|
||
|
||
onSSIDInput(e) {
|
||
this.setData({ selectedSSID: e.detail.value });
|
||
this.checkCanSubmit();
|
||
},
|
||
|
||
onPasswordInput(e) {
|
||
this.setData({ wifiPassword: e.detail.value });
|
||
this.checkCanSubmit();
|
||
},
|
||
|
||
checkCanSubmit() {
|
||
const canSubmit = this.data.selectedSSID.trim().length > 0;
|
||
this.setData({ canSubmit });
|
||
},
|
||
|
||
submitWiFiConfig(e) {
|
||
const formData = e.detail.value;
|
||
const ssid = formData.ssid || this.data.selectedSSID;
|
||
const password = formData.password || this.data.wifiPassword;
|
||
|
||
if (!ssid.trim()) {
|
||
wx.showToast({
|
||
title: '请输入WiFi名称',
|
||
icon: 'none'
|
||
});
|
||
return;
|
||
}
|
||
|
||
console.log('提交WiFi配置:', { ssid, password: '***' });
|
||
|
||
// 跳转到配网状态页面
|
||
wx.navigateTo({
|
||
url: `/pages/status/status?ssid=${encodeURIComponent(ssid)}&password=${encodeURIComponent(password)}`
|
||
});
|
||
}
|
||
});
|
||
```
|
||
|
||
### 4.4 第四阶段:WiFi凭据传输和连接确认
|
||
|
||
#### 4.4.1 WiFi凭据发送实现
|
||
```javascript
|
||
// 在BluFiProvisioning类中添加WiFi配网方法
|
||
class BluFiProvisioning {
|
||
// ... 前面的代码 ...
|
||
|
||
// 开始WiFi配网
|
||
async startProvisioning(ssid, password) {
|
||
try {
|
||
console.log('开始WiFi配网:', ssid);
|
||
this.provisioningState = 'provisioning';
|
||
|
||
// 步骤1:发送SSID
|
||
await this.sendWiFiSSID(ssid);
|
||
await this.delay(500);
|
||
|
||
// 步骤2:发送密码(如果有)
|
||
if (password && password.trim().length > 0) {
|
||
await this.sendWiFiPassword(password);
|
||
await this.delay(500);
|
||
}
|
||
|
||
// 步骤3:发送连接命令
|
||
await this.sendConnectWiFi();
|
||
|
||
console.log('WiFi配网命令已发送,等待设备响应...');
|
||
return true;
|
||
|
||
} catch (error) {
|
||
console.error('WiFi配网失败:', error);
|
||
this.provisioningState = 'failed';
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
// 发送WiFi SSID
|
||
async sendWiFiSSID(ssid) {
|
||
console.log('发送WiFi SSID:', ssid);
|
||
|
||
const data = new TextEncoder().encode(ssid);
|
||
const packet = this.packet.build(0x01, BLUFI_TYPE_DATA.STA_SSID, data);
|
||
|
||
await this.sendData(packet);
|
||
console.log('SSID发送完成');
|
||
}
|
||
|
||
// 发送WiFi密码
|
||
async sendWiFiPassword(password) {
|
||
console.log('发送WiFi密码');
|
||
|
||
const data = new TextEncoder().encode(password);
|
||
const packet = this.packet.build(0x01, BLUFI_TYPE_DATA.STA_PASSWD, data);
|
||
|
||
await this.sendData(packet);
|
||
console.log('密码发送完成');
|
||
}
|
||
|
||
// 发送连接WiFi命令
|
||
async sendConnectWiFi() {
|
||
console.log('发送连接WiFi命令');
|
||
|
||
const packet = this.packet.build(0x00, BLUFI_TYPE_CTRL.CONNECT_WIFI);
|
||
|
||
await this.sendData(packet);
|
||
console.log('连接命令发送完成');
|
||
}
|
||
|
||
// 获取WiFi连接状态
|
||
async getWiFiStatus() {
|
||
console.log('请求WiFi连接状态');
|
||
|
||
const packet = this.packet.build(0x00, BLUFI_TYPE_CTRL.GET_WIFI_STATUS);
|
||
|
||
await this.sendData(packet);
|
||
console.log('状态请求已发送');
|
||
}
|
||
|
||
// 延迟函数
|
||
delay(ms) {
|
||
return new Promise(resolve => setTimeout(resolve, ms));
|
||
}
|
||
|
||
// 设置配网回调
|
||
setProvisioningCallbacks(callbacks) {
|
||
this.onProvisioningSuccess = callbacks.onSuccess;
|
||
this.onProvisioningFailed = callbacks.onFailed;
|
||
this.onProvisioningProgress = callbacks.onProgress;
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 4.4.2 配网状态页面实现
|
||
```xml
|
||
<!-- pages/status/status.wxml -->
|
||
<view class="status-container">
|
||
<view class="status-header">
|
||
<text class="title">配网状态</text>
|
||
<view class="wifi-info">
|
||
<text class="wifi-name">{{ssid}}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="progress-section">
|
||
<view class="progress-circle {{provisioningState}}">
|
||
<view wx:if="{{provisioningState === 'provisioning'}}" class="loading-animation"></view>
|
||
<view wx:elif="{{provisioningState === 'success'}}" class="success-icon">✓</view>
|
||
<view wx:elif="{{provisioningState === 'failed'}}" class="error-icon">✗</view>
|
||
<view wx:else class="waiting-icon">⏳</view>
|
||
</view>
|
||
|
||
<view class="status-text">
|
||
<text class="main-status">{{statusText}}</text>
|
||
<text class="sub-status">{{subStatusText}}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="steps-section">
|
||
<view class="step {{step >= 1 ? 'completed' : step === 0 ? 'active' : ''}}">
|
||
<view class="step-icon">1</view>
|
||
<text>发送WiFi名称</text>
|
||
</view>
|
||
|
||
<view class="step {{step >= 2 ? 'completed' : step === 1 ? 'active' : ''}}">
|
||
<view class="step-icon">2</view>
|
||
<text>发送WiFi密码</text>
|
||
</view>
|
||
|
||
<view class="step {{step >= 3 ? 'completed' : step === 2 ? 'active' : ''}}">
|
||
<view class="step-icon">3</view>
|
||
<text>连接WiFi网络</text>
|
||
</view>
|
||
|
||
<view class="step {{step >= 4 ? 'completed' : step === 3 ? 'active' : ''}}">
|
||
<view class="step-icon">4</view>
|
||
<text>验证网络连接</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="actions-section">
|
||
<button wx:if="{{provisioningState === 'success'}}" class="success-btn" bindtap="goHome">
|
||
配网完成
|
||
</button>
|
||
|
||
<view wx:elif="{{provisioningState === 'failed'}}" class="failed-actions">
|
||
<button class="retry-btn" bindtap="retryProvisioning">重新配网</button>
|
||
<button class="back-btn" bindtap="goBack">返回修改</button>
|
||
</view>
|
||
|
||
<button wx:elif="{{provisioningState === 'provisioning'}}" class="cancel-btn" bindtap="cancelProvisioning">
|
||
取消配网
|
||
</button>
|
||
</view>
|
||
|
||
<!-- 错误信息显示 -->
|
||
<view wx:if="{{errorMessage}}" class="error-section">
|
||
<text class="error-title">错误信息:</text>
|
||
<text class="error-message">{{errorMessage}}</text>
|
||
</view>
|
||
</view>
|
||
```
|
||
|
||
```javascript
|
||
// pages/status/status.js
|
||
Page({
|
||
data: {
|
||
ssid: '',
|
||
password: '',
|
||
provisioningState: 'waiting', // waiting, provisioning, success, failed
|
||
statusText: '准备开始配网',
|
||
subStatusText: '请稍候...',
|
||
step: 0,
|
||
errorMessage: ''
|
||
},
|
||
|
||
onLoad(options) {
|
||
this.setData({
|
||
ssid: decodeURIComponent(options.ssid || ''),
|
||
password: decodeURIComponent(options.password || '')
|
||
});
|
||
|
||
// 获取全局的BluFi实例
|
||
const app = getApp();
|
||
this.blufi = app.globalData.blufi;
|
||
|
||
if (!this.blufi || !this.blufi.isConnected) {
|
||
wx.showToast({
|
||
title: '设备未连接',
|
||
icon: 'error'
|
||
});
|
||
wx.navigateBack();
|
||
return;
|
||
}
|
||
|
||
// 设置配网回调
|
||
this.blufi.setProvisioningCallbacks({
|
||
onSuccess: () => this.onProvisioningSuccess(),
|
||
onFailed: (reason) => this.onProvisioningFailed(reason),
|
||
onProgress: (step, message) => this.onProvisioningProgress(step, message)
|
||
});
|
||
|
||
// 开始配网
|
||
this.startProvisioning();
|
||
},
|
||
|
||
async startProvisioning() {
|
||
try {
|
||
this.setData({
|
||
provisioningState: 'provisioning',
|
||
statusText: '正在配网',
|
||
subStatusText: '发送WiFi信息到设备...',
|
||
step: 0,
|
||
errorMessage: ''
|
||
});
|
||
|
||
// 步骤1:发送SSID
|
||
this.updateProgress(1, '发送WiFi名称...');
|
||
await this.delay(1000);
|
||
|
||
// 步骤2:发送密码
|
||
this.updateProgress(2, '发送WiFi密码...');
|
||
await this.delay(1000);
|
||
|
||
// 步骤3:连接WiFi
|
||
this.updateProgress(3, '设备连接WiFi网络...');
|
||
|
||
// 执行实际配网
|
||
await this.blufi.startProvisioning(this.data.ssid, this.data.password);
|
||
|
||
// 等待连接结果
|
||
this.updateProgress(4, '等待连接结果...');
|
||
|
||
// 设置超时检查
|
||
this.timeoutTimer = setTimeout(() => {
|
||
if (this.data.provisioningState === 'provisioning') {
|
||
this.onProvisioningFailed('连接超时');
|
||
}
|
||
}, 30000); // 30秒超时
|
||
|
||
} catch (error) {
|
||
console.error('配网过程异常:', error);
|
||
this.onProvisioningFailed(error.message || '配网失败');
|
||
}
|
||
},
|
||
|
||
updateProgress(step, message) {
|
||
this.setData({
|
||
step: step,
|
||
subStatusText: message
|
||
});
|
||
},
|
||
|
||
onProvisioningSuccess() {
|
||
console.log('配网成功');
|
||
|
||
if (this.timeoutTimer) {
|
||
clearTimeout(this.timeoutTimer);
|
||
this.timeoutTimer = null;
|
||
}
|
||
|
||
this.setData({
|
||
provisioningState: 'success',
|
||
statusText: '配网成功',
|
||
subStatusText: '设备已成功连接到WiFi网络',
|
||
step: 4
|
||
});
|
||
|
||
wx.showToast({
|
||
title: '配网成功',
|
||
icon: 'success'
|
||
});
|
||
},
|
||
|
||
onProvisioningFailed(reason) {
|
||
console.error('配网失败:', reason);
|
||
|
||
if (this.timeoutTimer) {
|
||
clearTimeout(this.timeoutTimer);
|
||
this.timeoutTimer = null;
|
||
}
|
||
|
||
this.setData({
|
||
provisioningState: 'failed',
|
||
statusText: '配网失败',
|
||
subStatusText: '设备连接WiFi失败',
|
||
errorMessage: reason || '未知错误'
|
||
});
|
||
|
||
wx.showToast({
|
||
title: '配网失败',
|
||
icon: 'error'
|
||
});
|
||
},
|
||
|
||
onProvisioningProgress(step, message) {
|
||
this.updateProgress(step, message);
|
||
},
|
||
|
||
retryProvisioning() {
|
||
this.startProvisioning();
|
||
},
|
||
|
||
goBack() {
|
||
wx.navigateBack();
|
||
},
|
||
|
||
goHome() {
|
||
wx.reLaunch({
|
||
url: '/pages/index/index'
|
||
});
|
||
},
|
||
|
||
cancelProvisioning() {
|
||
if (this.timeoutTimer) {
|
||
clearTimeout(this.timeoutTimer);
|
||
this.timeoutTimer = null;
|
||
}
|
||
|
||
this.setData({
|
||
provisioningState: 'waiting',
|
||
statusText: '配网已取消',
|
||
subStatusText: '用户取消了配网操作',
|
||
step: 0
|
||
});
|
||
},
|
||
|
||
delay(ms) {
|
||
return new Promise(resolve => setTimeout(resolve, ms));
|
||
},
|
||
|
||
onUnload() {
|
||
if (this.timeoutTimer) {
|
||
clearTimeout(this.timeoutTimer);
|
||
}
|
||
}
|
||
});
|
||
```
|
||
|
||
## 5. 小程序项目结构
|
||
|
||
### 5.1 目录结构
|
||
```
|
||
blufi-miniprogram/
|
||
├── app.js // 小程序入口文件
|
||
├── app.json // 小程序配置文件
|
||
├── app.wxss // 全局样式文件
|
||
├── project.config.json // 项目配置文件
|
||
├── pages/ // 页面目录
|
||
│ ├── index/ // 首页
|
||
│ │ ├── index.js
|
||
│ │ ├── index.wxml
|
||
│ │ └── index.wxss
|
||
│ ├── scan/ // 设备扫描页面
|
||
│ │ ├── scan.js
|
||
│ │ ├── scan.wxml
|
||
│ │ └── scan.wxss
|
||
│ ├── connect/ // 设备连接页面
|
||
│ │ ├── connect.js
|
||
│ │ ├── connect.wxml
|
||
│ │ └── connect.wxss
|
||
│ ├── config/ // WiFi配置页面
|
||
│ │ ├── config.js
|
||
│ │ ├── config.wxml
|
||
│ │ └── config.wxss
|
||
│ └── status/ // 配网状态页面
|
||
│ ├── status.js
|
||
│ ├── status.wxml
|
||
│ └── status.wxss
|
||
├── utils/ // 工具类目录
|
||
│ ├── blufi.js // BluFi协议实现
|
||
│ ├── bluetooth.js // 蓝牙工具类
|
||
│ └── util.js // 通用工具函数
|
||
└── components/ // 组件目录
|
||
├── loading/ // 加载组件
|
||
└── device-item/ // 设备列表项组件
|
||
```
|
||
|
||
### 5.2 小程序配置文件
|
||
|
||
#### 5.2.1 app.json
|
||
```json
|
||
{
|
||
"pages": [
|
||
"pages/index/index",
|
||
"pages/scan/scan",
|
||
"pages/connect/connect",
|
||
"pages/config/config",
|
||
"pages/status/status"
|
||
],
|
||
"window": {
|
||
"backgroundTextStyle": "light",
|
||
"navigationBarBackgroundColor": "#fff",
|
||
"navigationBarTitleText": "BluFi配网",
|
||
"navigationBarTextStyle": "black",
|
||
"backgroundColor": "#f8f8f8"
|
||
},
|
||
"permission": {
|
||
"scope.bluetooth": {
|
||
"desc": "用于连接BluFi设备进行WiFi配网"
|
||
}
|
||
},
|
||
"requiredBackgroundModes": ["bluetooth-central"],
|
||
"style": "v2",
|
||
"sitemapLocation": "sitemap.json"
|
||
}
|
||
```
|
||
|
||
#### 5.2.2 project.config.json
|
||
```json
|
||
{
|
||
"description": "BluFi蓝牙配网小程序",
|
||
"packOptions": {
|
||
"ignore": []
|
||
},
|
||
"setting": {
|
||
"urlCheck": false,
|
||
"es6": true,
|
||
"enhance": true,
|
||
"postcss": true,
|
||
"preloadBackgroundData": false,
|
||
"minified": true,
|
||
"newFeature": false,
|
||
"coverView": true,
|
||
"nodeModules": false,
|
||
"autoAudits": false,
|
||
"showShadowRootInWxmlPanel": true,
|
||
"scopeDataCheck": false,
|
||
"uglifyFileName": false,
|
||
"checkInvalidKey": true,
|
||
"checkSiteMap": true,
|
||
"uploadWithSourceMap": true,
|
||
"compileHotReLoad": false,
|
||
"lazyloadPlaceholderEnable": false,
|
||
"useMultiFrameRuntime": true,
|
||
"useApiHook": true,
|
||
"useApiHostProcess": true,
|
||
"babelSetting": {
|
||
"ignore": [],
|
||
"disablePlugins": [],
|
||
"outputPath": ""
|
||
},
|
||
"enableEngineNative": false,
|
||
"useIsolateContext": true,
|
||
"userConfirmedBundleSwitch": false,
|
||
"packNpmManually": false,
|
||
"packNpmRelationList": [],
|
||
"minifyWXSS": true,
|
||
"disableUseStrict": false,
|
||
"minifyWXML": true,
|
||
"showES6CompileOption": false,
|
||
"useCompilerPlugins": false
|
||
},
|
||
"compileType": "miniprogram",
|
||
"libVersion": "2.19.4",
|
||
"appid": "your_app_id",
|
||
"projectname": "blufi-provisioning",
|
||
"debugOptions": {
|
||
"hidedInDevtools": []
|
||
},
|
||
"scripts": {},
|
||
"staticServerOptions": {
|
||
"baseURL": "",
|
||
"servePath": ""
|
||
},
|
||
"isGameTourist": false,
|
||
"condition": {
|
||
"search": {
|
||
"list": []
|
||
},
|
||
"conversation": {
|
||
"list": []
|
||
},
|
||
"game": {
|
||
"list": []
|
||
},
|
||
"plugin": {
|
||
"list": []
|
||
},
|
||
"gamePlugin": {
|
||
"list": []
|
||
},
|
||
"miniprogram": {
|
||
"list": []
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
### 5.3 全局应用文件
|
||
|
||
#### 5.3.1 app.js
|
||
```javascript
|
||
// app.js
|
||
App({
|
||
globalData: {
|
||
blufi: null, // 全局BluFi实例
|
||
deviceInfo: null, // 当前连接的设备信息
|
||
wifiConfig: null // WiFi配置信息
|
||
},
|
||
|
||
onLaunch() {
|
||
console.log('BluFi配网小程序启动');
|
||
|
||
// 检查蓝牙支持
|
||
this.checkBluetoothSupport();
|
||
|
||
// 初始化全局数据
|
||
this.initGlobalData();
|
||
},
|
||
|
||
checkBluetoothSupport() {
|
||
wx.getSystemInfo({
|
||
success: (res) => {
|
||
console.log('系统信息:', res);
|
||
|
||
// 检查是否支持蓝牙
|
||
if (!wx.openBluetoothAdapter) {
|
||
wx.showModal({
|
||
title: '提示',
|
||
content: '当前微信版本过低,无法使用蓝牙功能,请升级到最新微信版本后重试。',
|
||
showCancel: false
|
||
});
|
||
}
|
||
}
|
||
});
|
||
},
|
||
|
||
initGlobalData() {
|
||
// 初始化BluFi实例
|
||
const BluFiProvisioning = require('./utils/blufi.js');
|
||
this.globalData.blufi = new BluFiProvisioning();
|
||
},
|
||
|
||
onShow() {
|
||
console.log('小程序显示');
|
||
},
|
||
|
||
onHide() {
|
||
console.log('小程序隐藏');
|
||
},
|
||
|
||
onError(error) {
|
||
console.error('小程序错误:', error);
|
||
}
|
||
});
|
||
```
|
||
|
||
## 6. 错误处理和重试机制
|
||
|
||
### 6.1 错误码定义
|
||
```javascript
|
||
// utils/error-codes.js
|
||
const BluFiErrorCodes = {
|
||
// 蓝牙相关错误
|
||
BLUETOOTH_NOT_AVAILABLE: {
|
||
code: 1001,
|
||
message: '蓝牙不可用,请检查蓝牙是否开启'
|
||
},
|
||
BLUETOOTH_ADAPTER_INIT_FAILED: {
|
||
code: 1002,
|
||
message: '蓝牙适配器初始化失败'
|
||
},
|
||
DEVICE_SCAN_FAILED: {
|
||
code: 1003,
|
||
message: '设备扫描失败'
|
||
},
|
||
DEVICE_NOT_FOUND: {
|
||
code: 1004,
|
||
message: '未发现BluFi设备'
|
||
},
|
||
|
||
// 连接相关错误
|
||
CONNECTION_FAILED: {
|
||
code: 2001,
|
||
message: '设备连接失败'
|
||
},
|
||
CONNECTION_TIMEOUT: {
|
||
code: 2002,
|
||
message: '设备连接超时'
|
||
},
|
||
SERVICE_NOT_FOUND: {
|
||
code: 2003,
|
||
message: '未找到BluFi服务'
|
||
},
|
||
CHARACTERISTIC_NOT_FOUND: {
|
||
code: 2004,
|
||
message: '未找到BluFi特征值'
|
||
},
|
||
NOTIFICATION_ENABLE_FAILED: {
|
||
code: 2005,
|
||
message: '启用通知失败'
|
||
},
|
||
|
||
// 配网相关错误
|
||
WIFI_SCAN_FAILED: {
|
||
code: 3001,
|
||
message: 'WiFi扫描失败'
|
||
},
|
||
WIFI_SCAN_TIMEOUT: {
|
||
code: 3002,
|
||
message: 'WiFi扫描超时'
|
||
},
|
||
WIFI_CREDENTIALS_INVALID: {
|
||
code: 3003,
|
||
message: 'WiFi凭据无效'
|
||
},
|
||
WIFI_CONNECTION_FAILED: {
|
||
code: 3004,
|
||
message: 'WiFi连接失败'
|
||
},
|
||
WIFI_CONNECTION_TIMEOUT: {
|
||
code: 3005,
|
||
message: 'WiFi连接超时'
|
||
},
|
||
PROVISIONING_TIMEOUT: {
|
||
code: 3006,
|
||
message: '配网超时'
|
||
},
|
||
|
||
// 数据传输错误
|
||
DATA_SEND_FAILED: {
|
||
code: 4001,
|
||
message: '数据发送失败'
|
||
},
|
||
DATA_PARSE_FAILED: {
|
||
code: 4002,
|
||
message: '数据解析失败'
|
||
},
|
||
CHECKSUM_ERROR: {
|
||
code: 4003,
|
||
message: '数据校验失败'
|
||
},
|
||
|
||
// 通用错误
|
||
UNKNOWN_ERROR: {
|
||
code: 9999,
|
||
message: '未知错误'
|
||
}
|
||
};
|
||
|
||
module.exports = BluFiErrorCodes;
|
||
```
|
||
|
||
### 6.2 重试机制实现
|
||
```javascript
|
||
// utils/retry.js
|
||
class RetryManager {
|
||
constructor(options = {}) {
|
||
this.maxRetries = options.maxRetries || 3;
|
||
this.retryDelay = options.retryDelay || 2000;
|
||
this.backoffMultiplier = options.backoffMultiplier || 1.5;
|
||
this.maxDelay = options.maxDelay || 10000;
|
||
}
|
||
|
||
async execute(operation, context = '') {
|
||
let lastError;
|
||
let currentDelay = this.retryDelay;
|
||
|
||
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
|
||
try {
|
||
console.log(`${context} - 尝试 ${attempt + 1}/${this.maxRetries + 1}`);
|
||
const result = await operation();
|
||
|
||
if (attempt > 0) {
|
||
console.log(`${context} - 重试成功`);
|
||
}
|
||
|
||
return result;
|
||
|
||
} catch (error) {
|
||
lastError = error;
|
||
console.error(`${context} - 尝试 ${attempt + 1} 失败:`, error);
|
||
|
||
// 如果是最后一次尝试,直接抛出错误
|
||
if (attempt === this.maxRetries) {
|
||
break;
|
||
}
|
||
|
||
// 等待后重试
|
||
console.log(`${context} - ${currentDelay}ms 后重试`);
|
||
await this.delay(currentDelay);
|
||
|
||
// 增加延迟时间(指数退避)
|
||
currentDelay = Math.min(
|
||
currentDelay * this.backoffMultiplier,
|
||
this.maxDelay
|
||
);
|
||
}
|
||
}
|
||
|
||
console.error(`${context} - 所有重试都失败了`);
|
||
throw lastError;
|
||
}
|
||
|
||
delay(ms) {
|
||
return new Promise(resolve => setTimeout(resolve, ms));
|
||
}
|
||
}
|
||
|
||
module.exports = RetryManager;
|
||
```
|
||
|
||
### 6.3 错误处理工具类
|
||
```javascript
|
||
// utils/error-handler.js
|
||
const BluFiErrorCodes = require('./error-codes.js');
|
||
|
||
class ErrorHandler {
|
||
static handleError(error, context = '') {
|
||
console.error(`错误处理 [${context}]:`, error);
|
||
|
||
let errorInfo = {
|
||
code: BluFiErrorCodes.UNKNOWN_ERROR.code,
|
||
message: BluFiErrorCodes.UNKNOWN_ERROR.message,
|
||
detail: error.message || error.errMsg || '未知错误'
|
||
};
|
||
|
||
// 根据错误信息匹配错误码
|
||
if (error.errMsg) {
|
||
errorInfo = this.mapWxErrorToCode(error.errMsg);
|
||
} else if (error.code) {
|
||
errorInfo = this.findErrorByCode(error.code);
|
||
} else if (error.message) {
|
||
errorInfo = this.mapMessageToCode(error.message);
|
||
}
|
||
|
||
return errorInfo;
|
||
}
|
||
|
||
static mapWxErrorToCode(errMsg) {
|
||
const errorMappings = {
|
||
'bluetooth not available': BluFiErrorCodes.BLUETOOTH_NOT_AVAILABLE,
|
||
'bluetooth adapter init fail': BluFiErrorCodes.BLUETOOTH_ADAPTER_INIT_FAILED,
|
||
'createBLEConnection:fail': BluFiErrorCodes.CONNECTION_FAILED,
|
||
'getBLEDeviceServices:fail': BluFiErrorCodes.SERVICE_NOT_FOUND,
|
||
'getBLEDeviceCharacteristics:fail': BluFiErrorCodes.CHARACTERISTIC_NOT_FOUND,
|
||
'notifyBLECharacteristicValueChange:fail': BluFiErrorCodes.NOTIFICATION_ENABLE_FAILED,
|
||
'writeBLECharacteristicValue:fail': BluFiErrorCodes.DATA_SEND_FAILED
|
||
};
|
||
|
||
for (const [key, errorCode] of Object.entries(errorMappings)) {
|
||
if (errMsg.includes(key)) {
|
||
return {
|
||
code: errorCode.code,
|
||
message: errorCode.message,
|
||
detail: errMsg
|
||
};
|
||
}
|
||
}
|
||
|
||
return {
|
||
code: BluFiErrorCodes.UNKNOWN_ERROR.code,
|
||
message: BluFiErrorCodes.UNKNOWN_ERROR.message,
|
||
detail: errMsg
|
||
};
|
||
}
|
||
|
||
static findErrorByCode(code) {
|
||
for (const errorCode of Object.values(BluFiErrorCodes)) {
|
||
if (errorCode.code === code) {
|
||
return {
|
||
code: errorCode.code,
|
||
message: errorCode.message,
|
||
detail: ''
|
||
};
|
||
}
|
||
}
|
||
|
||
return {
|
||
code: BluFiErrorCodes.UNKNOWN_ERROR.code,
|
||
message: BluFiErrorCodes.UNKNOWN_ERROR.message,
|
||
detail: `错误码: ${code}`
|
||
};
|
||
}
|
||
|
||
static mapMessageToCode(message) {
|
||
const messageMappings = {
|
||
'蓝牙不可用': BluFiErrorCodes.BLUETOOTH_NOT_AVAILABLE,
|
||
'设备未连接': BluFiErrorCodes.CONNECTION_FAILED,
|
||
'连接超时': BluFiErrorCodes.CONNECTION_TIMEOUT,
|
||
'配网超时': BluFiErrorCodes.PROVISIONING_TIMEOUT,
|
||
'WiFi连接失败': BluFiErrorCodes.WIFI_CONNECTION_FAILED
|
||
};
|
||
|
||
for (const [key, errorCode] of Object.entries(messageMappings)) {
|
||
if (message.includes(key)) {
|
||
return {
|
||
code: errorCode.code,
|
||
message: errorCode.message,
|
||
detail: message
|
||
};
|
||
}
|
||
}
|
||
|
||
return {
|
||
code: BluFiErrorCodes.UNKNOWN_ERROR.code,
|
||
message: BluFiErrorCodes.UNKNOWN_ERROR.message,
|
||
detail: message
|
||
};
|
||
}
|
||
|
||
static showError(errorInfo, options = {}) {
|
||
const title = options.title || '错误';
|
||
const showDetail = options.showDetail !== false;
|
||
|
||
let content = errorInfo.message;
|
||
if (showDetail && errorInfo.detail) {
|
||
content += `\n\n详细信息: ${errorInfo.detail}`;
|
||
}
|
||
|
||
wx.showModal({
|
||
title: title,
|
||
content: content,
|
||
showCancel: false,
|
||
confirmText: '确定'
|
||
});
|
||
}
|
||
|
||
static showToast(errorInfo) {
|
||
wx.showToast({
|
||
title: errorInfo.message,
|
||
icon: 'error',
|
||
duration: 3000
|
||
});
|
||
}
|
||
}
|
||
|
||
module.exports = ErrorHandler;
|
||
```
|
||
|
||
## 7. 安全机制和数据保护
|
||
|
||
### 7.1 数据加密(可选)
|
||
```javascript
|
||
// utils/encryption.js
|
||
class BluFiEncryption {
|
||
constructor(options = {}) {
|
||
this.enabled = options.enabled || false;
|
||
this.algorithm = options.algorithm || 'AES-128-CFB';
|
||
this.key = options.key || null;
|
||
this.iv = options.iv || null;
|
||
}
|
||
|
||
// 设置加密密钥
|
||
setKey(key) {
|
||
this.key = key;
|
||
}
|
||
|
||
// 设置初始化向量
|
||
setIV(iv) {
|
||
this.iv = iv;
|
||
}
|
||
|
||
// 加密数据
|
||
encrypt(data) {
|
||
if (!this.enabled || !this.key) {
|
||
return data;
|
||
}
|
||
|
||
try {
|
||
// 这里应该实现实际的AES加密
|
||
// 由于微信小程序环境限制,可能需要使用第三方加密库
|
||
console.log('数据加密(模拟)');
|
||
return data; // 返回加密后的数据
|
||
} catch (error) {
|
||
console.error('数据加密失败:', error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
// 解密数据
|
||
decrypt(encryptedData) {
|
||
if (!this.enabled || !this.key) {
|
||
return encryptedData;
|
||
}
|
||
|
||
try {
|
||
// 这里应该实现实际的AES解密
|
||
console.log('数据解密(模拟)');
|
||
return encryptedData; // 返回解密后的数据
|
||
} catch (error) {
|
||
console.error('数据解密失败:', error);
|
||
throw error;
|
||
}
|
||
}
|
||
}
|
||
|
||
module.exports = BluFiEncryption;
|
||
```
|
||
|
||
### 7.2 数据校验
|
||
```javascript
|
||
// utils/checksum.js
|
||
class ChecksumCalculator {
|
||
// 计算简单校验和
|
||
static calculateSimpleChecksum(data) {
|
||
let checksum = 0;
|
||
const view = new Uint8Array(data);
|
||
|
||
for (let i = 0; i < view.length; i++) {
|
||
checksum += view[i];
|
||
}
|
||
|
||
return checksum & 0xFFFF;
|
||
}
|
||
|
||
// 验证校验和
|
||
static verifyChecksum(data, expectedChecksum) {
|
||
const calculatedChecksum = this.calculateSimpleChecksum(data);
|
||
return calculatedChecksum === expectedChecksum;
|
||
}
|
||
|
||
// CRC16校验(可选)
|
||
static calculateCRC16(data) {
|
||
let crc = 0xFFFF;
|
||
const polynomial = 0x1021;
|
||
|
||
const view = new Uint8Array(data);
|
||
|
||
for (let i = 0; i < view.length; i++) {
|
||
crc ^= (view[i] << 8);
|
||
|
||
for (let j = 0; j < 8; j++) {
|
||
if (crc & 0x8000) {
|
||
crc = (crc << 1) ^ polynomial;
|
||
} else {
|
||
crc <<= 1;
|
||
}
|
||
crc &= 0xFFFF;
|
||
}
|
||
}
|
||
|
||
return crc;
|
||
}
|
||
}
|
||
|
||
module.exports = ChecksumCalculator;
|
||
```
|
||
|
||
### 7.3 敏感数据处理
|
||
```javascript
|
||
// utils/security.js
|
||
class SecurityManager {
|
||
// 清理敏感数据
|
||
static clearSensitiveData() {
|
||
// 清理WiFi密码等敏感信息
|
||
const app = getApp();
|
||
if (app.globalData.wifiConfig) {
|
||
app.globalData.wifiConfig.password = null;
|
||
}
|
||
|
||
// 清理本地存储中的敏感数据
|
||
try {
|
||
wx.removeStorageSync('wifi_password');
|
||
wx.removeStorageSync('encryption_key');
|
||
} catch (error) {
|
||
console.warn('清理本地存储失败:', error);
|
||
}
|
||
}
|
||
|
||
// 验证WiFi凭据格式
|
||
static validateWiFiCredentials(ssid, password) {
|
||
const errors = [];
|
||
|
||
// SSID验证
|
||
if (!ssid || ssid.trim().length === 0) {
|
||
errors.push('WiFi名称不能为空');
|
||
} else if (ssid.length > 32) {
|
||
errors.push('WiFi名称长度不能超过32个字符');
|
||
}
|
||
|
||
// 密码验证(可选)
|
||
if (password && password.length > 64) {
|
||
errors.push('WiFi密码长度不能超过64个字符');
|
||
}
|
||
|
||
return {
|
||
valid: errors.length === 0,
|
||
errors: errors
|
||
};
|
||
}
|
||
|
||
// 生成随机序列号
|
||
static generateSequenceNumber() {
|
||
return Math.floor(Math.random() * 65536);
|
||
}
|
||
}
|
||
|
||
module.exports = SecurityManager;
|
||
```
|
||
|
||
## 8. 测试和调试
|
||
|
||
### 8.1 调试工具
|
||
```javascript
|
||
// utils/debug.js
|
||
class DebugManager {
|
||
constructor() {
|
||
this.enabled = true; // 生产环境应设为false
|
||
this.logLevel = 'DEBUG'; // DEBUG, INFO, WARN, ERROR
|
||
this.logs = [];
|
||
this.maxLogs = 1000;
|
||
}
|
||
|
||
log(level, message, data = null) {
|
||
if (!this.enabled) return;
|
||
|
||
const timestamp = new Date().toISOString();
|
||
const logEntry = {
|
||
timestamp,
|
||
level,
|
||
message,
|
||
data
|
||
};
|
||
|
||
// 添加到日志数组
|
||
this.logs.push(logEntry);
|
||
|
||
// 限制日志数量
|
||
if (this.logs.length > this.maxLogs) {
|
||
this.logs.shift();
|
||
}
|
||
|
||
// 控制台输出
|
||
const logMessage = `[${timestamp}] ${level}: ${message}`;
|
||
switch (level) {
|
||
case 'DEBUG':
|
||
console.log(logMessage, data);
|
||
break;
|
||
case 'INFO':
|
||
console.info(logMessage, data);
|
||
break;
|
||
case 'WARN':
|
||
console.warn(logMessage, data);
|
||
break;
|
||
case 'ERROR':
|
||
console.error(logMessage, data);
|
||
break;
|
||
}
|
||
}
|
||
|
||
debug(message, data) {
|
||
this.log('DEBUG', message, data);
|
||
}
|
||
|
||
info(message, data) {
|
||
this.log('INFO', message, data);
|
||
}
|
||
|
||
warn(message, data) {
|
||
this.log('WARN', message, data);
|
||
}
|
||
|
||
error(message, data) {
|
||
this.log('ERROR', message, data);
|
||
}
|
||
|
||
// 导出日志
|
||
exportLogs() {
|
||
return JSON.stringify(this.logs, null, 2);
|
||
}
|
||
|
||
// 清空日志
|
||
clearLogs() {
|
||
this.logs = [];
|
||
}
|
||
|
||
// 获取最近的错误日志
|
||
getRecentErrors(count = 10) {
|
||
return this.logs
|
||
.filter(log => log.level === 'ERROR')
|
||
.slice(-count);
|
||
}
|
||
}
|
||
|
||
// 创建全局调试实例
|
||
const debugManager = new DebugManager();
|
||
|
||
module.exports = debugManager;
|
||
```
|
||
|
||
### 8.2 测试用例
|
||
```javascript
|
||
// test/blufi-test.js
|
||
class BluFiTest {
|
||
constructor() {
|
||
this.testResults = [];
|
||
}
|
||
|
||
// 运行所有测试
|
||
async runAllTests() {
|
||
console.log('开始BluFi测试...');
|
||
|
||
await this.testBluetoothInit();
|
||
await this.testDeviceScan();
|
||
await this.testPacketParsing();
|
||
await this.testChecksumCalculation();
|
||
|
||
this.printTestResults();
|
||
}
|
||
|
||
// 测试蓝牙初始化
|
||
async testBluetoothInit() {
|
||
try {
|
||
const blufi = new BluFiProvisioning();
|
||
await blufi.initBluetooth();
|
||
|
||
this.addTestResult('蓝牙初始化', true, '成功');
|
||
} catch (error) {
|
||
this.addTestResult('蓝牙初始化', false, error.message);
|
||
}
|
||
}
|
||
|
||
// 测试设备扫描
|
||
async testDeviceScan() {
|
||
try {
|
||
const blufi = new BluFiProvisioning();
|
||
|
||
let deviceFound = false;
|
||
await blufi.startScan((device) => {
|
||
if (blufi.isValidBluFiDevice(device)) {
|
||
deviceFound = true;
|
||
}
|
||
});
|
||
|
||
// 等待5秒
|
||
await new Promise(resolve => setTimeout(resolve, 5000));
|
||
await blufi.stopScan();
|
||
|
||
this.addTestResult('设备扫描', deviceFound, deviceFound ? '发现设备' : '未发现设备');
|
||
} catch (error) {
|
||
this.addTestResult('设备扫描', false, error.message);
|
||
}
|
||
}
|
||
|
||
// 测试数据包解析
|
||
testPacketParsing() {
|
||
try {
|
||
const packet = new BluFiPacket();
|
||
|
||
// 构建测试数据包
|
||
const testData = new TextEncoder().encode('test');
|
||
const buffer = packet.build(0x01, 0x02, testData);
|
||
|
||
// 解析数据包
|
||
const parsed = packet.parse(buffer);
|
||
|
||
const success = parsed.type === 0x01 &&
|
||
parsed.subtype === 0x02 &&
|
||
new TextDecoder().decode(parsed.data) === 'test';
|
||
|
||
this.addTestResult('数据包解析', success, success ? '解析正确' : '解析错误');
|
||
} catch (error) {
|
||
this.addTestResult('数据包解析', false, error.message);
|
||
}
|
||
}
|
||
|
||
// 测试校验和计算
|
||
testChecksumCalculation() {
|
||
try {
|
||
const testData = new Uint8Array([0x01, 0x02, 0x03, 0x04]);
|
||
const checksum = ChecksumCalculator.calculateSimpleChecksum(testData.buffer);
|
||
|
||
// 预期校验和: 1+2+3+4 = 10
|
||
const expected = 10;
|
||
const success = checksum === expected;
|
||
|
||
this.addTestResult('校验和计算', success,
|
||
success ? `校验和正确: ${checksum}` : `校验和错误: 期望${expected}, 实际${checksum}`);
|
||
} catch (error) {
|
||
this.addTestResult('校验和计算', false, error.message);
|
||
}
|
||
}
|
||
|
||
addTestResult(testName, success, message) {
|
||
this.testResults.push({
|
||
name: testName,
|
||
success: success,
|
||
message: message,
|
||
timestamp: new Date().toISOString()
|
||
});
|
||
}
|
||
|
||
printTestResults() {
|
||
console.log('\n=== BluFi测试结果 ===');
|
||
|
||
let passCount = 0;
|
||
let totalCount = this.testResults.length;
|
||
|
||
this.testResults.forEach(result => {
|
||
const status = result.success ? '✓ 通过' : '✗ 失败';
|
||
console.log(`${status} ${result.name}: ${result.message}`);
|
||
|
||
if (result.success) {
|
||
passCount++;
|
||
}
|
||
});
|
||
|
||
console.log(`\n总计: ${passCount}/${totalCount} 个测试通过`);
|
||
|
||
if (passCount === totalCount) {
|
||
console.log('🎉 所有测试都通过了!');
|
||
} else {
|
||
console.log('❌ 部分测试失败,请检查相关功能');
|
||
}
|
||
}
|
||
}
|
||
|
||
module.exports = BluFiTest;
|
||
```
|
||
|
||
## 9. 性能优化
|
||
|
||
### 9.1 内存管理
|
||
```javascript
|
||
// utils/memory-manager.js
|
||
class MemoryManager {
|
||
static clearUnusedData() {
|
||
// 清理不再使用的设备列表
|
||
const app = getApp();
|
||
if (app.globalData.deviceList) {
|
||
app.globalData.deviceList = [];
|
||
}
|
||
|
||
// 清理WiFi列表缓存
|
||
if (app.globalData.wifiList) {
|
||
app.globalData.wifiList = [];
|
||
}
|
||
|
||
// 强制垃圾回收(如果支持)
|
||
if (typeof wx.triggerGC === 'function') {
|
||
wx.triggerGC();
|
||
}
|
||
}
|
||
|
||
static monitorMemoryUsage() {
|
||
if (typeof wx.getPerformance === 'function') {
|
||
const performance = wx.getPerformance();
|
||
if (performance.memory) {
|
||
console.log('内存使用情况:', {
|
||
used: performance.memory.usedJSHeapSize,
|
||
total: performance.memory.totalJSHeapSize,
|
||
limit: performance.memory.jsHeapSizeLimit
|
||
});
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
module.exports = MemoryManager;
|
||
```
|
||
|
||
### 9.2 连接优化
|
||
```javascript
|
||
// utils/connection-optimizer.js
|
||
class ConnectionOptimizer {
|
||
constructor() {
|
||
this.connectionPool = new Map();
|
||
this.maxConnections = 1; // BluFi通常只需要一个连接
|
||
}
|
||
|
||
// 优化连接参数
|
||
getOptimalConnectionParams() {
|
||
return {
|
||
timeout: 15000, // 15秒连接超时
|
||
interval: 100, // 100ms扫描间隔
|
||
allowDuplicatesKey: false,
|
||
services: [BLUFI_SERVICE_UUID]
|
||
};
|
||
}
|
||
|
||
// 连接重用
|
||
reuseConnection(deviceId) {
|
||
if (this.connectionPool.has(deviceId)) {
|
||
const connection = this.connectionPool.get(deviceId);
|
||
if (connection.isValid) {
|
||
return connection;
|
||
} else {
|
||
this.connectionPool.delete(deviceId);
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
// 添加连接到池
|
||
addConnection(deviceId, connection) {
|
||
// 如果池满了,移除最旧的连接
|
||
if (this.connectionPool.size >= this.maxConnections) {
|
||
const firstKey = this.connectionPool.keys().next().value;
|
||
this.connectionPool.delete(firstKey);
|
||
}
|
||
|
||
this.connectionPool.set(deviceId, connection);
|
||
}
|
||
|
||
// 清理连接池
|
||
clearPool() {
|
||
this.connectionPool.clear();
|
||
}
|
||
}
|
||
|
||
module.exports = ConnectionOptimizer;
|
||
```
|
||
|
||
## 10. 部署和发布
|
||
|
||
### 10.1 发布前检查清单
|
||
- [ ] 所有功能测试通过
|
||
- [ ] 与ESP官方espblufi应用对比测试完成
|
||
- [ ] 错误处理机制完善
|
||
- [ ] 用户界面友好性检查
|
||
- [ ] 性能测试通过
|
||
- [ ] 安全性检查完成
|
||
- [ ] 代码审查完成
|
||
- [ ] 文档更新完成
|
||
|
||
### 10.2 版本管理
|
||
```javascript
|
||
// utils/version.js
|
||
const VERSION_INFO = {
|
||
version: '1.0.0',
|
||
buildNumber: '20240101',
|
||
releaseDate: '2024-01-01',
|
||
features: [
|
||
'BluFi设备扫描和连接',
|
||
'WiFi网络配置',
|
||
'配网状态监控',
|
||
'错误处理和重试机制'
|
||
],
|
||
compatibility: {
|
||
minWechatVersion: '7.0.0',
|
||
minSystemVersion: {
|
||
ios: '10.0',
|
||
android: '6.0'
|
||
},
|
||
espIdfVersion: '4.4+'
|
||
}
|
||
};
|
||
|
||
module.exports = VERSION_INFO;
|
||
```
|
||
|
||
### 10.3 用户手册
|
||
|
||
#### 10.3.1 使用步骤
|
||
1. **准备工作**
|
||
- 确保手机蓝牙已开启
|
||
- 确保ESP32设备处于配网模式
|
||
- 确保手机已连接到互联网
|
||
|
||
2. **开始配网**
|
||
- 打开BluFi配网小程序
|
||
- 点击"开始扫描"按钮
|
||
- 从设备列表中选择要配网的设备
|
||
|
||
3. **连接设备**
|
||
- 等待设备连接完成
|
||
- 连接成功后会显示"设备连接成功"
|
||
|
||
4. **配置WiFi**
|
||
- 选择要连接的WiFi网络,或手动输入WiFi信息
|
||
- 输入WiFi密码
|
||
- 点击"开始配网"按钮
|
||
|
||
5. **等待配网完成**
|
||
- 等待设备连接到WiFi网络
|
||
- 配网成功后会显示"配网完成"
|
||
|
||
#### 10.3.2 常见问题解决
|
||
|
||
**Q: 扫描不到设备怎么办?**
|
||
A:
|
||
- 检查手机蓝牙是否开启
|
||
- 确认设备是否处于配网模式
|
||
- 尝试重新启动设备
|
||
- 检查设备距离是否过远
|
||
|
||
**Q: 连接设备失败怎么办?**
|
||
A:
|
||
- 确认设备未被其他应用占用
|
||
- 尝试重启手机蓝牙
|
||
- 重新扫描设备
|
||
|
||
**Q: WiFi配网失败怎么办?**
|
||
A:
|
||
- 检查WiFi密码是否正确
|
||
- 确认WiFi信号强度是否足够
|
||
- 检查WiFi网络是否正常
|
||
- 尝试重新配网
|
||
|
||
## 11. 附录
|
||
|
||
### 11.1 参考文档
|
||
- [ESP-IDF BluFi官方文档](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-guides/blufi.html)
|
||
- [微信小程序蓝牙API文档](https://developers.weixin.qq.com/miniprogram/dev/api/device/bluetooth/wx.openBluetoothAdapter.html)
|
||
- [BluFi协议规范](https://github.com/espressif/esp-idf/tree/master/examples/bluetooth/blufi)
|
||
|
||
### 11.2 技术支持
|
||
- 开发团队:[联系方式]
|
||
- 问题反馈:[反馈渠道]
|
||
- 更新通知:[通知方式]
|
||
|
||
### 11.3 更新日志
|
||
|
||
#### v1.0.0 (2024-01-01)
|
||
- 初始版本发布
|
||
- 实现基础BluFi配网功能
|
||
- 支持设备扫描、连接、WiFi配置
|
||
- 完善错误处理和重试机制
|
||
|
||
---
|
||
|
||
**文档版本**: 2.0
|
||
**创建日期**: 2025年8月
|
||
**最后更新**: 2025年8月
|
||
**文档状态**: 已完成 |