ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

微信小程序BLE蓝牙Dem实战:从连接、分包到Android/iOS兼容

微信小程序BLE蓝牙Dem实战:从连接、分包到Android/iOS兼容 简介这是一份面向微信小程序开发者的基础蓝牙通信实践Demo聚焦BLE设备搜索、连接、字符串写入与通知读取等核心功能适用于智能硬件联动、串口透传模块调试等IoT开发场景尤其适合刚接触小程序蓝牙API、需规避常见踩坑点的中初级开发者。压缩包共17个文件16KB涵盖4个JS逻辑文件含app.js与页面业务逻辑、3个WXML/WXSS界面文件、3个JSON配置文件页面与全局配置、2个说明类TXT文档、1个README.md项目指南及.gitignore等工程规范文件目录结构清晰pages分层明确utils封装可复用逻辑便于快速理解小程序蓝牙模块组织方式。已有100人学习下载读者可直接运行搜索页实现零配置设备发现参考device页中已固化的服务与特征UUID完成数据收发并基于字符串协议按需扩展指令格式同时获得完整的小程序标准目录骨架、蓝牙状态管理思路及BLE串口模块实测验证经验。 微信小程序做蓝牙市面上能搜到的Demo不少但大多数只给你一个扫描列表和几个接口调用真正涉及业务联调时Android和iOS的差异、数据包分包、服务发现流程、权限处理这些坑一个比一个深。我自己从零搭过几套完整的蓝牙小程序方案包括后面还会展开的ESP32控制类应用所以对这个标题下的内容特别有共鸣。这个Demo到底能解决什么问题适合谁参考我先说清楚它本质上是一套“微信小程序连接BLE低功耗蓝牙设备”的完整链路覆盖了打开蓝牙适配器、扫描设备、建立连接、发现服务和特征值、写入指令、接收硬件主动上报数据以及常见异常处理。无论你要做的是智能灯、遥控小车、温湿度传感器还是工程巡检类的信息采集系统这套链路都是绕不开的地基。这篇文章我会把我实际踩过的坑、改过的代码、总结出的排查思路全部写出来保证不是那种只贴接口文档的伪教程。1. 项目整体设计与思路拆解1.1 为什么小程序侧只能走BLE通道很多第一次接触小程序蓝牙开发的人第一个疑问就是为什么我买了一个HC-05蓝牙模块却死活连不上答案在小程序的能力边界上。微信小程序开放给开发者的蓝牙API只覆盖低功耗蓝牙BLEBluetooth Low Energy协议栈而HC-05、HC-06这类的经典蓝牙串口透传模块走的是SPP协议二者根本不在一个技术体系里。BLE的通信模型是GATT也就是服务和特征值组成的树状结构设备通过广播包宣告自己存在连接后客户端通过读写特征值和订阅通知来交换数据而SPP就是纯粹的串口透传没有这些分层模型。所以如果你手上的硬件模块是HC-05、HC-06在小程序里基本可以放弃适配了这些模块通常被用于手机App和单片机的串口通信但场景固定为经典蓝牙通道。做小程序端的话建议优先选HM-10、CC2541、nRF52832这些支持BLE从机模式的模块或者直接用ESP32它的BLE功能非常成熟而且开发资料多串口日志输出也方便。选型这个事情一定要在项目启动前确认否则等你代码写完了才发现协议栈不兼容整个方案都得推翻。从架构设计角度讲小程序的BLE能力可以用一条主线概括扫描发现 → 连接设备 → 发现服务 → 获取特征值 → 读写或订阅。理解这条主线后你写代码时才有全局观知道每一步到底在做哪一件事、为什么必须按这个顺序来。后续我会按这条主线拆开讲。1.2 这个Demo适合什么场景和硬件一个实用的蓝牙Demo通常不只是“连接一下”就完事它必须能落地到自己手头硬件的控制逻辑上。我拆解过这个Demo的常见用途大概能覆盖三类场景。第一类是控制类最典型的就是用小程序给硬件发指令比如控制ESP32开发板上的LED开关、电机转动、智能窗帘启闭。这种场景的特点是数据量小、实时性要求高指令通常就是几个字节的协议帧比如“0xA5 0x01 0x00 0x00 0x5A”代表开灯。第二类是数据采集类硬件端周期性地把传感器读数上报到小程序比如温湿度、心率、电量、姿态角等。这类场景的特点是数据是硬件主动推上来的小程序侧需要订阅特征值通知并且要处理好粘包和分包问题。第三类是设备调试类通过小程序查看蓝牙设备暴露的所有服务和特征值相当于把nRF Connect这类工具做成了小白可用的界面常用于产品开发初期的自测。硬件选型上我给出一个对比建议方便你结合手里的设备快速对号入座硬件方案蓝牙协议开发难度典型用途备注ESP32开发板BLE也支持经典蓝牙中控制类、数据采集类支持Arduino/MicroPython开发带串口日志强烈推荐用来开发验证HM-10模块BLE低串口透传类价格便宜但调试相对麻烦固件版本杂nRF52832开发板BLE高专业产品原型功耗低可定制性强但上手门槛较高HC-05/HC-06模块经典蓝牙SPP低传统串口透传小程序不支持不建议用于本项目我自己做Demo验证时最常用ESP32因为可以一边在小程序里点按钮一边在串口监视器里看收到的原始字节这对定位协议问题帮助巨大。硬件端蓝牙服务的UUID一定要在代码里写清楚Demo里通常通过服务发现动态获取实话说比硬编码UUID更靠谱因为不同厂商的固件配置可能不一样。1.3 总体代码结构规划刚开始写这类项目最忌讳把所有逻辑都堆在Page里不然蓝牙回调、页面生命周期、用户交互搅在一起排查问题会非常痛苦。我习惯把蓝牙能力封装成一个独立的工具模块比如utils/ble.js所有wx原生蓝牙接口都走这个模块Page只负责调用和渲染这样写起来清爽后面做多页面复用也方便。模块内部再按职责划分成三层第一层是基础适配层负责打开/关闭蓝牙适配器、监听适配器状态变化、处理系统权限第二层是设备管理层负责扫描、连接、断开、订阅通知它要维护一个当前连接设备的状态对象避免重复连接和回调野指针问题第三层是数据收发层负责write和notify的封装以及分包处理。这个设计思路不复杂但它是整个项目后续能不能稳定联调的关键。2. 核心API链路与数据协议设计2.1 从打开适配器到收到数据一次完整的流程微信小程序的蓝牙API链路表面上看起来就是几个wx接口的调用但每一步都有它存在的意义。我按实际运行顺序给你梳理一遍。第一步是初始化蓝牙适配器调用wx.openBluetoothAdapter。这一步要做两件事检查手机蓝牙是否打开、初始化系统底层蓝牙资源。关键注意点是这个接口在部分基础库版本或部分Android机型上如果重复调用会返回错误所以初始化前最好先用wx.getBluetoothAdapterState查询一下当前状态确认是on再继续否则会浪费一次错误处理逻辑。第二步是开始扫描调用wx.startBluetoothDevicesDiscovery。这里有两个隐藏参数值得注意services过滤数组和allowDuplicatesKey。如果你在扫描前已经知道目标设备的服务UUID可以直接传services系统会只回调包含这些服务的设备扫描效率和准确性都大幅提升。allowDuplicatesKey为true时同一个设备会持续重复上报方便你在界面上动态更新信号强度RSSI但如果只是为了发现设备列表我建议设false避免同一个设备疯狂刷屏列表去重逻辑会变得复杂。第三步是监听设备发现用wx.onBluetoothDeviceFound注册回调。这里要留个心眼扫描回调返回的设备对象里deviceId在小程序里是唯一的你应该拿它作为设备的业务主键设备名称不一定存在有些硬件厂商为了省电不在广播包里广播名字需要从advertisData广播数据里解析出来甚至有些设备name干脆是空的这种情况列表上只能显示“未知设备”你要在UI上做好兜底。第四步是建立连接调用wx.createBLEConnection。连接是异步的成功后设备才会进入可通信状态。根据我的经验连接这个环节最容易踩的坑是忘记停止扫描。部分Android手机在扫描状态下直接建立BLE连接会失败或异常卡住稳妥的做法是等onBluetoothDeviceFound找到目标设备后先调用wx.stopBluetoothDevicesDiscovery停止扫描再调用createBLEConnection。停顿几百毫秒再连接更稳。第五步是发现服务和特征值依次调用wx.getBLEDeviceServices和wx.getBLEDeviceCharacteristics。很多新手以为连接成功后就能直接收发数据其实不对。BLE的数据交换是在特征值上进行的如果不先拿到服务列表和特征值列表你根本不知道这个设备有哪些数据通道可用。拿到特征值后要重点关注properties对象里的read、write、notify、indicate标志这决定了你能对这个特征值做什么操作。第六步是数据交互要么小程序主动写数据调用wx.writeBLECharacteristicValue向硬件发指令要么订阅硬件上报调用wx.notifyBLECharacteristicValueChange启用通知并监听wx.onBLECharacteristicValueChange回调。订阅通知这步是BLE最有价值的地方相当于硬件端可以随时随地主动推数据给小程序不用小程序轮询。整个链路走完一个小程序蓝牙Demo的核心闭环就成立了。后面的代码实现我会按这个顺序写你可以对照着看。2.2 MTU限制与数据分包策略BLE通信默认的MTU最大传输单元在Android和iOS上不完全一致但传统BLE 4.x默认包大小是23字节其中3字节被ATT层头占用所以应用层单次能传的数据只有20字节。这是无数新手撞得头破血流的地方——你往writeBLECharacteristicValue里塞一个30字节的字符串结果发现写入失败或者硬件只收到前20字节。解决分包问题有两种常见思路。第一种是不要挑战硬件直接约定应用层协议按20字节一包来传超出就拆分。拆分时不能简单把字符串按长度截了发出去因为接收方要能区分包的边界和顺序。我建议在应用层设计一个简单的帧格式比如帧头2字节如0xAA5A 数据长度1字节 包序号1字节 数据N字节 校验1字节可选用异或校验。每帧最多12字节数据这样加上头部和尾部刚好不超20字节。这种协议虽然带了一点冗余但非常可靠我实测在Demo联调中几乎没有出过因为粘包导致的数据错乱。第二种方案是在Android端尝试协商更大的MTU。微信小程序从基础库2.11.0开始提供了wx.setBLEMTU接口可以主动请求把MTU提到最大517字节。但需要注意iOS系统不支持这个接口而且协商结果受硬件端最大MTU限制所以不能把“大包发送”当成唯一方案。项目中我的做法是能协商就协商但应用层协议依然按20字节分帧这样两端都能通用。再补充一个硬件端的注意点如果数据接收方是单片机ESP32这类设备你的单片机代码最好也按同样的MTU逻辑处理接收缓存因为有些蓝牙协议栈在收到超过MTU的完整包时会直接丢弃或异常。协议一致性在这里体现得淋漓尽致软件端和硬件端必须对齐帧格式和分包规则。2.3 广播数据解析与设备识别扫描列表里经常出现一些名称显示不全的设备甚至有些设备明明在手机系统蓝牙设置里能看到名字到小程序里却变成空名称。原因在于设备的广播包结构。BLE设备在广播时会把设备名称塞在广播包里有的厂商把名称放在AD Type为0x08/0x09的字段里微信解析后会把localName返回给你但有些设备因为广播长度限制或者固件设计原因只放了自定义的manufacturer data或service data名称字段就是空的。遇到这种情况不能干等着iOS或Android给你补名称你要么在硬件端固件里补全广播名称要么在小程序端解析advertisData来识别设备。advertisData是一个ArrayBuffer你可以按BLE广播包的规范解析它广播包的每个AD Structure由长度1字节 AD Type1字节 数据组成遍历一遍就能拿到完整广播内容。不过说实话如果你能控制硬件固件直接在固件里加一个可读的名称字段是最省事的。在小程序里解析advertisData属于事后补救方案代码量不小而且不同芯片厂商的广播数据格式五花八门通用性有限。真实的项目里我建议扫描列表展示用localName 信号强度RSSI设备唯一识别用deviceId。如果设备名称为空可以展示成“BLE设备MAC后四位”这样用户至少能分辨出哪台是目标设备。3. 实操Demo结构与核心代码实现3.1 工程目录与开发环境准备项目工程结构其实不用很复杂一个单页Demo就够用了。我习惯的目录结构是这样的miniprogram/ pages/ index/ index.js index.wxml index.wxss utils/ ble.js app.js app.jsonapp.json里需要声明一下小程序对蓝牙能力的权限说明尤其是iOS会弹窗询问用户是否允许使用蓝牙这个说明文字会展示在弹窗里。配置大致如下{ permission: { scope.bluetooth: { desc: 需要使用蓝牙连接附近设备 } } }注意这个配置在部分微信版本或Android平台上可能不是强制的但在iOS上如果没有配置说明系统弹窗会显示默认文案甚至直接自动拒绝。另外基础库版本建议不低于2.11.0因为需要用到wx.setBLEMTU这样的新接口尽量在开发者工具里把调试基础库调高一点。还有一个非常容易忽略的点微信开发者工具的模拟器是不支持蓝牙调用的你必须用手机真机预览或真机调试而且手机的系统蓝牙开关必须在打开状态。第一次跑Demo先用微信开发者工具的“真机调试”模式再把vConsole打开这样手机上的运行日志可以直接在开发者工具里看到调起蓝牙时系统层的错误信息也能暴露得更充分。3.2 蓝牙工具模块的完整封装我先把utils/ble.js的核心代码贴出来这段代码我经过多轮真机验证是能直接用的。它把扫描、连接、服务发现、数据收发都封装成了Promise风格方便Page里按调用链组织逻辑。// utils/ble.js const ble { _deviceId: , _serviceId: , _characterId: , _notifyCallback: null, // 初始化蓝牙适配器 openAdapter() { return new Promise((resolve, reject) { wx.openBluetoothAdapter({ success: resolve, fail: (err) { // 如果已经开启尝试直接获取状态 wx.getBluetoothAdapterState({ success: (res) { if (res.available) { resolve(res); } else { reject(err); } }, fail: () reject(err) }); } }); }); }, // 开始扫描 startScan(services) { return new Promise((resolve, reject) { wx.startBluetoothDevicesDiscovery({ services: services || [], allowDuplicatesKey: true, success: resolve, fail: reject }); }); }, // 停止扫描 stopScan() { return new Promise((resolve) { wx.stopBluetoothDevicesDiscovery({ success: resolve, fail: resolve }); }); }, // 监听设备发现由页面层调用并处理回调 onDeviceFound(callback) { wx.onBluetoothDeviceFound(callback); }, // 连接设备 connect(deviceId) { return new Promise((resolve, reject) { wx.createBLEConnection({ deviceId, success: () { this._deviceId deviceId; resolve(); }, fail: reject }); }); }, // 断开连接 disconnect() { if (this._deviceId) { wx.closeBLEConnection({ deviceId: this._deviceId }); } }, // 获取主服务和特征值 discoverServices() { return new Promise((resolve, reject) { wx.getBLEDeviceServices({ deviceId: this._deviceId, success: (res) { const services res.services || []; if (!services.length) { reject(new Error(未发现任何服务)); return; } // 这里简化处理遍历每个服务查找可用的特征值 const promises services.map((service) { return new Promise((resolveInner, rejectInner) { wx.getBLEDeviceCharacteristics({ deviceId: this._deviceId, serviceId: service.uuid, success: (chrRes) { resolveInner({ serviceId: service.uuid, characteristics: chrRes.characteristics || [] }); }, fail: rejectInner }); }); }); Promise.all(promises).then(resolve).catch(reject); }, fail: reject }); }); }, // 写入数据自动分包 writeData(dataBuffer) { return new Promise((resolve, reject) { if (!this._serviceId || !this._characterId) { reject(new Error(服务或特征值未设置)); return; } // 分包发送单次最多20字节 const chunkSize 20; const totalChunks Math.ceil(dataBuffer.byteLength / chunkSize); let sentChunks 0; const sendChunk (offset) { const chunk dataBuffer.slice(offset, offset chunkSize); wx.writeBLECharacteristicValue({ deviceId: this._deviceId, serviceId: this._serviceId, characteristicId: this._characterId, value: chunk, success: () { sentChunks; if (sentChunks totalChunks) { const nextOffset offset chunkSize; // 每包间隔50ms避免BLE协议栈压力过大 setTimeout(() sendChunk(nextOffset), 50); } else { resolve(); } }, fail: reject }); }; sendChunk(0); }); }, // 启用通知 startNotify(serviceId, characteristicId, callback) { this._serviceId serviceId; this._characterId characteristicId; this._notifyCallback callback; wx.onBLECharacteristicValueChange((res) { if (this._notifyCallback) { this._notifyCallback(res.value); } }); return new Promise((resolve, reject) { wx.notifyBLECharacteristicValueChange({ deviceId: this._deviceId, serviceId, characteristicId, state: true, success: resolve, fail: reject }); }); }, offNotify() { if (this._notifyCallback) { this._notifyCallback null; } } }; module.exports ble;这里有几个细节我说一下。writeData里我用了一个递归调用setTimeout的方式分包发送每包间隔50毫秒这个间隔是我在实际项目中测出来的。如果你把间隔压到10毫秒或20毫秒部分Android机型或某些BLE芯片会丢包隔得太慢则用户体验差50毫秒属于临界值附近比较稳的。当然如果你的数据量很小比如控制指令不超过20字节根本不需要走分包逻辑直接一次写入就行。另外startNotify里我先把回调存到模块变量再绑定wx.onBLECharacteristicValueChange。这个设计是为了防止页面多个地方同时注册监听导致回调重复执行。实际项目里你还可以在onBLECharacteristicValueChange的回调里做一些简单的数据解析比如把ArrayBuffer转成文本或十六进制再交给页面渲染。3.3 Page层调用与设备列表渲染工具模块封装好之后Page层的代码就清爽多了。核心逻辑是页面加载时初始化蓝牙适配器用户点击扫描后开始扫描并监听设备回调设备列表去重展示用户点击某一项后停止扫描并连接连接成功后执行服务发现选定读写特征值后进入数据收发界面。下面我给出一个精简但完整的Page代码关键注释我写到代码里// pages/index/index.js const ble require(../../utils/ble.js); Page({ data: { devices: [], connected: false, log: [], serviceList: [], characteristicList: [], sendText: }, onLoad() { this.initBluetooth(); }, async initBluetooth() { try { await ble.openAdapter(); this.addLog(蓝牙适配器已初始化); } catch (err) { this.addLog(蓝牙初始化失败 JSON.stringify(err)); } }, startScan() { this.setData({ devices: [] }); ble.onDeviceFound((res) { const newDevices res.devices || []; const current this.data.devices; newDevices.forEach((device) { const exists current.some((d) d.deviceId device.deviceId); if (!exists) { current.push(device); } }); this.setData({ devices: current }); }); ble.startScan([]).then(() { this.addLog(开始扫描); }).catch((err) { this.addLog(扫描启动失败 JSON.stringify(err)); }); }, stopScan() { ble.stopScan().then(() { this.addLog(停止扫描); }); }, async onTapDevice(e) { const deviceId e.currentTarget.dataset.deviceId; this.addLog(准备连接 deviceId); await ble.stopScan(); try { await ble.connect(deviceId); this.setData({ connected: true }); this.addLog(连接成功); const services await ble.discoverServices(); // 这里简化默认选择第一个可写/可通知的特征值 services.forEach((service) { service.characteristics.forEach((chr) { if (chr.properties.write || chr.properties.notify) { this.setData({ serviceList: [{ serviceId: service.serviceId, characteristic: chr }] }); } }); }); this.addLog(服务发现完成); } catch (err) { this.addLog(连接或发现服务失败 JSON.stringify(err)); } }, async sendData() { if (!this.data.connected) return; const text this.data.sendText; const buffer new ArrayBuffer(text.length); const dataView new DataView(buffer); for (let i 0; i text.length; i) { dataView.setUint8(i, text.charCodeAt(i)); } try { await ble.writeData(buffer); this.addLog(发送成功 text); } catch (err) { this.addLog(发送失败 JSON.stringify(err)); } }, addLog(msg) { const log this.data.log; log.push(new Date().toLocaleTimeString() msg); this.setData({ log }); }, onUnload() { ble.disconnect(); ble.offNotify(); } });这里我故意省略了wxml的细节因为不同的UI风格差异很大。但有几个交互细节值得提醒你扫描列表最好显示RSSI信号强度方便判断设备距离连接过程中要禁用按钮防止用户重复点击日志区用scroll-view滚动到底部避免调试时看不到最新日志。另外页面onUnload生命周期里一定要做清理工作断开连接、关闭notify监听。如果不做这一步用户在页面间跳转后底层蓝牙回调还在触发轻则内存泄漏重则出现“当前页面已卸载但回调还在执行”的诡异BUG。3.4 使用真机调试与其他蓝牙工具配合微信开发者工具里的模拟器没有蓝牙硬件所以调试这类项目第一步就是把代码跑到真机上。真机调试模式下Console面板会输出所有console日志vConsole也能在手机页面上直接展示日志这让排查问题方便很多。我一般会在蓝牙相关的所有回调里都打console.log把入参、出参、错误信息全部记录下来这比事后猜要高效得多。除了微信开发者工具我强烈建议手机里装一个nRF Connect或者BLE调试助手。它有两个大用处第一开发前用它对硬件设备做一次服务特征值扫描确认硬件端到底暴露了哪些服务、哪些特征值这样你在小程序里做筛选时就心里有数了第二当微信小程序连不上时先用nRF Connect试连一下硬件如果nRF Connect能连上说明硬件没问题问题大概率在小程序代码或者权限处理上这一点可以快速缩小排查范围。注意nRF Connect这类工具本质是协议分析调试器不属于任何违规操作它是蓝牙开发的标准工具。用它来查看设备广播包、服务UUID、特征值属性是安全合规的。4. 常见问题与排查技巧实录4.1 扫描不到设备先按清单逐项排查扫描不到设备是我被问得最多的问题而且一半以上其实不是代码问题。我把排查顺序整理成一张表你可以照着逐项过一遍。排查项检查方法解决方式手机蓝牙总开关进入系统设置确认蓝牙已打开打开蓝牙定位权限Android 6.0以上BLE扫描依赖定位权限在弹出的权限窗口允许位置权限基础库版本微信版本过旧或基础库过低升级微信开发者工具设置基础库2.11.0扫描过滤参数services参数填了不存在的UUID去掉services过滤重新扫描设备广播间隔部分硬件广播间隔太长扫描窗口错过增加扫描时间或硬件端调小广播间隔设备距离过远BLE有效距离一般10-30米靠近设备再扫描Android系统蓝牙缓存系统缓存旧设备信息导致扫描异常关闭蓝牙再打开清理系统蓝牙缓存设备已被连接手机系统蓝牙或另一台手机已占用设备断开设备后重新扫描广播数据不完整硬件只广播部分数据名称为空解析advertisData或硬件端补全广播名称其中Android系统蓝牙缓存问题非常隐蔽。设备改过固件、改过广播名称后手机里可能还留着旧的蓝牙配置导致小程序扫描到的是一个“旧身份”的设备连接后行为异常。这种情况参考经验做法是手机设置里找到蓝牙取消配对该设备然后重启手机蓝牙再重新扫描。iOS相对好一些但也有类似情况重启蓝牙能解决七成问题。4.2 连接失败、反复断连的典型原因连接失败或连上后过几秒自动断开这类问题比扫描不到更让人头大因为涉及的系统层和硬件层因素更多。第一个常见原因是连接前没有停止扫描。我前面说过部分Android机型在扫描状态下直接调用createBLEConnection会失败或异常解决方式就是先stopScan再connect最好加一个小延时让系统蓝牙协议栈缓一口气。第二个常见原因是iOS的deviceId变化。iOS系统会对BLE MAC地址做隐私处理同一个设备在不同时间扫描到的deviceId可能不一样。所以你不能在本地持久化保存deviceId跨会话直接重连时会失败。正确做法是每次都走完整的扫描、发现、连接流程或者参考外设的某些固定标识比如广播数据里的mac字段来二次匹配。第三个常见原因是硬件端连接参数配置问题。BLE的连接间隔、从机延迟、超时时间这些参数由硬件固件决定如果连接间隔过密比如7.5msiOS和部分Android系统上容易断连如果硬件端功耗设计不合理也可能在传输数据时供电不稳导致断开。这一点只能改硬件固件配置软件端能做的只是监听onBLEConnectionStateChange回调把断开原因打出来。第四个常见原因是重复连接。有些开发者在未断开旧连接的情况下尝试重新连接同一台设备这在小程序API上不会报错但系统底层可能表现异常。建议在connect之前先对同一设备做一次closeBLEConnection再做连接连接成功后把已连接状态存到全局变量或缓存里。4.3 数据收发异常可能卡在编码或协议上数据能连上但收发不对这类问题往往不是蓝牙链路断而是数据格式或协议不对。我遇到最多的是下面几种情况。写入不成功先确认特征值属性。有些特征值是只读的根本没有write权限你调用writeBLECharacteristicValue自然会失败。调试时把getBLEDeviceCharacteristics返回的properties对象打印出来确认write字段为true再执行写入。另外一次写入长度超过20字节也会失败先检查分包逻辑。notify收不到数据先看硬件端有没有真的发数据。有些硬件模块在连接成功后需要先发一个“打开通知”的指令然后才开始主动上报有些硬件则需要延时1到2秒之后再启用notify否则它还没准备好上报通道。这类问题只能靠硬件日志和手机端日志两边对照来定位。另外wx.notifyBLECharacteristicValueChange的state参数一定要传true而且每个特征值都要单独开启一次。收到乱码或莫名数据大概率是编码问题。BLE的数据是ArrayBuffer如果你强行把它转成字符串但硬件端发送的是UTF-8编码的中文而你在小程序里用了Latin-1或其他编码解析就会出现乱码。小程序端ArrayBuffer转字符串我推荐先用TextDecoder迭代解码或者用官方提供的ab2hex、ab2str等工具函数。如果只是调试用直接转十六进制看数据更直观。4.4 Android和iOS的兼容性差异Android和iOS在BLE实现上有不少行为差异同一个Demo在这两个平台上表现完全不同是常态。我总结几个核心差异供你在设计时提前规避。Android的deviceId就是蓝牙MAC地址相对稳定但部分国产ROM会做随机化iOS的deviceId是系统生成的UUID每次扫描结果都可能变化。这意味着扫描列表去重逻辑要基于deviceId但重连逻辑不能依赖它。iOS对后台蓝牙限制严格小程序一旦切到后台蓝牙回调可能被挂起数据交互基本停止所以做长时间数据采集类应用时要提醒用户保持小程序在前台运行。Android后台限制相对宽松但不同厂商ROM杀后台策略也不一样最稳妥的做法还是保持前台或使用“回到前台自动重连”机制。还有一个MTU的差异Android一般能协商更大MTUiOS的MTU在GATT协商后通常也是23字节的倍数但具体数值不确定。所以结论就是我前面反复强调的协议层按20字节分包是唯一能保证两端通用的做法。不要指望iOS能像Android一样发大包。4.5 日志调试与问题复现技巧踩过这么多坑之后我的调试习惯已经固化成一套流程。第一所有蓝牙相关回调必须打日志尤其是fail回调哪怕是一个成功回调也要打出来因为很多“成功”之后的异常行为是需要对比时间线才能发现的。第二尽量把代码里的操作步骤和硬件行为对照起来比如小程序里点了“连接”你同时看硬件端的串口日志是不是有连接事件中间卡在哪一步一目了然。第三用好微信的“真机调试”和vConsole把手机端的报错信息带回电脑端分析。有些Android机型的错误码会提示很具体的系统层原因比如“connection fail”或“gatt error”这些信息在模拟器里根本看不到。第四问题复现时保持环境一致性比如固定同一台手机、同一个硬件、同一路线否则你很难判断是代码问题还是网络环境问题。我不建议用传统意义上的“抓包”手段去调试这个Demo一是微信小程序运行在自己的沙箱里数据加密解密链路复杂二是这些手段涉及对第三方应用的逆向分析合规风险很高完全没必要。用系统级的蓝牙日志、nRF Connect跟Log plus对照已经足够解决绝大多数问题。5. 影响范围与后续应用扩展5.1 从Demo到真实项目典型的应用场景别看这个Demo标题简单它的框架可以直接迁移到很多实际项目中。我列几个我接触过的真实场景你会发现它们的底层逻辑都是一样的。第一个场景是智能硬件控制。ESP32或者Arduino配一个BLE模块小程序当遥控器控制灯光颜色、窗帘开合、风扇档位。这个场景的核心是“小程序到硬件”的下行链路数据量小实时性要求高正好是BLE的强项。我见过一个项目用这个思路做了一个会议室的智能灯光控制面板运维人员不用下载一个专用App扫码打开小程序就能调光部署成本非常低。第二个场景是设备数据采集。例如一个蓝牙温湿度记录仪硬件定时把温度和湿度塞到广播包里或者通过notify推给小程序小程序端展示实时曲线并上传到后台数据库。这个场景的核心是“硬件到小程序”的上行链路需要处理好粘包、分包和后台数据同步。用这个框架前端部分基本不用大改。第三个场景是工程巡检类的信息采集。标题热词里出现了“基于微信小程序的建筑工程质量缺陷图纸定位与智能信息采集系统”这类项目本质上就是“扫设备蓝牙 数据上报 服务端记录”的组合。硬件端可能是一台带BLE信标的巡检设备或RFID阅读器小程序靠近后扫描到设备自动带入当前工位信息再结合图纸定位和拍照填写缺陷内容。这套流程里蓝牙Demo负责设备识别和连接这块业务逻辑再往下接就平铺直叙了。第四个场景是低精度的蓝牙测距。利用RSSI信号强度可以粗略估算设备距离做成防丢提醒、门禁靠近识别等功能。虽然精度不如UWB但在Demo层面完全够用还能帮助新手理解信号强度衰减模型。5.2 从小程序端看蓝牙Demo的未来扩展方向这个Demo虽然基础但它是一个很好的起点。往上扩展你可以加入多个设备的并发管理比如一个页面同时连接两台BLE设备分别控制不同功能这需要你把工具模块里的_deviceId扩展成设备Map结构。再往上扩展你可以接入蓝牙Mesh类设备或者更多低功耗传感器不过这类扩展更依赖硬件端固件能力小程序端的API并没有质的变化。另一个值得关注的方向是数据安全。BLE通信本身默认不带加密广播数据可以被附近设备捕获明文传输的数据在商业场景里有安全隐患。如果你做的是生产环境项目最好在应用层增加加密逻辑比如对关键指令做AES或异或混淆再把密钥管理的逻辑放到小程序的服务端下发。这块虽然Demo里不涉及但从一开始就留好扩展点会让后续演进省力很多。还有一点是从工程化角度的建议这个Demo里的工具模块应该独立维护沉淀成团队内部的基础库因为它不只服务于当前这个项目。微信小程序的蓝牙API在可预见的范围内不会有太大变化这样一个成熟稳定的模块复用到后续项目里几乎零成本。5.3 我个人的一些工程经验总结最后结合这个Demo说一下我在实际项目中形成的几个习惯谈不上高深但确实能少走弯路。一是做蓝牙项目前花20分钟用nRF Connect把硬件端的服务模型列清楚确认每个特征值的读写权限和数据含义这一步比多写1000行代码更有价值。二是代码里所有UUID尽量用常量集中管理不要让字符串散落在各个文件里不然等你要适配不同固件版本的时候改起来会让你想骂人。三是页面生命周期一定做好蓝牙资源的清理和重连策略用户切换页面后能不能恢复连接这部分体验往往决定了一个工具类小程序能不能被长期使用。我还养成了一个习惯就是把开发期间碰到的问题和解决方案维护成一个Markdown文档类似一个团队内部的知识库。这个Demo相关的坑比如Android的缓存问题、MTU分包、notify时序我全部沉淀下来了。后续团队成员遇到类似问题翻一下文档就能定位根本不用重新踩一遍。做小程序蓝牙开发本质上是一个软件和硬件反复对齐的过程耐心比聪明重要日志比猜测可靠。这篇文章里写的每一个问题都是我真机验证过的如果你正好被某个问题卡住按照里面的排查步骤走一遍大概率能找到答案。本文还有配套的精品资源点击获取
返回列表