尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

微信小程序蓝牙开发实战:BLE通信完整流程与踩坑指南

微信小程序蓝牙开发实战:BLE通信完整流程与踩坑指南 简介这是一份面向微信小程序开发者的学习型蓝牙通信实战Demo聚焦BLE设备交互核心流程解决初学者在小程序蓝牙API调用中常见的搜索失败、连接不稳定、特征值读写异常等典型问题。资源共17个文件包含4个JS逻辑文件实现蓝牙状态管理、设备发现与数据收发、3个WXML/WXSS页面组件搜索页与设备页UI、3个JSON配置文件页面路由与窗口设置、2个文本说明文件资源内容与标签说明、1个README.md项目文档、1个.gitignore版本控制配置及1张蓝牙图标PNG整体仅16KB轻量易导入。已有100人学习下载。读者可直接运行并快速掌握小程序蓝牙模块的完整链路从初始化、扫描、选择设备、连接服务、启用通知到字符串级数据写入与实时接收代码结构清晰search页面开箱即用device页面服务/特征已固化便于调试utils中封装了通用蓝牙工具方法是理解小程序硬件通信机制与工程化组织方式的优质入门参考。1. 项目概述提起微信小程序里的蓝牙功能很多人的第一反应是不就调一下蓝牙接口嘛能有多难结果一动手就发现事情完全不是这么回事。设备搜索不到、配对失败、数据发出去没反应、iOS 和安卓表现还不一样……桩桩件件都能把人折磨到怀疑人生。我手头正好有一套整理完的“微信小程序 蓝牙Demo”把低功耗蓝牙BLE从扫描到通信的完整流程串了起来分享给正准备入坑或者已经在坑里挣扎的朋友。这个Demo解决的核心问题很明确小程序端如何稳定地完成蓝牙设备的发现、连接、服务发现、特征值读写和通知接收。它适合三类人看一是刚接触小程序蓝牙开发、想快速跑通一个完整流程的初学者二是已经在写蓝牙功能但经常被各种兼容性问题卡住的前端工程师三是做硬件配套App、需要一份可参考的小程序端实现方案的开发者。Demo本身不大但流程是完整的能直接改改UUID和业务逻辑就用在真实项目里。简单说结论这套Demo走的是微信官方wx.openBluetoothAdapter到wx.writeBLECharacteristicValue的完整链路核心逻辑集中在连接管理和数据收发两个模块真机调试表现稳定在 iOS 和大部分安卓机型上都能正常跑通。下面我把整个实现思路、关键代码和踩过的坑一条条拆开讲。2. 整体设计与实现思路2.1 为什么选择小程序原生蓝牙 API在动手写代码之前先回答一个很多人会问的问题蓝牙功能是用小程序原生 API 做还是用 uni-app、Taro 这类跨端框架封装好的蓝牙插件我的建议是如果目标平台只有微信小程序直接上原生 API别绕弯子。原因有三个。第一原生 API 的调用链和微信官方文档完全对应。也就是说你打开文档搜wx.openBluetoothAdapter看到的说明和实际代码里的调用方式是一一对应的排错的时候查资料最省心。跨端框架虽然封装了统一接口但封装层一多遇到问题反而要先去翻框架源码或者提 issue定位问题成本高不少。第二蓝牙 API 的很多坑都跟平台底层实现有关原生 API 能让你直接感知到这些差异。比如 iOS 上系统弹窗的时机、安卓上定位权限和蓝牙权限的联动关系这些用原生 API 时更容易定位到底是小程序层的问题还是系统层的问题。第三原生 API 本身已经够简单了跨端框架带来的“统一性”收益在蓝牙这个场景下并不明显。我在实际项目里试过用 uni-app 封装蓝牙结果遇到一个诡异的问题安卓上onBLECharacteristicValueChange回调不稳定后来排查发现是框架层对原生事件的转发有 bug。这种坑在原生 API 下根本不存在。2.2 蓝牙通信的基本链路与核心概念要理解这个 Demo 的代码结构得先清楚 BLE 通信的基本逻辑。BLE 设备不像经典蓝牙那样需要配对后建立串口连接它的核心是GATT通用属性协议一句话概括就是设备暴露出一组服务Service每个服务下面有几个特征值Characteristic这些特征值就是数据的出入口。拿常见的蓝牙透传模块 HC-05/HC-06 来类比虽然它们更偏经典蓝牙但概念类似模块上电后广播自己手机扫描到之后发起连接连接成功后找到服务的 UUID然后往特征值里写数据模块就能把数据转发到串口。反过来模块从串口收到的数据会通过特征值的通知Notify机制传给手机。整个流程对应到小程序的 API 调用链是wx.openBluetoothAdapter()初始化蓝牙适配器。这是第一步适配器没打开后面所有操作都没法做。wx.startBluetoothDevicesDiscovery()开始扫描附近的蓝牙设备。wx.onBluetoothDeviceFound()监听扫描结果回调里能拿到设备列表。wx.createBLEConnection()连接目标设备。wx.getBLEDeviceServices()获取设备上所有的服务列表。wx.getBLEDeviceCharacteristics()获取指定服务下的特征值列表。wx.notifyBLECharacteristicValueChange()开启指定特征值的通知这样才能收到设备主动发来的数据。wx.writeBLECharacteristicValue()向特征值写入数据。wx.onBLECharacteristicValueChange()监听特征值变化也就是接收设备发来的数据。这套链路就是整个 Demo 的骨架。后面所有代码都是围绕这九个步骤展开的。2.3 Demo 的目录结构与模块划分我把 Demo 的代码按功能拆成了独立模块避免把所有逻辑堆在一个页面里。实际工程里的目录结构大致是├── pages │ └── index │ ├── index.js # 页面逻辑负责 UI 交互和蓝牙状态管理 │ ├── index.wxml # 页面结构 │ └── index.wxss # 样式 ├── utils │ ├── bluetooth.js # 蓝牙核心逻辑封装扫描、连接、收发 │ ├── buffer.js # 数据转换工具ArrayBuffer 转字符串等 │ └── constant.js # 全局常量UUID、指令定义等 └── app.js # 小程序入口其中bluetooth.js是核心所有蓝牙操作都封装成了 Promise 风格的方法页面层只需要关心调用和结果回调不需要关心实现细节。为什么要用 Promise 而不是直接回调因为蓝牙操作本身就是一连串有先后顺序的异步步骤Promise 的链式调用能让代码从上到下读起来像同步逻辑逻辑清晰且好维护。如果用回调嵌套扫描、连接、发现服务、发现特征值这四层嵌套能把人看吐。constant.js里放的是设备相关的关键参数。如果你的设备是 HC-05/HC-06 这类常见的蓝牙透传模块默认的服务 UUID 通常是0000FFE0-0000-1000-8000-00805F9B34FB特征值 UUID 是0000FFE1-0000-1000-8000-00805F9B34FB。但要注意不同厂家的模块 UUID 可能不一样有的用FFE0/FFE1有的用FFF0/FFF1甚至一些定制模块会直接用自定义的 UUID。拿到模块的第一件事就是查它的数据手册确认服务和特征值 UUID这个步骤错了后面连上了也收发不了数据。3. 核心代码实现与关键步骤解析3.1 初始化蓝牙适配器一切的前提openBluetoothAdapter是整个蓝牙操作的第一道门槛但我见过太多人在这上面翻车。最常见的错误是用户没打开手机蓝牙就直接调这个 API结果返回错误码10001蓝牙未打开。正确的做法是先检查系统蓝牙状态再决定要不要打开。这个 Demo 里我封装了一个initBluetooth方法逻辑如下// utils/bluetooth.js function initBluetooth() { return new Promise((resolve, reject) { wx.openBluetoothAdapter({ success: (res) { console.log(蓝牙适配器初始化成功, res); resolve(res); }, fail: (err) { console.error(蓝牙适配器初始化失败, err); // 错误码 10001 表示蓝牙未打开10002 表示不支持蓝牙 if (err.errCode 10001) { wx.showModal({ title: 提示, content: 请先打开手机蓝牙, showCancel: false, }); } else { wx.showModal({ title: 提示, content: 当前设备不支持蓝牙或蓝牙初始化失败, showCancel: false, }); } reject(err); }, }); }); }这里有个细节微信的蓝牙适配器在某些机型上需要用户授权定位权限才能扫描到设备。尤其是安卓 6.0 以上的系统蓝牙扫描往往会关联定位权限如果用户拒绝了定位授权扫描会一直返回空列表。所以我在初始化之后通常还会加一个校验逻辑在小程序启动时尝试调用wx.getSetting检查权限状态必要时引导用户去设置页打开授权。注意iOS 13 及以后版本对蓝牙权限管理更严格首次调用蓝牙相关 API 时系统会弹出蓝牙权限询问框用户必须选择“允许”否则后续操作全部失败。这个弹窗在开发者工具里看不到真机上才会出现调试时要留意。3.2 扫描设备如何精准过滤目标设备初始化完成之后就是扫描。扫描本身不复杂但问题是如果不加过滤扫描结果会非常杂乱周边所有在广播的 BLE 设备都会出现在列表里。这个 Demo 里我保留了两个过滤维度设备名称name和信号强度RSSI。具体代码// utils/bluetooth.js function startScan(filterName) { return new Promise((resolve, reject) { wx.startBluetoothDevicesDiscovery({ allowDuplicatesKey: false, // 默认 false不重复上报同一设备 success: (res) { console.log(开始扫描成功, res); resolve(res); }, fail: (err) { console.error(开始扫描失败, err); reject(err); }, }); // 监听新设备发现 wx.onBluetoothDeviceFound((res) { const devices res.devices; devices.forEach((device) { const deviceName device.name || device.localName || ; // 如果设置了过滤名称则只保留匹配的设备 if (filterName deviceName.indexOf(filterName) -1) { return; } // 用 deviceId 去重避免重复添加 if (!this.deviceList.find((item) item.deviceId device.deviceId)) { this.deviceList.push({ deviceId: device.deviceId, name: deviceName, RSSI: device.RSSI, }); } }); }); }); }几个关键点allowDuplicatesKey这个参数很多人会忽略。如果设为true同一个设备在广播期间会被重复上报你需要自己在回调里做去重设为false则只在新设备出现时上报一次。实测下来在设备密集的场景下设为false更省心但代价是回调里拿不到设备的实时 RSSI 变化。有些设备广播时不会上报name只有空字符串。遇到这种情况如果只按名称过滤设备会直接被漏掉。所以我加了device.localName作为备选再不行就干脆不过滤name而是让用户从列表里手动选择。扫描到设备之后deviceId是连接时唯一需要的标识一定不能丢。它通常是一串类似XX:XX:XX:XX:XX:XX的 MAC 地址格式在 iOS 上可能是 UUID 格式但不要关心格式直接用就行。扫描过程需要注意一个体验问题扫描不能一直开着。蓝牙扫描比较耗电而且会持续占用系统资源。正确做法是用户选了设备或者扫码成功后立即调用wx.stopBluetoothDevicesDiscovery()停止扫描。同时建议在页面onUnload或onHide时也停掉扫描避免切后台之后还在扫描。3.3 连接设备成功与失败的分水岭扫描到设备之后点击列表项就进入连接环节。连接的代码相对简单// utils/bluetooth.js function connectDevice(deviceId) { return new Promise((resolve, reject) { // 先停止扫描避免干扰连接 wx.stopBluetoothDevicesDiscovery({ complete: () { wx.createBLEConnection({ deviceId: deviceId, timeout: 10000, // 连接超时时间默认是 0表示不超时 success: (res) { console.log(连接成功, res); resolve(res); }, fail: (err) { console.error(连接失败, err); reject(err); }, }); }, }); }); }这里必须强调timeout参数。微信文档里写的是“超时时间单位为 ms默认不超时”但我在安卓上实测发现如果不设置超时某些异常设备会导致连接请求挂起很久都不返回用户只能干等。所以我一般会设为1000010 秒内连不上就提示失败。连接成功后并不意味着可以立即读写数据。接下来需要做两件事获取服务列表和获取特征值。这两步是连在一起的缺一不可。async function getServiceAndCharacteristic(deviceId) { try { // 1. 获取所有服务 const servicesRes await getBLEDeviceServices(deviceId); const services servicesRes.services; console.log(服务列表, services); // 2. 遍历服务找到我们需要的服务 UUID let targetService null; let targetCharacteristic null; for (const service of services) { if (service.uuid.toUpperCase() TARGET_SERVICE_UUID.toUpperCase()) { targetService service; break; } } if (!targetService) { throw new Error(未找到目标服务 UUID); } // 3. 从目标服务下获取特征值 const charRes await getBLEDeviceCharacteristics(deviceId, targetService.uuid); const characteristics charRes.characteristics; console.log(特征值列表, characteristics); for (const char of characteristics) { if (char.uuid.toUpperCase() TARGET_CHARACTERISTIC_UUID.toUpperCase()) { targetCharacteristic char; break; } } if (!targetCharacteristic) { throw new Error(未找到目标特征值 UUID); } // 4. 保存服务和特征值供后续读写使用 this.serviceId targetService.uuid; this.characteristicId targetCharacteristic.uuid; this.deviceId deviceId; this.isConnected true; // 5. 开启通知 await this.enableNotify(targetCharacteristic.uuid); return { serviceId: this.serviceId, characteristicId: this.characteristicId }; } catch (err) { console.error(获取服务或特征值失败, err); throw err; } }我习惯把服务发现和特征值发现合成一个步骤因为实际开发中这两步必然连续发生。而且这里有个容易被忽视的点服务列表里可能有好几个服务特征值列表里也可能有好几个特征值不能想当然地认为第一个就是你要的。一定要用 UUID 精确匹配宁可多写几行判断也不要赌顺序。我之前接过一个厂家定制的蓝牙模块服务列表里有三个服务特征值全在一个不起眼的服务下面如果只看第一个服务数据根本收不到。3.4 开启通知设备主动上报数据的开关连接成功后如果要接收设备主动发来的数据比如传感器读数、串口透传数据必须调用wx.notifyBLECharacteristicValueChange开启通知。这一步是新手最容易漏掉的漏掉之后的表现是手机给设备发数据没问题但设备发给手机的数据怎么也收不到。// utils/bluetooth.js function enableNotify(characteristicId) { return new Promise((resolve, reject) { wx.notifyBLECharacteristicValueChange({ deviceId: this.deviceId, serviceId: this.serviceId, characteristicId: characteristicId, state: true, // 开启通知 success: (res) { console.log(开启通知成功, res); // 监听特征值变化设备上报数据会触发这个回调 wx.onBLECharacteristicValueChange((changeRes) { const { value } changeRes; const data ab2hex(value); // ArrayBuffer 转十六进制字符串 this.onDataReceived this.onDataReceived(data); }); resolve(res); }, fail: (err) { console.error(开启通知失败, err); reject(err); }, }); }); }这里注册的wx.onBLECharacteristicValueChange回调是整个数据接收链路的核心。注意它的位置一定要在开启通知成功之后注册不能在页面初始化时就注册。因为回调注册是一种全局监听如果在连接前就注册一旦收到其他设备的数据逻辑会混乱。还有一点要注意wx.onBLECharacteristicValueChange注册的是全局回调同一个页面多次调用不会覆盖而是叠加。如果页面逻辑里开了多个连接回调会被触发多次。这个 Demo 简化了场景只维护单连接如果你要做多设备连接需要自己维护回调队列不然数据会串。3.5 数据发送与接收核心的字节处理BLE 的数据收发全部基于ArrayBuffer这对很多只写过 JavaScript 字符串操作的前端开发者来说是个坎。简单说ArrayBuffer是一块固定长度的二进制内存区域不能直接读写需要通过DataView或Uint8Array这类视图来操作。发送数据时我需要把字符串转成ArrayBuffer// utils/buffer.js function string2ArrayBuffer(str) { const bytes []; for (let i 0; i str.length; i) { bytes.push(str.charCodeAt(i) 0xff); } const buffer new ArrayBuffer(bytes.length); const dataView new DataView(buffer); for (let i 0; i bytes.length; i) { dataView.setUint8(i, bytes[i]); } return buffer; }接收数据时把ArrayBuffer转回字符串function arrayBuffer2String(buffer) { const uint8Array new Uint8Array(buffer); let str ; for (let i 0; i uint8Array.length; i) { str String.fromCharCode(uint8Array[i]); } return str; }实际使用中string2ArrayBuffer存在一个隐患当字符编码大于0xff时比如中文、Emoji按位与操作会丢失高位导致数据错误。如果你要传输的数据涉及中文不能简单用charCodeAt(0) 0xff得用TextEncoder先把字符串编码成 UTF-8 字节流。不过标准小程序基础库可能不支持TextEncoder这时可以引入一份 UTF-8 编码转换工具或者约定传输内容只用 ASCII 字符集。写入数据时还有一个非常关键的限制单次写入的字节数不能超过 20 字节。这是 BLE 协议层的限制不同设备可能略有差异但大多数标准 BLE 模块一次只能接收 20 字节的写请求。如果你的数据超过 20 字节需要自己拆包分多次写入每次写入之间加适当的延时一般 20-50ms否则数据包会丢失。Demo 里我写了一个拆包发送工具// utils/bluetooth.js function sendData(str) { const buffer string2ArrayBuffer(str); const chunkSize 20; // 每次最多 20 字节 const totalChunks Math.ceil(buffer.byteLength / chunkSize); for (let i 0; i totalChunks; i) { const start i * chunkSize; const end Math.min(start chunkSize, buffer.byteLength); const chunk buffer.slice(start, end); // 每个分包延迟 30ms 发送避免粘包 setTimeout(() { wx.writeBLECharacteristicValue({ deviceId: this.deviceId, serviceId: this.serviceId, characteristicId: this.characteristicId, value: chunk, success: (res) { console.log(第 ${i 1}/${totalChunks} 包发送成功, res); }, fail: (err) { console.error(第 ${i 1}/${totalChunks} 包发送失败, err); }, }); }, i * 30); } }注意用setTimeout做分包延时只是最基础的做法。真实项目中更稳妥的方案是等上一包写入成功后再发下一包即在success回调里递归发送下一包而不是盲目地定时发送。因为某些蓝牙模块对接收缓冲区的处理能力有限如果写入速度太快即使单包不超过 20 字节也会出现丢包。3.6 设备断开与资源清理蓝牙连接不是一次性的用户可能随时切换设备、退出页面、或者微信切后台。这些场景下如果不做清理会出现连接泄漏之后重新连接会失败。清理的要点有三个// utils/bluetooth.js function closeConnection() { // 1. 关闭通知 if (this.deviceId this.characteristicId) { wx.notifyBLECharacteristicValueChange({ deviceId: this.deviceId, serviceId: this.serviceId, characteristicId: this.characteristicId, state: false, complete: () {}, }); } // 2. 断开蓝牙连接 if (this.deviceId) { wx.closeBLEConnection({ deviceId: this.deviceId, complete: () { this.deviceId null; this.serviceId null; this.characteristicId null; this.isConnected false; }, }); } // 3. 关闭蓝牙适配器可选 wx.closeBluetoothAdapter({ complete: () {}, }); }这里有个细节断开连接时我把this.deviceId等状态置空是为了防止下次连接时残留旧状态。有些同学不清理状态结果连接新设备时代码里还拿着旧设备的deviceId去读数据白白踩坑。另外wx.onBLEConnectionStateChange这个监听方法要注意注册时机和注销时机。当连接意外断开时比如设备超出范围、设备关电微信会回调给这个监听器。Demo 里我在初始化蓝牙时注册了一个全局的onBLEConnectionStateChange回调里判断connected字段是否为false如果是就更新 UI 并清理状态。4. 常见问题与排查技巧实录4.1 搜索不到设备搜索不到设备的排查链路我总结成一张表排查点检查方法解决方案手机蓝牙是否打开系统设置查看蓝牙状态打开蓝牙后重试定位权限是否授权小程序设置页查看权限引导用户授权定位权限设备是否在广播用第三方蓝牙调试工具如 nRF Connect扫描确认设备端重新上电确认广播开启设备是否被过滤看代码里是否设置了filterName临时去掉过滤条件查看所有设备iOS 蓝牙权限是否开启系统设置-隐私-蓝牙授权后重启小程序其中最常见的就是定位权限问题。安卓上第一次调用startBluetoothDevicesDiscovery时系统可能会弹出定位权限请求如果用户点了拒绝扫描会静默失败界面没有任何报错设备也扫不到。这种问题特别坑因为错误提示非常不明显。我后来在代码里加了一个权限预检查在进入蓝牙页面时先用wx.getSetting查一下scope.userLocation的授权状态如果被拒绝就弹窗引导用户去设置页打开。4.2 扫描到设备但连不上这个问题分两种场景第一种是扫描到了设备但createBLEConnection直接报错。常见错误码有-1未知错误和10004连接失败。-1在安卓上出现概率较高通常是因为设备协议栈有问题或者系统蓝牙服务异常最简单的处理方式是提示用户关闭并重新打开蓝牙再重新扫描连接。第二种是连接回调显示成功但之后getBLEDeviceServices拿到的服务列表是空的。这种情况多见于一些低成本的 BLE 模块它们在连接后需要几十到几百毫秒准备 GATT 服务表。解决方法是连接成功后加一个短暂延时await sleep(300)再获取服务列表。不要觉得延时是土办法在蓝牙开发里适当的延时是规避竞态条件的常见手法。4.3 数据发送成功但设备没反应这个问题的本质是你往一个“可写”的特征值写数据但那个特征值并不是设备真正监听的那个。排查步骤确认你写入的特征值 UUID 是正确的。用 nRF Connect 这类工具连接设备查看服务和特征值确认数据手册里写的 UUID 和实际设备一致。确认特征值具备“Write”属性。每个特征值有properties字段里面有read、write、notify等属性。如果特征值不具备 write 属性写入会失败或无效。在小程序里可以通过getBLEDeviceCharacteristics返回的properties字段判断。确认数据格式符合设备要求。有些设备要求特定帧头帧尾比如AA 55开头、校验和结尾。直接发纯文本可能被设备视为无效数据丢弃。4.4 设备数据收不到“收不到数据”的分支比较多我挨个说明没有开启通知刚说了notifyBLECharacteristicValueChange必须调用state 必须为true且必须在连接成功后、读数据之前完成。注册监听时机不对wx.onBLECharacteristicValueChange要在通知开启成功后再调用wx.onBLECharacteristicValueChange去监听。有个坑是先调用notify再注册监听中间设备恰好发来一条数据这条数据就会因为监听未就绪而丢失。稳妥的做法是把监听注册放在notifyBLECharacteristicValueChange的success回调里保证时序。特征值属性不对接收数据依赖的是 Notify 属性不是 Read 属性。如果设备端的特征值不具备 Notify 属性小程序的notifyBLECharacteristicValueChange调用会失败或者成功了也收不到数据。用工具确认特征值的 properties 里是否有notify: true。安卓 6.0 以上设备名偶发为空这个问题比较隐蔽。某些安卓设备在扫描到 BLE 设备后回调里的name字段会为空但设备的localName字段可能有值。如果你在列表里显示设备名要注意兼容device.name || device.localName。4.5 蓝牙在 iOS 上比安卓上更难调通这是很多开发者的共识。iOS 对 BLE 的管控比安卓严格得多主要体现在iOS 上蓝牙的deviceId不是 MAC 地址而是系统生成的 UUID这意味着你无法跨会话记住某个设备重启小程序后 deviceId 会变化。所以 iOS 上“记住上次连接设备并自动重连”的需求实现起来比较麻烦需要在应用层用其他方式识别设备比如连接后读取设备的广播数据或特定特征值来确认身份。iOS 上wx.openBluetoothAdapter之后如果系统蓝牙权限弹窗被用户拒绝之后所有蓝牙 API 都会失败而且失败的错误信息比较模糊。必须引导用户去系统设置里打开权限。iOS 上onBLECharacteristicValueChange的回调频率比安卓低如果设备高频上报数据iOS 可能会有一定丢包。这个属于系统层面限制只能通过调整设备端上报频率或增加分包重传机制来缓解。4.6 用开发者工具调试蓝牙的正确姿势微信开发者工具里有个“模拟蓝牙”的功能但说实话它只能用来验证流程不能用来验证真实设备交互。因为模拟接口不涉及真实的 BLE 协议栈很多错误码都不一致。我的经验是开发者工具只用来调试 UI 和逻辑比如扫描列表显示、按钮状态切换、数据收发界面的布局。真机调试务必使用wx.getSystemInfoSync()确认基础库版本尽量使用2.12.0以上的版本蓝牙相关 API 在这个版本之后稳定了很多。真机调试时建议打开调试器的“网络”和“Console”把wx.onBluetoothDeviceFound回调里的原始数据打出来看能发现很多 UI 层看不到的问题。5. 工程化封装与项目实战扩展5.1 把蓝牙逻辑抽象成单例 Store这个 Demo 为了降低理解门槛把蓝牙逻辑直接写在utils/bluetooth.js里。但真实项目中如果数据收发状态要跨页面共享比如设备列表页选完设备跳到控制页面发指令直接用全局变量或者getApp()挂载会变得难以维护。我的建议是把蓝牙模块封装成单例或者用一个简单的全局 EventBus 来分发数据事件。Demo 里我用了最朴素的方案// app.js const bluetoothManager require(./utils/bluetooth.js); App({ onLaunch() { this.globalData.bluetoothManager bluetoothManager; }, });这样任意页面都能通过getApp().globalData.bluetoothManager访问同一个蓝牙实例连接状态、已选设备、收到的数据都能共享。如果你的项目用了 MobX 或 Vuex 类似的状态管理也可以把蓝牙状态放进去但核心思路不变蓝牙连接实例全局唯一页面只负责展示和触发。5.2 自动重连机制的设计实际项目里用户的蓝牙设备经常因为断电、超远距离等原因断开连接。一个体验好的小程序应该有自动重连机制。但自动重连不能无脑做不然设备不在范围内时会陷入死循环。我采用的是“有限重试策略”监听onBLEConnectionStateChange当检测到断线时先弹出提示“蓝牙已断开正在重连...”然后启动一个重连计数器。最多重试 3 次每次间隔 1 秒、2 秒、4 秒指数退避。如果 3 次都失败就提示用户手动重新连接。这个策略避免了设备不在附近时无限重试耗尽电量。// 伪代码示例 wx.onBLEConnectionStateChange((res) { if (!res.connected this.isConnected) { this.retryCount 0; this.reconnect(); } }); function reconnect() { if (this.retryCount 3) { this.showReconnectFail(); return; } this.retryCount; const delay Math.pow(2, this.retryCount) * 1000; // 2s, 4s setTimeout(async () { try { await this.connectDevice(this.lastDeviceId); await this.getServiceAndCharacteristic(this.lastDeviceId); console.log(重连成功); } catch (err) { this.reconnect(); } }, delay); }5.3 蓝牙透传之外的常见业务扩展这个 Demo 的基础能力是 BLE 读写实际项目中你还会遇到这些扩展需求蓝牙打印小程序蓝牙打印机的原理其实就是向打印机模块的特征值写入排版好的打印指令通常是 ESC/POS 指令。核心还是writeBLECharacteristicValue区别在于数据必须按指令格式组装。最常见的坑是打印指令里包含大量二进制数据不能用字符串拼接必须用ArrayBuffer按字节组装。OTA 固件升级这个相对复杂需要分包发送固件文件每包都有序号和校验设备端收到后回 ACK。小程序的writeBLECharacteristicValue单次 20 字节限制在这里非常致命所以必须实现一个可靠的流式传输协议。好消息是只要你能把控好分包和 ACK 机制小程序完全可以胜任 OTA 功能。多设备连接微信小程序支持同时连接多个 BLE 设备数量取决于系统限制核心是在事件处理时区分deviceId。我处理过多设备场景一个页面同时控制两块蓝牙板卡用MapdeviceId, ConnectionState管理状态比用单个字段优雅得多。MQTT 桥接如果你需要把小程序的蓝牙数据转发到服务器一般做法是小程序通过wx.request把收到的数据 POST 到后端后端再通过 MQTT 发给设备管理平台。这里要注意频率控制蓝牙设备高频上报时不能每条数据都直接发 HTTP 请求不然请求量大到后端扛不住。合理做法是在小程序端做节流、聚合比如每 1 秒批量上传一次。5.4 兼容性测试清单最后整理一份我在上线前必跑的兼容性测试清单照着测一遍能避开大部分线上问题测试项测试场景预期结果基础流程首次打开小程序授权蓝牙扫描设备连接收发数据全流程无异常蓝牙关闭状态系统蓝牙关闭时进入蓝牙页提示打开蓝牙无白屏或死循环定位权限拒绝拒绝定位授权后扫描提示授权引导不静默失败设备断电连接过程中拔掉设备电源提示连接失败或者断线重连页面切后台连接成功后切后台再切回来蓝牙连接状态正常无异常断开页面销毁连接状态下退出页面连接关闭无泄漏多次连接连续连接、断开不同设备 10 次无累积错误状态正确大数据传输发送超过 300 字节数据分包发送成功无丢包低电量模式手机开启低电量模式蓝牙功能正常部分安卓机型会限制扫描这 9 项是我每次改完蓝牙代码必跑的回归项。尤其是“多次连接”这一项最容易暴露状态清理不干净的问题。6. 写在最后的几个实战心得做小程序蓝牙开发这两三年我最大的体会是蓝牙功能本身不难难的是把各种异常情况考虑周全。微信的蓝牙 API 表面上只有几个方法但每个方法在不同机型、不同系统版本上的表现都有细微差别。写这套 Demo 的时候我把常见的问题都提前处理了但真到上线前还是建议你拿真机多测几个品牌尤其是华为、小米、iPhone 这三个主流阵营覆盖低端机和旗舰机。另外一个很实用的经验是调试蓝牙问题手机上的第三方工具比微信开发者工具靠谱得多。像 nRF Connect、LightBlue 这些工具可以让你绕过微信小程序直接查看设备上的服务和特征值快速判断是设备问题还是小程序代码问题。我遇到过几次设备连上但拿不到服务列表的诡异问题就是用 nRF Connect 一看才发现是设备端 GATT 服务没准备好跟小程序没关系。如果你决定把这套 Demo 用到自己的项目里建议先从最简单的场景跑通手机连上一个蓝牙透传模块用微信小程序发一串 ASCII 字符串设备端能通过串口收到设备端通过串口发数据小程序能收到并显示。把这条路跑通之后再逐步加业务逻辑比如自定义协议、分包重传、OTA、多设备管理。不要一上来就想着做大而全蓝牙的调试链路长问题定位成本高小步快跑才是正道。这套 Demo 的代码结构、逻辑设计和踩坑记录基本都在上面了。如果你正在做类似的功能希望这份内容能帮你少走点弯路。有具体问题也欢迎留言交流我在不忙的时候会尽量回复。本文还有配套的精品资源点击获取
返回列表