uniAPP 蓝牙API连接流程
// ==================== 工具函数 ====================// ArrayBuffer 转 16 进制数组(用于显示收到的数据)export function ab2hex(buffer) {const hexArr = Array.prototype.map.call(new Uint8Array(buffer),function(bit) {return ('00' + bit.toString(16)).slice(-2)})return hexArr}// 字符串转 ArrayBuffer(发送文本指令用)export function string2ArrayBuffer(str) {let buffer = new ArrayBuffer(str.length)let dataView = new Uint8Array(buffer)for (let i = 0; i < str.length; i++) {dataView[i] = str.charCodeAt(i)}return buffer}// 16进制字符串转 ArrayBuffer(发送 hex 指令用)export function hexString2ArrayBuffer(hexString) {hexString = hexString.replace(/\s/g, '')let buffer = new ArrayBuffer(hexString.length / 2)let dataView = new Uint8Array(buffer)for (let i = 0; i < hexString.length; i += 2) {dataView[i/2] = parseInt(hexString.substr(i, 2), 16)}return buffer}
二、BLE 蓝牙连接流程
// ==================== 蓝牙连接流程 ====================data(){return {deviceId: '', // 设备IDserviceId: '', // 服务IDwriteCharId: '', // 写特征值IDnotifyCharId: '', // 通知特征值IDconnected: false // 连接状态}},methods:{/*** 1. 初始化蓝牙适配器* 这是所有蓝牙操作的第一步,相当于打开手机的蓝牙功能开关。* 如果失败,通常是因为手机蓝牙未开启,或者应用未获得相关权限。*/initBluetooth() {uni.openBluetoothAdapter({success: (res) => {console.log('初始化蓝牙成功', res)this.startDiscover()},fail: (err) => {console.error('初始化蓝牙失败', err)// 常见原因:手机未开启蓝牙、位置权限未开启uni.showToast({title: '请开启手机蓝牙',icon: 'none'})}})},/*** 2. 先监听设备发现,再开始扫描** 重要:很多开发者在这里顺序搞反了!* 必须先注册监听函数,再开始扫描。否则在监听函数注册之前扫描到的设备,* 你的程序是收不到的,就会导致“明明设备在那里,就是扫不到”的诡异问题。*/startDiscover() {// 先注册监听(重要!必须先监听后扫描)uni.onBluetoothDeviceFound((res) => {const devices = res.devicesconsole.log('发现设备', devices)// 这里可以根据设备名筛选目标设备devices.forEach(device => {if (device.name && device.name.includes('目标设备名')) {console.log('找到目标设备', device)this.stopScanAndConnect(device.deviceId)}})})// 开始扫描uni.startBluetoothDevicesDiscovery({allowDuplicatesKey: false, // 是否允许重复上报success: (res) => {console.log('开始扫描设备', res)// 设置扫描超时(比如10秒)setTimeout(() => {this.stopScan()}, 10000)},fail: (err) => {console.error('启动扫描失败', err)}})},/*** 3. 停止扫描* 找到目标设备后应立即停止扫描,既省电又避免干扰后续的连接操作。*/stopScan() {uni.stopBluetoothDevicesDiscovery({success: (res) => {console.log('停止扫描', res)}})},/*** 4. 找到设备后停止扫描并连接*/stopScanAndConnect(deviceId) {this.stopScan()this.connectDevice(deviceId)},/*** 5. 连接蓝牙设备* 使用扫描到的 deviceId 建立连接。这个 deviceId 是设备的唯一标识,* 后续所有操作都要用到它,一定要保存好。*/connectDevice(deviceId) {uni.createBLEConnection({deviceId: deviceId,success: (res) => {console.log('连接设备成功', res)this.setData({deviceId,connected: true})// 6. 设置MTU(Android必做!)this.setMTU(deviceId)},fail: (err) => {console.error('连接设备失败', err)}})},/*** 6. 设置MTU(关键步骤)** MTU 是“最大传输单元”,决定了你一次能发送多少字节的数据。* Android 设备默认 MTU 只有 20 字节,如果不设置大一点,发稍长的指令就会被截断。* iOS 会自动协商,但 Android 必须手动设置。** 注意:setBLEMTU 在部分平台可能不支持,需要做兼容处理。*/setMTU(deviceId) {// 注意:setBLEMTU 在微信小程序中可能不支持,UniApp 需根据平台判断// #ifdef APP-PLUSuni.setBLEMTU({deviceId: deviceId,mtu: 128, // 通常设为128,部分设备支持512success: (res) => {console.log('设置MTU成功', res)this.getServices(deviceId)},fail: (err) => {console.log('设置MTU失败,继续获取服务', err)this.getServices(deviceId)}})// #endif// #ifdef H5 || MP - WEIXINthis.getServices(deviceId)// #endif},/*** 7. 获取设备服务** 蓝牙设备可以包含多个服务(Service),每个服务相当于一个功能模块。* 比如有的服务负责电量显示,有的负责数据传输。* 我们需要找到负责数据通信的那个服务,通常 UUID 以 FFF0、FFE0 等开头。*/getServices(deviceId) {uni.getBLEDeviceServices({deviceId: deviceId,success: (res) => {console.log('获取服务列表', res.services)// 找到需要的服务(通常根据UUID筛选)const services = res.services// 示例:假设目标服务的UUID以 'FFF0' 开头const targetService = services.find(s =>s.uuid.toUpperCase().includes('FFF0'))if (targetService) {this.setData({serviceId: targetService.uuid})this.getCharacteristics(deviceId, targetService.uuid)} else {console.error('未找到目标服务')}},fail: (err) => {console.error('获取服务失败', err)}})},/*** 8. 获取特征值** 每个服务下面包含多个特征值(Characteristic),* 特征值才是真正读写数据的地方。我们需要找到:* - 写特征值(write):用来给设备发指令* - 通知特征值(notify):用来接收设备主动发来的数据*/getCharacteristics(deviceId, serviceId) {uni.getBLEDeviceCharacteristics({deviceId: deviceId,serviceId: serviceId,success: (res) => {console.log('获取特征值列表', res.characteristics)const chars = res.characteristics// 找到写特征值(properties.write 为 true)const writeChar = chars.find(c => c.properties.write)// 找到通知特征值(properties.notify 为 true)const notifyChar = chars.find(c => c.properties.notify)if (writeChar) {this.setData({writeCharId: writeChar.uuid})}if (notifyChar) {this.setData({notifyCharId: notifyChar.uuid})// 9. 先监听数据(重要!)this.listenData()// 10. 再开启notifythis.enableNotify(deviceId, serviceId, notifyChar.uuid)}},fail: (err) => {console.error('获取特征值失败', err)}})},/*** 9. 监听设备返回的数据** 这个监听器要放在开启 notify 之前注册,否则设备发来的第一包数据可能会漏掉。* 收到的数据是 ArrayBuffer 格式,需要用 ab2hex 转成可读的十六进制字符串。*/listenData() {uni.onBLECharacteristicValueChange((res) => {// 收到的数据是 ArrayBuffer 格式const hexData = ab2hex(res.value)console.log('收到数据', hexData)// 也可以转成字符串(如果是文本数据)// const strData = String.fromCharCode.apply(null, new Uint8Array(res.value))// 更新到页面显示this.setData({receivedData: hexData.join(' ')})})},/*** 10. 开启notify服务** 只有开启了 notify,设备才会主动把数据推送给手机。* 这就像是你跟设备说:“好了,现在有什么消息随时告诉我吧。”*/enableNotify(deviceId, serviceId, characteristicId) {uni.notifyBLECharacteristicValueChange({state: true,deviceId: deviceId,serviceId: serviceId,characteristicId: characteristicId,success: (res) => {console.log('开启notify成功', res)uni.showToast({title: '蓝牙连接成功',icon: 'success'})},fail: (err) => {console.error('开启notify失败', err)}})},/*** 11. 发送指令** 支持两种格式:* - 文本格式:直接发 "AT+CMD" 这样的字符串* - 十六进制格式:发 "A1 B2 C3" 这样的 hex 数据** @param {string} command - 指令内容(文本或hex字符串)* @param {string} type - 指令类型 'text' 或 'hex'*/sendCommand(command, type = 'text') {if (!this.data.connected) {uni.showToast({title: '蓝牙未连接',icon: 'none'})return}// 转换数据格式let bufferif (type === 'hex') {buffer = hexString2ArrayBuffer(command)} else {buffer = string2ArrayBuffer(command)}uni.writeBLECharacteristicValue({deviceId: this.data.deviceId,serviceId: this.data.serviceId,characteristicId: this.data.writeCharId,value: buffer,success: (res) => {console.log('指令发送成功', command)// 注意:设备的回复数据会触发 onBLECharacteristicValueChange// 不要在这里等待回复,回复会通过步骤9的监听器收到},fail: (err) => {console.error('指令发送失败', err)}})},/*** 12. 断开连接(退出页面时必须调用)** 很多新手开发者忘了这一步,结果下次打开小程序发现蓝牙连不上了,* 或者手机蓝牙被占用导致其他设备连不上。* 一定要在页面卸载时释放资源!*/disconnectBluetooth() {if (this.data.deviceId) {// 关闭连接uni.closeBLEConnection({deviceId: this.data.deviceId,success: (res) => {console.log('断开连接成功', res)}})}// 关闭蓝牙适配器uni.closeBluetoothAdapter({success: (res) => {console.log('关闭蓝牙适配器', res)this.setData({connected: false})}})},} ,
夜雨聆风