前言:UniApp提供了跨平台的蓝牙API,官方文档。最近有个需求,需要搜索并连接到对应的蓝牙设备,然后连接到对应的 wifi 网络,有同样需求的伙伴可以参考下。
本次功能主要使用到了以下几种蓝牙API:
本次功能涉及到两个页面:【本次ui页面的代码没有放出来,大家可以根据自己的实际需求进行调整】
一、第一个页面内涉及到的API:
页面初始值:
javascript
data() {
return {
bluetoothDeviceList: [],
isShowLoading: false,
};
},
onLoad() {
this.searchBluetoothDevice()
},
1. 初始化蓝牙设备。uni.openBluetoothAdapter()
javascript
// 初始化蓝牙设备
searchBluetoothDevice(){
let that = this
uni.openBluetoothAdapter({
success(res) {
console.log('openBluetoothAdapter success', res)
that.startBluetoothDevicesDiscovery()
},
fail(err) {
console.log('openBluetoothAdapter err=', err);
if (err.errCode === 10001) {
uni.$u.toast('请打开蓝牙')
}
}
})
},
2. 开始搜索附近的蓝牙设备。uni.startBluetoothDevicesDiscovery()
(此操作比较耗费系统资源,请在搜索并连接到设备后调用 uni.stopBluetoothDevicesDiscovery 方法停止搜索。)
javascript
// 搜索蓝牙设备
startBluetoothDevicesDiscovery() {
let that = this;
if (this.isShowLoading) {
this.stopBluetoothDevicesDiscovery()
return
}
this.isShowLoading = true
uni.startBluetoothDevicesDiscovery({
allowDuplicatesKey: true,
// services: ['0000abcd-0000-1000-8000-00805f9bffff'], //传入这个参数,只搜索主服务为该UUID的设备
success(res) {
console.log('startBluetoothDevicesDiscovery success', res)
that.onBluetoothDeviceFound()
setTimeout(() => {
console.log("----BluetoothDevicesDiscovery finish---- ");
if (that.isShowLoading){
that.stopBluetoothDevicesDiscovery()
}
}, 10000);
},
fail(err) {
console.log('startBluetoothDevicesDiscovery err=', err);
}
})
},
3. 监听已经搜索到的蓝牙设备。uni.onBluetoothDeviceFound()
(并且展示到页面上)
javascript
// 监听搜索到的蓝牙设备
onBluetoothDeviceFound() {
uni.onBluetoothDeviceFound((res) => {
res.devices.forEach(device => {
if (!device.name && !device.localName) {
return
}
const idx = this.bluetoothDeviceList.findIndex(d => d.deviceId === device.deviceId)
if (idx === -1) {
this.bluetoothDeviceList.push(device)
} else {
this.bluetoothDeviceList[idx] = device
}
})
})
},
4. 停止搜索蓝牙设备。uni.stopBluetoothDevicesDiscovery()
(搜索到之后或者搜索失败,都需要调用停止搜索蓝牙设备api)
javascript
// 停止蓝牙设备的搜索
stopBluetoothDevicesDiscovery() {
this.isShowLoading = false
uni.stopBluetoothDevicesDiscovery()
},
5. 关闭蓝牙模块。uni.closeBluetoothAdapter()
( 页面关闭时,调用该api )
javascript
onUnload() {
uni.closeBluetoothAdapter()
},
6. 连接低功耗蓝牙设备。uni.createBLEConnection()
javascript
// 点击列表里需要的蓝牙进行连接,蓝牙列表里可以拿到deviceId
createBLEConnection(deviceId){
uni.showLoading({
mask: true
})
let that = this
uni.createBLEConnection({
// 这里的 deviceId 需要已经通过 createBLEConnection 与对应设备建立链接
deviceId,
success(res) {
uni.hideLoading()
uni.$u.toast('连接成功')
setTimeout(()=>{
uni.$u.route(`/pages3/bluetooth/form?deviceId=${deviceId}`)
}, 1000)
},
fail(err) {
uni.hideLoading()
console.log('createBLEConnection err:', err)
}
})
},
二、第二个页面内涉及到的API:
页面初始值:
javascript
import { hexToUtf8Text, stringToArrayBuffer } from '@/util/hexUtils.js'
data() {
return {
deviceId: null,
globalServiceuuId: '0000abcd-0000-1000-8000-00805f9b34fb', // 自己设备提供的serviceId
globalWriteId: '0000abce-0000-1000-8000-00805f9b34fb', // 自己设备提供的写入需要的characteristicId
globalNotifyId: '0000abd0-0000-1000-8000-00805f9b34fb', // 自己设备提供的notify需要的characteristicId
ismy_service: false,
characteristicId: '',
serviceId: '',
};
},
onLoad({deviceId}) {
this.deviceId = deviceId
this.getBLEDeviceServices(deviceId)
},
1. 获取蓝牙设备所有服务(service)。uni.getBLEDeviceServices()
javascript
// 获取serviceId
getBLEDeviceServices(deviceId) {
let that = this
uni.getBLEDeviceServices({
deviceId,
success: (res) => {
console.log("service size = ", res.services.length)
for (let i = 0; i < res.services.length; i++) {
console.log(res.services[i].uuid, 'res.services[i].uuid');
if (this.globalServiceuuId.toUpperCase() == res.services[i].uuid){
that.getBLEDeviceCharacteristics(deviceId, res.services[i].uuid)
}
}
}
})
},
2. 获取蓝牙设备某个服务中所有特征值(characteristic)。uni.getBLEDeviceCharacteristics()
注:因为 2 3 4 涉及到的三个api写到一个了方法里,所以这三个模块的代码粘贴的一样。
javascript
// 获取characteristicId
getBLEDeviceCharacteristics(deviceId, serviceId) {
uni.getBLEDeviceCharacteristics({
deviceId,
serviceId,
success: (res) => {
var ismy_service = false
console.log("compute ", serviceId, this.globalServiceuuId.toUpperCase())
if (serviceId == this.globalServiceuuId.toUpperCase()) {
ismy_service = true
console.warn("this is my service ")
}
console.log('getBLEDeviceCharacteristics success', res.characteristics)
for (let i = 0; i < res.characteristics.length; i++) {
let item = res.characteristics[i]
// 该特征值是否支持 write 操作
if (item.properties.write) {
if (ismy_service && (this.globalWriteId.toUpperCase() == item.uuid)){
console.warn("find write uuid ready to ", item.uuid)
this.characteristicId = item.uuid
this.serviceId = serviceId
}
}
// 该特征值是否支持 notify 操作
if (item.properties.notify || item.properties.indicate) {
console.log("[Notify]", item.uuid)
if (ismy_service && (this.globalNotifyId.toUpperCase() == item.uuid)){
uni.notifyBLECharacteristicValueChange({ //开启通知
deviceId,
serviceId,
characteristicId: item.uuid,
state: true,
success(res) {
console.log('notifyBLECharacteristicValueChange success', res)
},
fail(err) {
console.warn("notifyBLECharacteristicValueChange err", err)
}
})
}
}
}
},
fail(err) {
console.error('getBLEDeviceCharacteristics err', err)
}
})
// 后端返回的数据-进行接收
// 操作之前先监听,保证第一时间获取数据
uni.onBLECharacteristicValueChange((res) => {
uni.showLoading({
mask: true
})
const buffer = res.value;
if (buffer.byteLength > 0) {
uni.hideLoading()
const uint8Arr = new Uint8Array(buffer); // 转成 Uint8Array 查看二进制值
const hexStr = Array.from(uint8Arr, byte => byte.toString(16).padStart(2, '0')).join(''); // 转16进制字符串
let resData = hexToUtf8Text(hexStr) // 16进制转换成文本
console.log(resData, '===resData');
if(resData==='A1'){
uni.$u.toast('连接成功')
}else if(resData==='A0'){
uni.$u.toast('连接失败')
}else if(resData==='B1'){
uni.$u.toast('检测联网成功')
}else if(resData==='B0'){
uni.$u.toast('检测联网失败')
}else if(resData==='B2'){
uni.$u.toast('检测联网超时')
}else{
uni.showToast({
title: resData,
icon:'none',
duration: 3000
})
}
}else{
uni.hideLoading()
}
})
},
3. 启用 notify 功能,订阅特征值。uni.notifyBLECharacteristicValueChange()
javascript
// 获取characteristicId
getBLEDeviceCharacteristics(deviceId, serviceId) {
uni.getBLEDeviceCharacteristics({
deviceId,
serviceId,
success: (res) => {
var ismy_service = false
console.log("compute ", serviceId, this.globalServiceuuId.toUpperCase())
if (serviceId == this.globalServiceuuId.toUpperCase()) {
ismy_service = true
console.warn("this is my service ")
}
console.log('getBLEDeviceCharacteristics success', res.characteristics)
for (let i = 0; i < res.characteristics.length; i++) {
let item = res.characteristics[i]
// 该特征值是否支持 write 操作
if (item.properties.write) {
if (ismy_service && (this.globalWriteId.toUpperCase() == item.uuid)){
console.warn("find write uuid ready to ", item.uuid)
this.characteristicId = item.uuid
this.serviceId = serviceId
}
}
// 该特征值是否支持 notify 操作
if (item.properties.notify || item.properties.indicate) {
console.log("[Notify]", item.uuid)
if (ismy_service && (this.globalNotifyId.toUpperCase() == item.uuid)){
uni.notifyBLECharacteristicValueChange({ //开启通知
deviceId,
serviceId,
characteristicId: item.uuid,
state: true,
success(res) {
console.log('notifyBLECharacteristicValueChange success', res)
},
fail(err) {
console.warn("notifyBLECharacteristicValueChange err", err)
}
})
}
}
}
},
fail(err) {
console.error('getBLEDeviceCharacteristics err', err)
}
})
// 后端返回的数据-进行接收
// 操作之前先监听,保证第一时间获取数据
uni.onBLECharacteristicValueChange((res) => {
uni.showLoading({
mask: true
})
const buffer = res.value;
if (buffer.byteLength > 0) {
uni.hideLoading()
const uint8Arr = new Uint8Array(buffer); // 转成 Uint8Array 查看二进制值
const hexStr = Array.from(uint8Arr, byte => byte.toString(16).padStart(2, '0')).join(''); // 转16进制字符串
let resData = hexToUtf8Text(hexStr) // 16进制转换成文本
console.log(resData, '===resData');
if(resData==='A1'){
uni.$u.toast('连接成功')
}else if(resData==='A0'){
uni.$u.toast('连接失败')
}else if(resData==='B1'){
uni.$u.toast('检测联网成功')
}else if(resData==='B0'){
uni.$u.toast('检测联网失败')
}else if(resData==='B2'){
uni.$u.toast('检测联网超时')
}else{
uni.showToast({
title: resData,
icon:'none',
duration: 3000
})
}
}else{
uni.hideLoading()
}
})
},
4. 监听低功耗蓝牙设备的特征值变化事件。uni.onBLECharacteristicValueChange()
javascript
// 获取characteristicId
getBLEDeviceCharacteristics(deviceId, serviceId) {
uni.getBLEDeviceCharacteristics({
deviceId,
serviceId,
success: (res) => {
var ismy_service = false
console.log("compute ", serviceId, this.globalServiceuuId.toUpperCase())
if (serviceId == this.globalServiceuuId.toUpperCase()) {
ismy_service = true
console.warn("this is my service ")
}
console.log('getBLEDeviceCharacteristics success', res.characteristics)
for (let i = 0; i < res.characteristics.length; i++) {
let item = res.characteristics[i]
// 该特征值是否支持 write 操作
if (item.properties.write) {
if (ismy_service && (this.globalWriteId.toUpperCase() == item.uuid)){
console.warn("find write uuid ready to ", item.uuid)
this.characteristicId = item.uuid
this.serviceId = serviceId
}
}
// 该特征值是否支持 notify 操作
if (item.properties.notify || item.properties.indicate) {
console.log("[Notify]", item.uuid)
if (ismy_service && (this.globalNotifyId.toUpperCase() == item.uuid)){
uni.notifyBLECharacteristicValueChange({ //开启通知
deviceId,
serviceId,
characteristicId: item.uuid,
state: true,
success(res) {
console.log('notifyBLECharacteristicValueChange success', res)
},
fail(err) {
console.warn("notifyBLECharacteristicValueChange err", err)
}
})
}
}
}
},
fail(err) {
console.error('getBLEDeviceCharacteristics err', err)
}
})
// 后端返回的数据-进行接收
// 操作之前先监听,保证第一时间获取数据
uni.onBLECharacteristicValueChange((res) => {
uni.showLoading({
mask: true
})
const buffer = res.value;
if (buffer.byteLength > 0) {
uni.hideLoading()
const uint8Arr = new Uint8Array(buffer); // 转成 Uint8Array 查看二进制值
const hexStr = Array.from(uint8Arr, byte => byte.toString(16).padStart(2, '0')).join(''); // 转16进制字符串
let resData = hexToUtf8Text(hexStr) // 16进制转换成文本
console.log(resData, '===resData'); // 这里是设备端返回的数据,根据情况进行提示
if(resData==='A1'){
uni.$u.toast('连接成功')
}else if(resData==='A0'){
uni.$u.toast('连接失败')
}else if(resData==='B1'){
uni.$u.toast('检测联网成功')
}else if(resData==='B0'){
uni.$u.toast('检测联网失败')
}else if(resData==='B2'){
uni.$u.toast('检测联网超时')
}else{
uni.showToast({
title: resData,
icon:'none',
duration: 3000
})
}
}else{
uni.hideLoading()
}
})
},
5. 向低功耗蓝牙设备特征值中写入二进制数据。uni.writeBLECharacteristicValue()
(这里写入数据成功之后,onBLECharacteristicValueChange这个api里面可以接收到设备端返回的数据,然后进行判断或提示)
javascript
// 一键连接
writeBLECharacteristicValue() {
uni.showLoading({
mask: true
})
let body = {
...this.form,
msgTag: "wifi_connect",
}
const str = JSON.stringify(body) // 这是我们设备端要求的参数,其他人可根据需求情况定
var buffer = stringToArrayBuffer(str)
uni.writeBLECharacteristicValue({
// 这里的 deviceId 需要在 getBluetoothDevices 或 onBluetoothDeviceFound 接口中获取
deviceId: this.deviceId,
// 这里的 serviceId 需要在 getBLEDeviceServices 接口中获取
serviceId: this.serviceId,
// 这里的 characteristicId 需要在 getBLEDeviceCharacteristics 接口中获取
characteristicId: this.characteristicId,
// 这里的value是ArrayBuffer类型
value: buffer,
success(res) {
// uni.hideLoading()
console.log('writeBLECharacteristicValue success', res)
},
fail(err) {
uni.hideLoading()
console.log('writeBLECharacteristicValue err', err)
uni.$u.toast('连接失败')
}
})
},
6. 断开与低功耗蓝牙的连接。uni.closeBLEConnection()
javascript
onUnload() {
if (this.deviceId) {
uni.closeBLEConnection({
deviceId: this.deviceId
})
}
},
提示:这里是转换二进制,或者16进制转换成可读文本用到的两个方法,单独封装到了utils里面。
javascript
// 1. 先定义 utf8Decode 函数(内部函数,无需导出,但需在调用前声明)
function utf8Decode(byteArray) {
let str = '';
let i = 0;
const len = byteArray.length;
while (i < len) {
if (byteArray[i] < 0x80) {
// 1字节字符(0xxxxxxx)
str += String.fromCharCode(byteArray[i]);
i++;
} else if (byteArray[i] >= 0xC0 && byteArray[i] < 0xE0) {
// 2字节字符(110xxxxx 10xxxxxx)
if (i + 1 >= len) break;
const charCode = ((byteArray[i] & 0x1F) << 6) | (byteArray[i + 1] & 0x3F);
str += String.fromCharCode(charCode);
i += 2;
} else if (byteArray[i] >= 0xE0 && byteArray[i] < 0xF0) {
// 3字节字符(1110xxxx 10xxxxxx 10xxxxxx)
if (i + 2 >= len) break;
const charCode = ((byteArray[i] & 0x0F) << 12) | ((byteArray[i + 1] & 0x3F) << 6) | (byteArray[i + 2] & 0x3F);
str += String.fromCharCode(charCode);
i += 3;
} else if (byteArray[i] >= 0xF0 && byteArray[i] < 0xF8) {
// 4字节字符(11110xxx 10xxxxxx 10xxxxxx 10xxxxxx)
if (i + 3 >= len) break;
let charCode = ((byteArray[i] & 0x07) << 18) | ((byteArray[i + 1] & 0x3F) << 12) | ((byteArray[i + 2] & 0x3F) << 6) | (byteArray[i + 3] & 0x3F);
if (charCode > 0xFFFF) {
// 处理UTF-16代理对
charCode -= 0x10000;
str += String.fromCharCode((charCode >> 10) + 0xD800, (charCode & 0x3FF) + 0xDC00);
} else {
str += String.fromCharCode(charCode);
}
i += 4;
} else {
// 无效字节,跳过
i++;
}
}
return str;
}
// 2. 再定义 hexToUtf8Text 函数(调用 utf8Decode,此时函数已声明)
export function hexToUtf8Text(hexStr) {
try {
const cleanHex = hexStr.replace(/\s/g, '').toLowerCase();
if (!/^[0-9a-f]+$/.test(cleanHex)) {
throw new Error('16进制格式错误:仅允许0-9、a-f字符');
}
if (cleanHex.length % 2 !== 0) {
throw new Error('16进制长度错误:需为偶数(每2位对应1个字节)');
}
// 16进制转字节数组
const byteLength = cleanHex.length / 2;
const byteArray = new Uint8Array(byteLength);
for (let i = 0; i < byteLength; i++) {
byteArray[i] = parseInt(cleanHex.substr(i * 2, 2), 16);
}
// 调用 utf8Decode(此时函数已存在,不会报"未找到"错误)
return utf8Decode(byteArray);
} catch (error) {
console.error('16进制转文本失败:', error);
return `转换失败:${error.message}`;
}
}
// 3. 转成二进制
export function stringToArrayBuffer(str) {
var bytes = new Array();
var len, c;
len = str.length;
for (var i = 0; i < len; i++) {
c = str.charCodeAt(i);
if (c >= 0x010000 && c <= 0x10ffff) {
bytes.push(((c >> 18) & 0x07) | 0xf0);
bytes.push(((c >> 12) & 0x3f) | 0x80);
bytes.push(((c >> 6) & 0x3f) | 0x80);
bytes.push((c & 0x3f) | 0x80);
} else if (c >= 0x000800 && c <= 0x00ffff) {
bytes.push(((c >> 12) & 0x0f) | 0xe0);
bytes.push(((c >> 6) & 0x3f) | 0x80);
bytes.push((c & 0x3f) | 0x80);
} else if (c >= 0x000080 && c <= 0x0007ff) {
bytes.push(((c >> 6) & 0x1f) | 0xc0);
bytes.push((c & 0x3f) | 0x80);
} else {
bytes.push(c & 0xff);
}
}
var array = new Int8Array(bytes.length);
for (var i in bytes) {
array[i] = bytes[i];
}
return array.buffer;
}
快乐学习!