📱Uniapp与ESP32-S3蓝牙配网全解析:从原理到实战
一、🚀 项目背景与目标
在物联网应用开发中,为设备配置WiFi网络是最基础的需求。传统方式需要用户通过复杂的命令行或专业工具进行配置,对普通用户不友好。本文将介绍如何使用Uniapp开发一个可视化蓝牙配网界面,让用户通过手机App轻松完成ESP32-S3设备的WiFi配置。
二、💡 技术原理与核心概念
1. 蓝牙低功耗(BLE)通信基础
- GATT协议:基于属性的分层协议,是BLE通信的基础
- Service(服务):包含一组相关特征的集合,如"电池服务"
- Characteristic(特征):最小数据单元,有唯一UUID,如"电池电量"
- Descriptor(描述符):提供特征值的附加信息
2. Uniapp蓝牙API核心方法
方法名 | 作用 |
---|---|
openBluetoothAdapter | 初始化蓝牙模块 |
startBluetoothDevicesDiscovery | 开始扫描蓝牙设备 |
createBLEConnection | 连接到蓝牙设备 |
getBLEDeviceServices | 获取设备的服务 |
getBLEDeviceCharacteristics | 获取特征值 |
writeBLECharacteristicValue | 向特征值写入数据 |
notifyBLECharacteristicValueChange | 启用特征值变化通知 |
三、🛠️ 完整代码实现
1. 核心页面代码
<template>
<view class="container">
<view class="header">
<text class="title">ESP32-S3 蓝牙配网</text>
</view>
<view class="form">
<input v-model="ssid" placeholder="请输入 WiFi 名称" class="input" />
<input v-model="password" placeholder="请输入 WiFi 密码" type="password" class="input" />
</view>
<view class="buttons">
<button @click="startScan" :disabled="scanning">{{ scanning ? '扫描中...' : '扫描设备' }}</button>
<button @click="connectToDevice" :disabled="!selectedDeviceId || scanning">连接设备</button>
<button @click="sendCredentials" :disabled="!isConnected || scanning">发送配网信息</button>
</view>
<view class="status">状态:{{ statusText }}</view>
<view class="devices" v-if="devices.length">
<text>扫描到的设备:</text>
<view v-for="item in devices" :key="item.deviceId" @click="selectDevice(item)" class="device-item">
<text :style="{ color: selectedDeviceId === item.deviceId ? 'green' : '#333' }">
{{ item.name || item.localName || '未知设备' }}
({{ item.deviceId.slice(-6) }})
</text>
</view>
</view>
<view class="log">
<text>日志输出:</text>
<scroll-view scroll-y style="max-height: 200px;">
<view v-for="(log, index) in logs" :key="index">{{ log }}</view>
</scroll-view>
</view>
</view>
</template>
<script>
export default {
data() {
return {
ssid: 'Marvellous',
password: 'Marvellous170303',
devices: [],
selectedDeviceId: '',
// 蓝牙服务与特征值UUID(需与ESP32端一致)
serviceId: '000000FF-0000-1000-8000-00805f9b34fb',
charSSID: '0000FFE2-0000-1000-8000-00805f9b34fb',
charPASS: '0000FFE3-0000-1000-8000-00805f9b34fb',
charNotify: '0000FFE1-0000-1000-8000-00805f9b34fb',
isConnected: false,
scanning: false,
statusText: '等待操作',
logs: [],
deviceFoundListener: null,
connectRetryCount: 0, // 连接重试计数
maxConnectRetries: 3, // 最大重试次数
isAppPlatform: uni.getSystemInfoSync().platform === 'android' || uni.getSystemInfoSync().platform === 'ios'
};
},
methods: {
// 日志输出方法(带时间戳)
log(msg) {
const time = new Date().toLocaleTimeString();
this.logs.push(`[${time}] ${msg}`);
this.ensureScrollVisible(); // 确保日志可见性
console.log(`[${time}] ${msg}`);
},
// 确保日志滚动到底部(修复DOM访问异常)
ensureScrollVisible() {
setTimeout(() => {
try {
const query = uni.createSelectorQuery();
query.select('.log scroll-view').boundingClientRect(rect => {
if (rect) {
query.select('.log scroll-view').scrollOffset(offset => {
if (offset && offset.scrollHeight > offset.height) {
query.select('.log scroll-view')
.scrollOffset({ scrollTop: offset.scrollHeight, duration: 0 })
.exec();
}
}).exec();
}
}).exec();
} catch (e) {
console.error('滚动日志失败:', e);
}
}, 100);
},
// 开始扫描设备
startScan() {
// 重置状态
this.devices = [];
this.scanning = true;
this.selectedDeviceId = '';
this.statusText = '正在初始化蓝牙...';
this.log('开始扫描流程:初始化蓝牙适配器');
// 清除之前的监听
if (this.deviceFoundListener) {
uni.offBluetoothDeviceFound(this.deviceFoundListener);
}
// App平台特殊处理
if (this.isAppPlatform) {
this.checkAppBluetoothPermission();
} else {
this.initBluetoothAdapter();
}
},
// 检查App平台蓝牙权限
checkAppBluetoothPermission() {
// #ifdef APP-PLUS
plus.android.requestPermissions(
['android.permission.BLUETOOTH', 'android.permission.ACCESS_FINE_LOCATION'],
() => {
this.log('蓝牙权限已获取');
this.initBluetoothAdapter();
},
err => {
this.log(`用户拒绝蓝牙权限:${JSON.stringify(err)}`);
this.statusText = '请授予蓝牙权限';
this.scanning = false;
// 提示用户去设置中开启权限
uni.showModal({
title: '权限请求',
content: '蓝牙功能需要位置权限才能正常工作,请在设置中授予权限',
success: (res) => {
if (res.confirm) {
plus.runtime.openURL('app-settings:'); // 打开应用设置页面
}
}
});
}
);
// #endif
// #ifndef APP-PLUS
this.initBluetoothAdapter(); // 非App平台直接初始化
// #endif
},
// 初始化蓝牙适配器
initBluetoothAdapter() {
// 1. 初始化蓝牙适配器
uni.openBluetoothAdapter({
success: () => {
this.log('蓝牙适配器初始化成功');
this.statusText = '正在扫描设备(8秒)...';
// 2. 监听设备发现事件(实时获取新设备)
this.deviceFoundListener = uni.onBluetoothDeviceFound(res => {
this.handleDeviceFound(res);
});
// 3. 开始扫描设备(不限制服务UUID,扫描所有设备)
uni.startBluetoothDevicesDiscovery({
services: [], // 关键:不指定服务,避免过滤掉目标设备
allowDuplicatesKey: false, // 不允许重复上报同一设备
interval: 0, // 扫描间隔(0为默认最快速度)
success: () => {
this.log('扫描命令已发送,开始接收设备...');
// 4. 扫描8秒后自动停止(延长扫描时间提高成功率)
setTimeout(() => {
this.stopScan();
}, 8000);
},
fail: err => {
this.log(`启动扫描失败:${JSON.stringify(err)}`);
this.statusText = '扫描启动失败';
this.scanning = false;
}
});
},
fail: err => {
this.log(`蓝牙初始化失败:${JSON.stringify(err)}`);
this.scanning = false;
// 错误处理:提示用户开启蓝牙
if (err.errCode === 10001) {
this.statusText = '蓝牙未开启';
uni.showModal({
title: '蓝牙未开启',
content: '请先开启手机蓝牙功能',
showCancel: false,
success: () => {
this.statusText = '等待操作';
}
});
} else {
this.statusText = '蓝牙初始化失败';
}
}
});
},
// 处理发现的设备
handleDeviceFound(res) {
const devices = res.devices || [];
devices.forEach(device => {
// 过滤无效设备(信号强度过低或无ID)
if (!device.deviceId || device.RSSI < -85) {
return;
}
// 检查设备是否可连接(部分设备会返回canConnect属性)
if (device.canConnect !== undefined && !device.canConnect) {
this.log(`忽略不可连接设备:${device.name || '未知'}`);
return;
}
// 检查是否已添加(去重)
const isExist = this.devices.some(d => d.deviceId === device.deviceId);
if (!isExist) {
// 设备名称优先级:name > localName > 未知
const deviceName = device.name || device.localName || '未知设备';
this.log(`发现新设备:${deviceName}(信号强度:${device.RSSI}dBm)`);
this.devices.push(device);
}
});
},
// 停止扫描设备
stopScan() {
if (!this.scanning) return;
uni.stopBluetoothDevicesDiscovery({
success: () => {
this.log(`扫描结束,共发现 ${this.devices.length} 个设备`);
this.statusText = `扫描完成(${this.devices.length}个设备)`;
},
fail: err => {
this.log(`停止扫描失败:${JSON.stringify(err)}`);
},
complete: () => {
this.scanning = false;
}
});
},
// 选择设备
selectDevice(device) {
if (this.scanning) return; // 扫描中不允许选择
this.selectedDeviceId = device.deviceId;
const deviceName = device.name || device.localName || '未知设备';
this.statusText = `已选择:${deviceName}`;
this.log(`选中设备:${deviceName}(ID:${device.deviceId})`);
},
// 连接设备(主方法)
connectToDevice() {
if (!this.selectedDeviceId) return;
this.connectRetryCount = 0; // 重置重试计数
this.statusText = '正在连接设备...';
const device = this.devices.find(d => d.deviceId === this.selectedDeviceId);
const deviceName = device?.name || device?.localName || '未知设备';
this.log(`开始连接设备:${deviceName}`);
// 关键优化:先断开可能的已有连接
if (this.isConnected) {
this.log('关闭已有连接...');
uni.closeBLEConnection({
deviceId: this.selectedDeviceId,
success: () => {
this.log('已断开旧连接,准备重新连接');
this.performBLEConnection();
},
fail: () => {
this.log('断开旧连接失败,尝试直接连接');
this.performBLEConnection();
}
});
} else {
this.performBLEConnection();
}
},
// 执行蓝牙连接(带重试机制)
performBLEConnection() {
uni.createBLEConnection({
deviceId: this.selectedDeviceId,
timeout: 15000, // 延长超时时间至15秒
success: () => {
this.connectRetryCount = 0; // 重置重试计数
this.log('设备连接成功,开始发现服务...');
this.statusText = '连接成功,发现服务中...';
setTimeout(() => {
this.discoverServices();
}, 800); // 增加延迟时间,确保连接稳定
},
fail: err => {
this.log(`连接失败:${JSON.stringify(err)}`);
// 连接超时错误,尝试重试
if (err.errCode === 10012 && this.connectRetryCount < this.maxConnectRetries) {
this.connectRetryCount++;
this.log(`尝试第 ${this.connectRetryCount}/${this.maxConnectRetries} 次重试连接...`);
// 增加随机延迟,避免固定频率重试导致系统阻塞
const delay = 1000 + Math.random() * 1000;
setTimeout(() => {
this.performBLEConnection();
}, delay);
return;
}
// 最终连接失败处理
this.statusText = '连接设备失败';
// 提供更详细的错误提示
if (err.errCode === 10012) {
uni.showModal({
title: '连接超时',
content: '1. 请确保设备处于可连接状态\n2. 尝试重启设备和手机蓝牙\n3. 请将设备靠近手机',
showCancel: false
});
}
}
});
},
// 发现设备服务
discoverServices() {
uni.getBLEDeviceServices({
deviceId: this.selectedDeviceId,
success: res => {
this.log(`发现服务列表:${JSON.stringify(res.services.map(s => s.uuid))}`);
// 查找目标服务(忽略大小写匹配)
const targetService = res.services.find(s =>
s.uuid.toLowerCase() === this.serviceId.toLowerCase()
);
if (targetService) {
this.log(`找到目标服务:${this.serviceId}`);
this.isConnected = true;
this.statusText = '已连接,等待发送配网信息';
this.enableNotify(); // 启用通知特征
} else {
this.log(`未找到目标服务(${this.serviceId})`);
this.statusText = '连接失败:未找到目标服务';
this.disconnect(); // 断开连接
uni.showToast({ title: '未找到目标服务', icon: 'none' });
}
},
fail: err => {
this.log(`获取服务失败:${JSON.stringify(err)}`);
this.statusText = '获取服务失败';
this.disconnect();
}
});
},
// 启用通知(接收设备返回的结果)
enableNotify() {
uni.getBLEDeviceCharacteristics({
deviceId: this.selectedDeviceId,
serviceId: this.serviceId,
success: res => {
this.log(`服务特征列表:${JSON.stringify(res.characteristics.map(c => c.uuid))}`);
// 查找通知特征
const notifyChar = res.characteristics.find(c =>
c.uuid.toLowerCase() === this.charNotify.toLowerCase()
);
if (!notifyChar) {
this.log(`未找到通知特征(${this.charNotify})`);
return;
}
// 检查是否支持通知
if (!notifyChar.properties.notify) {
this.log(`通知特征不支持notify属性`);
return;
}
// 启用通知
uni.notifyBLECharacteristicValueChange({
deviceId: this.selectedDeviceId,
serviceId: this.serviceId,
characteristicId: this.charNotify,
state: true,
success: () => {
this.log('通知功能已启用,等待设备响应...');
this.statusText = '已连接,可发送配网信息';
},
fail: err => {
this.log(`启用通知失败:${JSON.stringify(err)}`);
}
});
},
fail: err => {
this.log(`获取特征失败:${JSON.stringify(err)}`);
}
});
},
// 发送配网信息(SSID和密码)
sendCredentials() {
if (!this.ssid.trim() || !this.password) {
uni.showToast({
title: '请输入WiFi名称和密码',
icon: 'none'
});
return;
}
this.statusText = '正在发送SSID...';
// 先发送SSID,成功后再发送密码
this.writeToCharacteristic(this.charSSID, this.ssid, () => {
this.statusText = '正在发送密码...';
this.writeToCharacteristic(this.charPASS, this.password, () => {
this.statusText = '配网信息发送完成,等待设备回应...';
this.log('SSID和密码已全部发送,等待设备连接WiFi...');
});
});
},
// 向特征值写入数据
writeToCharacteristic(uuid, value, callback) {
try {
// 将字符串转换为ArrayBuffer(UTF-8编码)
const buffer = new TextEncoder().encode(value);
uni.writeBLECharacteristicValue({
deviceId: this.selectedDeviceId,
serviceId: this.serviceId,
characteristicId: uuid,
value: buffer.buffer,
success: () => {
this.log(`成功写入${uuid}:${value}`);
callback && callback();
},
fail: err => {
this.log(`写入${uuid}失败:${JSON.stringify(err)}`);
this.statusText = `发送失败:${err.errMsg}`;
}
});
} catch (e) {
this.log(`写入数据异常:${e.message}`);
}
},
// 处理设备返回的通知
onNotify(res) {
try {
const result = new TextDecoder().decode(res.value);
this.statusText = `设备回应:${result}`;
this.log(`收到设备通知:${result}`);
// 常见结果处理(根据ESP32返回的实际内容调整)
if (result.includes('成功')) {
uni.showToast({
title: '配网成功!',
icon: 'success'
});
} else if (result.includes('失败')) {
uni.showToast({
title: '配网失败',
icon: 'none'
});
}
} catch (e) {
this.log(`解析通知数据失败:${e.message}`);
}
},
// 断开连接
disconnect() {
if (!this.selectedDeviceId) return;
uni.closeBLEConnection({
deviceId: this.selectedDeviceId,
success: () => {
this.log('已断开设备连接');
},
fail: err => {
this.log(`断开连接失败:${JSON.stringify(err)}`);
// 尝试强制重置蓝牙适配器
this.resetBluetoothAdapter();
},
complete: () => {
this.isConnected = false;
this.selectedDeviceId = '';
}
});
},
// 重置蓝牙适配器
resetBluetoothAdapter() {
this.log('正在重置蓝牙适配器...');
uni.closeBluetoothAdapter({
success: () => {
setTimeout(() => {
uni.openBluetoothAdapter({
success: () => {
this.log('蓝牙适配器重置成功');
},
fail: err => {
this.log(`蓝牙适配器重置失败:${JSON.stringify(err)}`);
}
});
}, 1000);
}
});
}
},
// 页面加载时初始化通知监听
onLoad() {
// 监听设备特征值变化(接收配网结果)
uni.onBLECharacteristicValueChange(res => {
this.onNotify(res);
});
},
// 页面卸载时清理资源
onUnload() {
this.log('页面关闭,清理蓝牙资源');
// 停止扫描
if (this.scanning) {
uni.stopBluetoothDevicesDiscovery();
}
// 断开连接
this.disconnect();
// 关闭蓝牙适配器
uni.closeBluetoothAdapter();
// 移除设备发现监听
if (this.deviceFoundListener) {
uni.offBluetoothDeviceFound(this.deviceFoundListener);
}
}
};
</script>
<style scoped>
.container {
padding: 20rpx;
font-size: 28rpx;
}
.header {
text-align: center;
padding: 30rpx 0;
}
.title {
font-size: 36rpx;
font-weight: bold;
color: #333;
}
.form {
margin: 20rpx 0;
}
.input {
border: 1px solid #ccc;
border-radius: 8rpx;
margin-bottom: 20rpx;
padding: 20rpx;
font-size: 28rpx;
}
.buttons {
display: flex;
gap: 15rpx;
margin: 30rpx 0;
}
.buttons button {
flex: 1;
padding: 20rpx 0;
border-radius: 8rpx;
background-color: #007aff;
color: white;
}
.buttons button:disabled {
background-color: #ccc;
}
.status {
margin: 20rpx 0;
padding: 15rpx;
border-left: 4rpx solid #007aff;
background-color: #f5f7fa;
color: #333;
}
.devices {
margin: 20rpx 0;
padding: 15rpx;
border: 1px solid #eee;
border-radius: 8rpx;
}
.device-item {
padding: 15rpx 0;
border-bottom: 1px solid #eee;
cursor: pointer;
}
.device-item:last-child {
border-bottom: none;
}
.log {
margin: 20rpx 0;
padding: 15rpx;
border: 1px solid #eee;
border-radius: 8rpx;
color: #666;
}
.log text {
display: block;
margin-bottom: 10rpx;
font-weight: bold;
color: #333;
}
.log scroll-view {
border: 1px dashed #eee;
padding: 10rpx;
}
</style>
四、🔌 蓝牙通信流程详解
1. 蓝牙初始化与扫描
2. 设备连接与服务发现
3. 数据传输流程
五、⚙️ 关键技术点解析
1. UUID匹配机制
代码中定义的UUID必须与ESP32端完全一致:
// 服务UUID - 标识ESP32的配网服务
serviceId: '000000FF-0000-1000-8000-00805f9b34fb',
// 特征值UUID - 分别用于接收SSID、密码和发送通知
charSSID: '0000FFE2-0000-1000-8000-00805f9b34fb',
charPASS: '0000FFE3-0000-1000-8000-00805f9b34fb',
charNotify: '0000FFE1-0000-1000-8000-00805f9b34fb',
2. ArrayBuffer数据处理
蓝牙传输要求数据为二进制格式,通过以下方式转换:
// 字符串转ArrayBuffer(发送时)
const buffer = new TextEncoder().encode("WiFi名称");
// ArrayBuffer转字符串(接收时)
const result = new TextDecoder().decode(arrayBuffer);
3. 连接重试机制
针对连接超时问题,实现了自动重试:
// 连接超时错误处理
if (err.errCode === 10012 && this.connectRetryCount < 3) {
this.connectRetryCount++;
this.log(`尝试第 ${this.connectRetryCount}/3 次重试连接...`);
// 随机延迟避免系统阻塞
const delay = 1000 + Math.random() * 1000;
setTimeout(() => this.performBLEConnection(), delay);
}
4. 平台权限适配
// #ifdef APP-PLUS
// App平台需要额外申请位置权限
plus.android.requestPermissions([
'android.permission.BLUETOOTH',
'android.permission.ACCESS_FINE_LOCATION'
]);
// #endif
六、🛠️ 开发与调试指南
1. 环境准备
- HBuilderX 3.2.0+
- 安卓/iOS真机(蓝牙功能在模拟器中受限)
- ESP32-S3开发板(已烧录蓝牙配网固件)
2. 调试技巧
- 日志输出:页面下方的日志区域实时显示操作记录,便于追踪问题
- 信号强度:扫描到的设备会显示RSSI值(信号强度),选择信号强的设备
- 错误码参考:
错误码 | 含义 | 解决方案 |
---|---|---|
10001 | 蓝牙未开启 | 提示用户开启蓝牙 |
10002 | 没有找到指定设备 | 检查设备是否在范围内 |
10003 | 连接失败 | 检查设备是否被占用,尝试重启 |
10004 | 没有找到指定服务 | 检查UUID是否与设备一致 |
10008 | 连接超时 | 增加超时时间或重试次数 |
七、💡 常见问题与解决方案
1. 扫描不到设备
- 检查手机蓝牙是否开启
- 确认ESP32设备处于可发现状态
- 尝试重启手机和设备
- 检查App的蓝牙权限是否已授予
2. 连接失败或超时
- 确保设备UUID匹配
- 检查设备是否已被其他设备连接
- 增加连接超时时间(代码中默认15秒)
- 实现自动重试机制(代码中已包含)
3. 数据发送失败
- 确认特征值权限是否正确配置(需支持WRITE属性)
- 检查数据长度是否超过MTU限制(通常为20字节,超长需分包)
- 添加错误处理和重发机制
八、📈 性能优化建议
-
扫描优化:
- 限制扫描时间(代码中为8秒)
- 过滤无效设备(信号弱、无名称)
-
连接稳定性:
- 实现自动重试机制
- 连接前先断开已有连接
- 发现服务前增加短暂延迟
-
内存管理:
- 页面卸载时清理所有蓝牙资源
- 移除事件监听避免内存泄漏
九、🎉 总结与扩展
通过本文提供的代码和解析,你可以实现一个完整的ESP32-S3蓝牙配网功能。这个方案具有以下特点:
- 跨平台支持:同时支持安卓和iOS设备
- 用户友好:直观的界面和详细的日志输出
- 高稳定性:完善的错误处理和重试机制
- 可扩展性:可根据需求添加更多功能,如:
- 设备固件升级
- 设备状态监控
- 多设备管理
十、📚 参考资源
希望这篇文章对你理解蓝牙配网和Uniapp开发有所帮助!如果你有任何问题或建议,欢迎在评论区留言讨论~