业务功能·
基于uni-app的蓝牙设备连接开发实践
本文结合uni-app跨平台框架,通过封装可复用的蓝牙连接模块,实现从权限管理到设备通信的全流程控制,并提供完整的实战案例,帮助开发者快速落地蓝牙设备交互功能。
背景
在智能硬件与移动应用深度融合的场景中,蓝牙连接是实现设备交互的核心桥梁。本文结合uni-app跨平台框架,通过封装可复用的蓝牙连接模块,实现从权限管理到设备通信的全流程控制,并提供完整的实战案例,帮助开发者快速落地蓝牙设备交互功能。
实现
一、蓝牙连接核心模块设计
我们通过两个核心类实现逻辑分层:BluetoothConnector
处理业务逻辑与状态管理,InnerBluetoothConnector
封装uni-app原生蓝牙API,确保代码的可维护性与跨平台兼容性。
// 蓝牙连接核心类
class BluetoothConnector {
constructor() {
// 内部蓝牙连接实例
this.connector = new InnerBluetoothConnector();
// 连接状态:0 表示未连接,1 表示扫描中,2 表示已连接
this.currentState = 0;
// 存储扫描到的设备列表
this.deviceList = [];
// 存储当前选中要连接的设备
this.selectedDevice = null;
// 常见蓝牙错误码对应的错误信息
this.errorMap = {
10000: '蓝牙未初始化',
10001: '蓝牙不可用',
10002: '未找到指定设备',
10003: '连接失败',
10004: '未找到指定服务',
10005: '未找到指定特征值',
10006: '连接已断开',
10007: '当前操作不支持',
10008: '系统蓝牙异常',
10009: '系统版本过低,不支持蓝牙功能',
10012: '连接超时',
10013: '设备 ID 无效'
};
}
// 初始化蓝牙模块,包括权限检查、适配器初始化和监听设置
async init() {
try {
await this.checkPermissions();
await this.initAdapter();
this.setupAdapterListeners();
} catch (error) {
console.error('蓝牙初始化失败:', error);
throw error;
}
}
// 检查蓝牙权限,若未授权则请求授权
checkPermissions() {
return new Promise((resolve, reject) => {
uni.getSetting({
success: ({ authSetting }) => {
if (authSetting['scope.bluetooth']) {
resolve();
} else {
uni.authorize({
scope: 'scope.bluetooth',
success: resolve,
fail: () => {
uni.showModal({
title: '权限申请',
content: '需要蓝牙权限以连接设备',
success: ({ confirm }) => {
if (confirm) {
uni.openSetting({
success: () => {
uni.getSetting({
success: ({ authSetting }) => {
if (authSetting['scope.bluetooth']) {
resolve();
} else {
reject(new Error('用户未授予蓝牙权限'));
}
},
fail: reject
});
},
fail: reject
});
} else {
reject(new Error('用户拒绝授予蓝牙权限'));
}
},
fail: reject
});
}
});
}
},
fail: reject
});
});
}
// 初始化蓝牙适配器
initAdapter() {
return new Promise((resolve, reject) => {
uni.openBluetoothAdapter({
success: () => {
resolve();
this.setupAdapterListeners();
},
fail: (err) => {
if (err.errMsg.includes('already opened')) {
resolve();
} else {
reject(new Error('蓝牙初始化失败: ' + this.getErrorMessage(err.errCode)));
}
}
});
});
}
// 开始扫描设备
startScan() {
return new Promise((resolve, reject) => {
if (this.currentState === 1) {
reject(new Error('正在扫描中,请勿重复操作'));
return;
}
this.currentState = 1;
this.connector.scanEcgCards({
duration: 10000,
discovery: (devices) => {
// 去重处理
const newDevices = devices.filter(
d =>!this.deviceList.some(exist => exist.device.deviceId === d.deviceId)
);
this.deviceList = [...this.deviceList, ...newDevices];
},
success: () => {
this.currentState = 0;
resolve(this.deviceList);
},
fail: (err) => {
this.currentState = 0;
reject(new Error('设备扫描失败: ' + this.getErrorMessage(err.errCode)));
}
});
});
}
// 连接指定设备
connect(device) {
return new Promise((resolve, reject) => {
if (this.currentState === 2) {
reject(new Error('已连接设备,请勿重复连接'));
return;
}
this.connector.connect({
device,
success: () => {
this.currentState = 2;
this.selectedDevice = device;
resolve();
},
fail: (err) => {
this.currentState = 0;
reject(new Error('设备连接失败: ' + this.getErrorMessage(err.errCode)));
}
});
});
}
// 断开当前连接的设备
disconnect() {
return new Promise((resolve, reject) => {
if (this.currentState!== 2) {
reject(new Error('未连接设备,无需断开'));
return;
}
this.connector.disconnect({
success: () => {
this.currentState = 0;
this.selectedDevice = null;
resolve();
},
fail: (err) => {
this.currentState = 2;
reject(new Error('设备断开失败: ' + this.getErrorMessage(err.errCode)));
}
});
});
}
// 设置连接状态变化的回调函数
setConnectCallback(callback) {
this.connector.setConnectCallback({
disconnectionCallback: () => {
this.currentState = 0;
this.selectedDevice = null;
if (typeof callback === 'function') {
callback();
}
}
});
}
// 根据错误码获取对应的错误信息
getErrorMessage(errCode) {
return this.errorMap[errCode] || `未知错误: ${errCode}`;
}
// 设置蓝牙适配器状态变化的监听
setupAdapterListeners() {
uni.onBluetoothAdapterStateChange((res) => {
if (!res.available) {
if (this.currentState === 2) {
this.disconnect().catch((err) => {
console.error('蓝牙适配器不可用,断开连接失败:', err);
});
}
this.currentState = 0;
this.deviceList = [];
this.selectedDevice = null;
}
});
}
}
// 内部蓝牙连接类,封装具体的蓝牙操作
class InnerBluetoothConnector {
// 扫描设备
scanEcgCards(options) {
const { duration, discovery, success, fail } = options;
uni.startBluetoothDevicesDiscovery({
services: [],
allowDuplicatesKey: false,
success: () => {
uni.onBluetoothDeviceFound((res) => {
if (discovery) {
discovery(res.devices);
}
});
setTimeout(() => {
uni.stopBluetoothDevicesDiscovery({
success: () => {
if (success) {
success();
}
},
fail: (err) => {
if (fail) {
fail(err);
}
}
});
}, duration);
},
fail: (err) => {
if (fail) {
fail(err);
}
}
});
}
// 连接设备
connect(options) {
const { device, success, fail } = options;
uni.createBLEConnection({
deviceId: device.deviceId,
success: () => {
if (success) {
success();
}
},
fail: (err) => {
if (fail) {
fail(err);
}
}
});
}
// 断开连接
disconnect(options) {
const { success, fail } = options;
if (this.connectedDevice) {
uni.closeBLEConnection({
deviceId: this.connectedDevice.deviceId,
success: () => {
if (success) {
success();
}
},
fail: (err) => {
if (fail) {
fail(err);
}
}
});
} else {
if (fail) {
fail(new Error('未连接设备'));
}
}
}
// 设置连接状态变化的回调
setConnectCallback(options) {
const { disconnectionCallback } = options;
uni.onBLEConnectionStateChange((res) => {
if (!res.connected && this.connectedDevice && res.deviceId === this.connectedDevice.deviceId) {
if (disconnectionCallback) {
disconnectionCallback();
}
}
});
}
}
// 导出单例对象
const connector = new BluetoothConnector();
export default connector;
二、关键功能实现细节
- 状态机驱动的流程控制
通过currentState
严格控制操作流程,避免异步竞态问题:- 扫描中(1)时禁止重复扫描
- 已连接(2)时禁止重复连接
- 适配器关闭时自动重置所有状态
- 权限处理最佳实践
// 渐进式权限申请(检查 → 申请 → 设置页引导) checkPermissions() { return new Promise((resolve, reject) => { uni.getSetting({ success: ({ authSetting }) => { authSetting['scope.bluetooth'] ? resolve() : this.requestPermission(resolve, reject); } }); }); } requestPermission(resolve, reject) { uni.authorize({ scope: 'scope.bluetooth', success: resolve, fail: () => this.showPermissionModal(reject); }); } showPermissionModal(reject) { uni.showModal({ title: '权限申请', content: '需要蓝牙权限以连接设备', success: ({ confirm }) => confirm ? uni.openSetting() : reject(new Error('权限拒绝')), }); }
- 设备去重与过滤
- 基于
deviceId
进行唯一性校验 - 过滤信号强度弱的设备(RSSI > -80dBm)
- 按信号强度降序排序
- 基于
- 健壮的错误处理
通过errorMap
映射uni-app蓝牙错误码,提供清晰的错误提示:getErrorMessage(errCode) { return this.errorMap[errCode] || `未知错误(代码:${errCode})`; }
案例
以下是一个完整的Vue组件,演示如何使用封装好的BluetoothConnector
实现设备连接功能:
<template>
<view class="bluetooth-container">
<!-- 连接状态按钮 -->
<button
:disabled="isScanning || isConnecting"
@click="handleConnect"
>
{{ currentStateText }}
</button>
<!-- 设备列表(弹窗显示) -->
<uni-popup v-model="showDeviceList">
<view class="device-item" v-for="device in filteredDevices" :key="device.deviceId">
<text>{{ device.name || '未知设备' }}</text>
<text class="rssi">信号:{{ device.RSSI }}dBm</text>
</view>
</uni-popup>
</view>
</template>
<script setup>
import { ref, computed, onUnmounted } from 'vue';
import connector from './bluetooth_connector.js';
// 状态映射
const currentState = ref(connector.currentState);
const isScanning = computed(() => currentState.value === 1);
const isConnecting = computed(() => currentState.value === 2);
const currentStateText = computed(() =>
isScanning.value ? '扫描中...' :
isConnecting.value ? '已连接' : '搜索设备'
);
// 过滤后的设备列表(信号过滤+排序)
const filteredDevices = computed(() =>
connector.deviceList
.filter(d => d.RSSI > -80)
.sort((a, b) => b.RSSI - a.RSSI)
);
// 连接处理
const handleConnect = async () => {
if (isConnecting.value) {
// 已连接时断开连接
await connector.disconnect();
return;
}
try {
await connector.init(); // 初始化权限和适配器
await connector.startScan(); // 启动设备扫描
showDeviceList.value = true; // 显示设备列表
} catch (error) {
uni.showToast({ title: error.message, icon: 'none' });
}
};
// 设备选择
const selectDevice = (device) => {
connector.connect(device)
.then(() => {
showDeviceList.value = false;
uni.showToast({ title: '连接成功', icon: 'success' });
})
.catch((err) => {
uni.showToast({ title: err.message, icon: 'none' });
});
};
// 页面卸载时清理资源
onUnmounted(() => {
connector.resetConnection(); // 断开连接并清理监听
});
</script>
<style scoped>
.bluetooth-container {
padding: 20px;
text-align: center;
}
button {
padding: 10px 20px;
background-color: #1677FF;
color: white;
border-radius: 4px;
border: none;
margin-bottom: 10px;
}
.device-item {
padding: 10px;
border-bottom: 1px solid #eee;
}
.rssi {
color: #666;
font-size: 0.9em;
}
</style>
最佳实践与注意事项
- 资源释放
- 在页面卸载时调用
connector.resetConnection()
,确保断开连接并移除所有监听 - 使用
uni.removeAllBluetoothListeners()
避免内存泄漏
- 在页面卸载时调用
- 性能优化
- 限制单次扫描时长(建议10-15秒),降低功耗
- 实时增量更新设备列表,避免全量刷新
- 跨平台适配
- 处理iOS和Android在蓝牙权限提示、状态回调上的差异
- 使用
uni.getSystemInfo()
判断平台,添加特殊逻辑(如iOS后台扫描)
- 用户体验
- 连接过程中显示加载状态,避免重复点击
- 对弱信号设备进行视觉弱化,优先展示强信号设备
总结
通过将蓝牙连接逻辑封装为独立的BluetoothConnector
模块,开发者可专注于业务功能实现,大幅降低跨设备开发成本。核心优势包括:
- 状态驱动:通过状态机确保操作流程的原子性
- 错误闭环:完善的权限处理与错误提示机制
- 资源安全:严格的状态校验与资源释放策略
此方案适用于心率监测、智能穿戴、医疗设备等多种蓝牙交互场景。实际开发中,只需根据设备特定的GATT服务/特征值扩展数据收发逻辑,即可快速落地完整的蓝牙通信功能。