uniapp小程序连接蓝牙设备实战指南
一、引言
在许多物联网(IoT)应用场景中,如智能手环、蓝牙耳机、智能家居、传感器设备等,通过蓝牙与设备通信是必不可少的环节。微信/支付宝小程序如果想与 BLE(Bluetooth Low Energy)设备交互,需要借助对应平台提供的蓝牙 API。uniapp
作为一个跨端开发框架,将这些 API 进行了统一封装,让你能用一套代码同时支持多端环境。
本指南将带你从零开始,学习如何在 uniapp 小程序中:
- 初始化蓝牙模块(检查适配器状态)
- 扫描附近可用设备
- 连接指定蓝牙设备
- 发现设备服务与特征
- 开启特征消息订阅并读写数据
- 断开与销毁连接
- 处理异常与边界情况
内容同时配备ASCII 流程图与详细代码示例,让你更容易理解蓝牙流程背后的原理。在开始之前,请确保你的蓝牙设备为 BLE(低功耗蓝牙)协议,且已正确打开,并与手机配对或处于可被扫描状态。
二、蓝牙通信基础
2.1 BLE(Bluetooth Low Energy)概念
- BLE(低功耗蓝牙):主要用于短距离、低功耗的数据交换,适合物联网设备。
BLE 设备由 服务(Service) 和 特征(Characteristic) 组成:
- Service:一组相关特征的集合,比如“心率服务”中包含多个“心率测量特征”。
- Characteristic:可读或可写的具体数据项,比如“心率值”、“电池电量”等。
在小程序中,常见的 BLE 流程为:
- 打开蓝牙模块 → 2. 扫描设备 → 3. 连接指定设备 → 4. 获取服务列表 → 5. 获取特征列表 → 6. 读写/订阅特征 → 7. 断开连接 → 8. 关闭蓝牙模块(可选)。
2.2 小程序蓝牙适配与 uniapp 封装
不同平台(微信小程序、支付宝小程序、百度小程序等)对蓝牙的原生 API 稍有差异,但 uniapp 在运行时会映射到对应平台。本文所有示例均以微信小程序为主,支付宝小程序模式下也基本一致,只需将 uni
替换为 my
(支付宝)或 swan
(百度)即可。
在 uniapp 中,一些常用核心方法包括:
uni.openBluetoothAdapter()
uni.onBluetoothAdapterStateChange(callback)
uni.startBluetoothDevicesDiscovery(options)
uni.onBluetoothDeviceFound(callback)
uni.createBLEConnection(options)
uni.getBLEDeviceServices(options)
uni.getBLEDeviceCharacteristics(options)
uni.notifyBLECharacteristicValueChange(options)
uni.onBLECharacteristicValueChange(callback)
uni.writeBLECharacteristicValue(options)
uni.closeBLEConnection(options)
uni.closeBluetoothAdapter()
本指南后续会依序介绍每个步骤的使用方法与细节。
三、环境准备与权限配置
3.1 app.json / manifest.json 配置
在小程序中使用蓝牙,需要在 app.json
(或对应页面的 json
配置)里声明使用蓝牙模块权限。以微信小程序为例,app.json
中应该包含:
// app.json
{
"pages": [
"pages/index/index",
"pages/bluetooth/bluetooth"
],
"window": {
"navigationBarTitleText": "蓝牙示例"
},
// 在 "permission" 节点中声明蓝牙权限(仅微信小程序 2.10.0+ 支持)
"permission": {
"scope.userLocation": {
"desc": "您的地理位置将用于搜索附近的蓝牙设备"
}
}
}
说明:
- 扫描蓝牙 可能需要打开设备定位权限,尤其是在 iOS 设备上,否则无法扫描到 BLE 设备。
- 在支付宝/百度小程序,可参考它们的权限要求,无需额外在
app.json
中声明,但用户会在首次使用时被弹窗授权。
3.2 兼容性检查
在真正调用蓝牙 API 前,需要检查当前环境是否支持蓝牙。常见做法:
// utils/bluetooth.js
export function checkBluetoothAdapter() {
return new Promise((resolve, reject) => {
uni.openBluetoothAdapter({
success(res) {
console.log('蓝牙适配器已启动', res);
resolve(res);
},
fail(err) {
console.error('打开蓝牙适配器失败', err);
uni.showToast({ title: '请检查手机蓝牙或系统版本是否支持', icon: 'none' });
reject(err);
}
});
});
}
- 调用时机:建议在页面
onLoad
或用户点击“连接蓝牙”按钮时调用,以免小程序启动即打开蓝牙,影响性能。
四、蓝牙扫描与发现设备
4.1 打开蓝牙适配器
- 在页面的
methods
或onLoad
里调用uni.openBluetoothAdapter()
,启动本机蓝牙模块。 - 监听蓝牙状态变化,若用户关闭蓝牙或设备离线,可及时提示。
<script>
export default {
data() {
return {
isAdapterOpen: false
};
},
onLoad() {
this.initBluetooth();
},
methods: {
initBluetooth() {
uni.openBluetoothAdapter({
success: (res) => {
console.log('openBluetoothAdapter success', res);
this.isAdapterOpen = true;
// 监听蓝牙适配器状态变化
uni.onBluetoothAdapterStateChange((adapterState) => {
console.log('adapterState changed', adapterState);
this.isAdapterOpen = adapterState.available;
if (!adapterState.available) {
uni.showToast({ title: '蓝牙已关闭', icon: 'none' });
}
});
},
fail: (err) => {
console.error('openBluetoothAdapter fail', err);
uni.showToast({ title: '请先打开手机蓝牙', icon: 'none' });
}
});
}
}
};
</script>
uni.onBluetoothAdapterStateChange(callback)
会实时回调蓝牙模块的available
(是否可用)和discovering
(是否正在扫描)等状态。- 如果用户在小程序后台或其他地方关闭蓝牙,需要通过该监听及时更新 UI 并停止相关操作。
4.2 开始扫描蓝牙设备
- 在确认适配器可用后,调用
uni.startBluetoothDevicesDiscovery()
开始扫描。 - 可通过传入
services
(要搜索的服务 UUID 列表)参数进行定向扫描;如果想搜索所有设备则无需传入。 - 监听
uni.onBluetoothDeviceFound(callback)
,在回调里获取到附近每个新发现的设备信息。
<template>
<view class="container">
<button @click="startScan">开始扫描</button>
<text v-if="isScanning">扫描中...</text>
<view class="device-list">
<view
v-for="(dev, index) in devices"
:key="dev.deviceId"
class="device-item"
@click="connectDevice(dev)"
>
<text>{{ dev.name || '未知设备' }} ({{ dev.deviceId }})</text>
<text>RSSI: {{ dev.RSSI }}</text>
</view>
</view>
</view>
</template>
<script>
export default {
data() {
return {
isAdapterOpen: false,
isScanning: false,
devices: [] // 已发现设备列表
};
},
onLoad() {
this.initBluetooth();
},
methods: {
initBluetooth() {
uni.openBluetoothAdapter({
success: () => {
this.isAdapterOpen = true;
uni.onBluetoothAdapterStateChange((state) => {
this.isAdapterOpen = state.available;
this.isScanning = state.discovering;
});
},
fail: () => {
uni.showToast({ title: '请先打开手机蓝牙', icon: 'none' });
}
});
},
startScan() {
if (!this.isAdapterOpen) {
uni.showToast({ title: '蓝牙未初始化', icon: 'none' });
return;
}
this.devices = [];
uni.startBluetoothDevicesDiscovery({
// allowDuplicatesKey: false, // 微信小程序可选,是否重复上报同一设备
success: (res) => {
console.log('start discovery success', res);
this.isScanning = true;
// 监听新设备发现事件
uni.onBluetoothDeviceFound((res) => {
// res.devices 为数组
res.devices.forEach((device) => {
// 过滤已经存在的设备
const exists = this.devices.findIndex((d) => d.deviceId === device.deviceId) !== -1;
if (!exists) {
this.devices.push(device);
}
});
});
},
fail: (err) => {
console.error('start discovery fail', err);
uni.showToast({ title: '扫描失败', icon: 'none' });
}
});
},
stopScan() {
uni.stopBluetoothDevicesDiscovery({
success: () => {
this.isScanning = false;
console.log('停止扫描');
}
});
},
connectDevice(device) {
// 点击设备后停止扫描
this.stopScan();
// 跳转到连接页面或执行连接逻辑
uni.navigateTo({
url: `/pages/bluetoothDetail/bluetoothDetail?deviceId=${device.deviceId}&name=${device.name}`
});
}
},
onUnload() {
// 页面卸载时停止扫描以节省资源
this.stopScan();
}
};
</script>
<style>
.container {
padding: 20px;
}
button {
margin-bottom: 10px;
}
.device-list {
margin-top: 10px;
}
.device-item {
padding: 10px;
border-bottom: 1px solid #eee;
}
</style>
核心说明:
uni.startBluetoothDevicesDiscovery()
:开始扫描附近 BLE 设备。uni.onBluetoothDeviceFound(callback)
:监听到新设备时回调,返回如{ devices: [{ deviceId, name, RSSI, advertisData, advertisServiceUUIDs }] }
。device.deviceId
:唯一标识每个 BLE 设备,用于后续连接。- 过滤重复设备:微信小程序会多次上报同一设备,需自行去重(如示例中使用
deviceId
)。
4.3 停止扫描
- 一旦找到目标设备并准备连接,应及时调用
uni.stopBluetoothDevicesDiscovery()
停止扫描,否则会一直消耗手机资源和电量。 - 在页面
onUnload
或用户后退时,也应调用停止扫描,避免扫码界面卸载后仍在后台扫描。
五、连接蓝牙设备并发现服务
扫描到目标设备后,接下来要与该设备建立 BLE 连接,然后发现其提供的服务和特征。
5.1 创建 BLE 连接
进入设备详情页(例如 bluetoothDetail.vue
),在页面 onLoad
中获取从列表页传来的 deviceId
和 name
,然后调用 uni.createBLEConnection()
建立连接。
<template>
<view class="container">
<text>连接设备:{{ name }}</text>
<text v-if="connected">已连接</text>
<text v-else>正在连接...</text>
<button v-if="connected" @click="getServices">获取服务</button>
<view v-for="svc in services" :key="svc.uuid" class="service-item">
<text>服务 UUID:{{ svc.uuid }}</text>
<button @click="getCharacteristics(svc.uuid)">获取特征</button>
<view v-for="char in characteristicsList[svc.uuid] || []" :key="char.uuid" class="char-item">
<text>特征 UUID:{{ char.uuid }}</text>
<text>properties:{{ JSON.stringify(char.properties) }}</text>
<!-- 可根据 properties 选择读写/订阅 -->
<button v-if="char.properties.read" @click="readCharacteristic(svc.uuid, char.uuid)">读取</button>
<button v-if="char.properties.write" @click="writeCharacteristic(svc.uuid, char.uuid)">写入</button>
<button v-if="char.properties.notify" @click="notifyCharacteristic(svc.uuid, char.uuid)">订阅通知</button>
</view>
</view>
</view>
</template>
<script>
export default {
data() {
return {
deviceId: '',
name: '',
connected: false,
services: [],
characteristicsList: {} // 以 serviceUUID 为 key 存储特征列表
};
},
onLoad(options) {
// options.deviceId 和 options.name 来自扫描页
this.deviceId = options.deviceId;
this.name = options.name || '未知设备';
this.createConnection();
},
methods: {
createConnection() {
uni.createBLEConnection({
deviceId: this.deviceId,
success: (res) => {
console.log('createBLEConnection success', res);
this.connected = true;
// 监听连接状态变化
uni.onBLEConnectionStateChange((data) => {
console.log('连接状态 change:', data);
if (!data.connected) {
this.connected = false;
uni.showToast({ title: '设备已断开', icon: 'none' });
}
});
},
fail: (err) => {
console.error('createBLEConnection fail', err);
uni.showToast({ title: '连接失败', icon: 'none' });
}
});
},
getServices() {
uni.getBLEDeviceServices({
deviceId: this.deviceId,
success: (res) => {
console.log('getBLEDeviceServices', res);
this.services = res.services;
},
fail: (err) => {
console.error('getBLEDeviceServices fail', err);
}
});
},
getCharacteristics(serviceId) {
uni.getBLEDeviceCharacteristics({
deviceId: this.deviceId,
serviceId,
success: (res) => {
console.log('getBLEDeviceCharacteristics', res);
this.$set(this.characteristicsList, serviceId, res.characteristics);
},
fail: (err) => {
console.error('getBLEDeviceCharacteristics fail', err);
}
});
},
readCharacteristic(serviceId, charId) {
uni.readBLECharacteristicValue({
deviceId: this.deviceId,
serviceId,
characteristicId: charId,
success: (res) => {
console.log('read success', res);
// 监听数据返回
uni.onBLECharacteristicValueChange((charRes) => {
console.log('characteristic change', charRes);
// charRes.value 为 ArrayBuffer
const data = this.ab2hex(charRes.value);
console.log('读取到的数据(16进制)', data);
});
},
fail: (err) => {
console.error('read fail', err);
}
});
},
writeCharacteristic(serviceId, charId) {
// 示例:写入一个 0x01 0x02 的 ArrayBuffer 数据到特征
const buffer = new ArrayBuffer(2);
const dataView = new DataView(buffer);
dataView.setUint8(0, 0x01);
dataView.setUint8(1, 0x02);
uni.writeBLECharacteristicValue({
deviceId: this.deviceId,
serviceId,
characteristicId: charId,
value: buffer,
success: (res) => {
console.log('write success', res);
},
fail: (err) => {
console.error('write fail', err);
}
});
},
notifyCharacteristic(serviceId, charId) {
// 开启低功耗设备特征 notifications
uni.notifyBLECharacteristicValueChange({
state: true, // true: 启用通知;false: 关闭通知
deviceId: this.deviceId,
serviceId,
characteristicId: charId,
success: (res) => {
console.log('notify change success', res);
},
fail: (err) => {
console.error('notify change fail', err);
}
});
// 需监听 onBLECharacteristicValueChange 事件
uni.onBLECharacteristicValueChange((charRes) => {
console.log('notify char change', charRes);
const data = this.ab2hex(charRes.value);
console.log('notify 数据(16进制)', data);
});
},
// ArrayBuffer 转 hex 字符串,便于调试
ab2hex(buffer) {
const hexArr = Array.prototype.map.call(
new Uint8Array(buffer),
(byte) => byte.toString(16).padStart(2, '0')
);
return hexArr.join(' ');
},
disconnect() {
uni.closeBLEConnection({
deviceId: this.deviceId,
success: () => {
console.log('已断开连接');
this.connected = false;
}
});
}
},
onUnload() {
// 页面卸载时断开连接
if (this.connected) {
this.disconnect();
}
}
};
</script>
<style>
.container {
padding: 20px;
}
.service-item, .char-item {
margin-top: 10px;
padding: 10px;
border: 1px solid #eee;
}
button {
margin-top: 5px;
}
</style>
关键说明
uni.createBLEConnection({ deviceId })
:对指定deviceId
建立 BLE 连接,连接成功后才能读写。uni.onBLEConnectionStateChange(callback)
:实时监听设备连接状态,如果对方设备断电或超出范围会触发此回调。uni.getBLEDeviceServices({ deviceId })
:获取该设备上所有公开的服务(返回services: [{ uuid, isPrimary }]
)。uni.getBLEDeviceCharacteristics({ deviceId, serviceId })
:获取指定服务下的所有特征(返回characteristics: [{ uuid, properties: { read, write, notify, indicate } }]
)。读写特征:
- 读取:调用
uni.readBLECharacteristicValue({ deviceId, serviceId, characteristicId })
后,需要再使用uni.onBLECharacteristicValueChange(callback)
回调才能拿到数据。 - 写入:调用
uni.writeBLECharacteristicValue({ value: ArrayBuffer })
;注意写入数据必须是ArrayBuffer
。
- 读取:调用
- 订阅特征通知:调用
uni.notifyBLECharacteristicValueChange({ state: true, ... })
,然后在uni.onBLECharacteristicValueChange
中获得服务器推送的变化。 - 断开连接:
uni.closeBLEConnection({ deviceId })
,断开后需要调用uni.closeBluetoothAdapter()
释放蓝牙模块资源(可选)。
六、完整蓝牙流程 ASCII 图解
┌─────────────────────────────────────────────────┐
│ 用户打开“蓝牙页” │
└─────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────┐
│ 1. uni.openBluetoothAdapter() │
│ └──> 初始化蓝牙模块,开启本机 BLE 适配器 │
└─────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────┐
│ 2. uni.startBluetoothDevicesDiscovery() │
│ └──> 开始扫描附近 BLE 设备 │
└─────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────┐
│ 3. uni.onBluetoothDeviceFound(callback) │
│ └──> 回调返回扫描到的设备列表 devices[] │
│ devices 包含 deviceId、name、RSSI 等 │
└─────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────┐
│ 用户从列表中点击 “连接” 某设备 │
│ → 调用 uni.stopBluetoothDevicesDiscovery │
└─────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────┐
│ 4. uni.createBLEConnection({ deviceId }) │
│ └──> 与目标设备建立 BLE 连接 │
└─────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────┐
│ 5. uni.onBLEConnectionStateChange(callback) │
│ └──> 监听连接状态,如断开会触发 │
└─────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────┐
│ 6. uni.getBLEDeviceServices({ deviceId }) │
│ └──> 获取设备所有 Service 列表 │
└─────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────┐
│ 7. uni.getBLEDeviceCharacteristics({ │
│ deviceId, serviceId }) │
│ └──> 获取该 service 下的所有 Characteristic │
│ { uuid, properties: { read, write, notify, ... } } │
└─────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────┐
│ 8. 读/写/订阅 特征 │
│ ├─ uni.readBLECharacteristicValue(...) │
│ │ └─ onBLECharacteristicValueChange │
│ ├─ uni.writeBLECharacteristicValue(...) │
│ └─ uni.notifyBLECharacteristicValueChange(..│
│ └─ onBLECharacteristicValueChange │
└─────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────┐
│ 9. uni.closeBLEConnection({ deviceId }) │
│ └──> 断开与设备连接 │
└─────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────┐
│ 10. uni.closeBluetoothAdapter() (可选) │
│ └──> 关闭本机蓝牙模块,释放系统资源 │
└─────────────────────────────────────────────────┘
七、常见问题与注意事项
iOS 扫描需打开定位权限
- iOS 系统要求在使用 BLE 扫描前,必须打开地理位置权限,否则将无法扫描到任何设备。务必在
app.json
中声明scope.userLocation
并在运行时调用uni.authorize({ scope: 'scope.userLocation' })
。 示例:
uni.authorize({ scope: 'scope.userLocation', success: () => { // 已授权,继续扫描 this.startScan(); }, fail: () => { uni.showModal({ title: '提示', content: '需要开启定位权限才能扫描蓝牙设备', showCancel: false }); } });
- iOS 系统要求在使用 BLE 扫描前,必须打开地理位置权限,否则将无法扫描到任何设备。务必在
重连机制
如果设备断开连接,可监听
uni.onBLEConnectionStateChange
,在监听到connected: false
时尝试重连:uni.onBLEConnectionStateChange((res) => { if (!res.connected) { this.createConnection(); // 最简单的重连策略 } });
- 注意避免无限重连导致阻塞,可做一定次数或时延后重试。
写入数据长度限制
- BLE 单次写入的数据包长度有限制,通常最大约 20 字节(具体取决于设备 MTU)。如果需要写入更大数据,需要自行分包。
示例分包:
function writeInChunks(deviceId, serviceId, charId, dataBuffer) { const mtu = 20; // 一次最大写入 20 字节 let offset = 0; while (offset < dataBuffer.byteLength) { const length = Math.min(mtu, dataBuffer.byteLength - offset); const chunk = dataBuffer.slice(offset, offset + length); uni.writeBLECharacteristicValue({ deviceId, serviceId, characteristicId: charId, value: chunk }); offset += length; } }
订阅特征通知前必须先启用 notify
- 如果在调用
uni.onBLECharacteristicValueChange
前未调用uni.notifyBLECharacteristicValueChange({ state: true })
,则不会收到变化回调。
- 如果在调用
关闭蓝牙时先断开再关闭适配器
- 调用
uni.closeBLEConnection
后再调用uni.closeBluetoothAdapter()
,否则可能无法正常断开连接。
- 调用
不同平台 API 差异
- 支付宝小程序:方法名为
my.openBluetoothAdapter
、my.startBluetoothDevicesDiscovery
等,与微信小程序一致; - 百度小程序:对应
swan.openBluetoothAdapter
、swan.startBluetoothDevicesDiscovery
等; - 在 uniapp 中使用
uni.*
封装后自动映射,通常无需区分。
- 支付宝小程序:方法名为
断电、超距断开提醒
- 当设备主动断电或超出 BLE 范围时,会触发
onBLEConnectionStateChange
,需及时在 UI 上提示用户重新连接。
- 当设备主动断电或超出 BLE 范围时,会触发
RSSI(信号强度)过滤
在
onBluetoothDeviceFound
返回的device.RSSI
(信号强度)可以进行过滤,只展示接近的设备:if (device.RSSI > -70) { // 信号较强的设备,才加入列表 this.devices.push(device); }
八、总结
本文详细介绍了在 uniapp 小程序 中连接 BLE 设备的完整实战流程,从打开蓝牙适配器、扫描设备、连接设备、发现服务与特征、到读写订阅特征、断开连接的每一个环节,并提供了丰富的代码示例与 ASCII 流程图,帮助你更清晰地理解蓝牙通信的原理与步骤。
关键要点回顾:
- 初始化蓝牙适配器:
uni.openBluetoothAdapter()
并监听onBluetoothAdapterStateChange
,确保蓝牙可用。 - 扫描设备:
uni.startBluetoothDevicesDiscovery()
+uni.onBluetoothDeviceFound
,将多个蓝牙设备信息收集到列表,并去重。 - 建立 BLE 连接:
uni.createBLEConnection({ deviceId })
,并监听onBLEConnectionStateChange
。 - 发现服务与特征:
uni.getBLEDeviceServices
→uni.getBLEDeviceCharacteristics
,拿到可读写、可订阅的特征。 数据通信:
- 读取:
uni.readBLECharacteristicValue()
+uni.onBLECharacteristicValueChange
; - 写入:
uni.writeBLECharacteristicValue()
,注意分包; - 订阅通知:
uni.notifyBLECharacteristicValueChange({ state: true })
+uni.onBLECharacteristicValueChange
。
- 读取:
- 断开与清理:页面卸载或用户退出时,先
uni.closeBLEConnection
断开连接,再uni.closeBluetoothAdapter
关闭适配器,避免资源泄漏。 - 权限与异常处理:iOS 需授权定位才能扫描,蓝牙关闭或超距会触发回调;写入需要分包,二维码扫描时也同理。
评论已关闭