恒美微站 Logo 恒美微站
  • 首页
  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心
  • 联系我们

微信小程序蓝牙连接与读写数据:BLE透传链路及避坑实践

  • 首页
  • 资讯中心
  • /
  • 微信小程序蓝牙连接与读写数据:BLE透传链路及避坑实践

相关资讯

双麦克风语音分离源码实战:从原理到嵌入式复现的避坑指南 2026/10/15 15:16:19
ThreadPoolExecutor核心解析:状态机、Worker复用与拒绝策略 2026/10/15 15:16:19
微信小程序BLE蓝牙连接读写实战:从扫描到数据交互全解析 2026/10/15 15:16:19

最新资讯

Google发布A2A开源协议:“MCP+A2A”成未来标配?TaoToken统一Key视角下的Agent协作落地
SAP GUI 770 升级指南:TLS 1.2、SNC 加密与证书链配置实战
MyBatis代码生成插件:元数据驱动的IDE级开发实践
用C++手写增量Delaunay三角剖分:从原理到工程实现
OpenClaw 2.6.6 绑定 QQ 功能配置说明(含最新版安装包)
Python知识整理指南:从语法到工程化,构建可复用知识体系

今日推荐

AI视频生成的时间一致性难题与时空建模演进
基于bbox的PDF文档解析:多栏重排与水印过滤实战
探矿数据进RAG前的清洗全攻略:TXT、Word、PDF、网页四类文档处理实践

本周热门

UE动画修改实战:从资产编辑到重定向与蒙太奇驱动
统计随机数生成器攻击下的KLJN安全密钥交换协议Matlab仿真
政务API安全治理:资产测绘、低代码编排与行标对标实践

本月精选

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证
2026 大模型集体涨价:用 Python 做企业 Token 成本测算与选型避坑(附配置)

微信小程序蓝牙连接与读写数据:BLE透传链路及避坑实践

发布时间:2026/10/15 15:16:19
微信小程序蓝牙连接与读写数据:BLE透传链路及避坑实践 做微信小程序蓝牙连接和读写数据最折磨人的不是功能本身而是所有API都按文档调成功了设备却毫无反应。搜索、连接、服务发现、特征值读写这套流程我前后折腾了不短时间才彻底跑通中间踩过不少文档里不会写明的坑。这篇内容不聊空泛的理论直接把我在一个模拟项目里整理出的完整链路、可用代码和真机排障思路分享出来。如果你正准备接手小程序蓝牙功能或者已经被各种errCode折腾到怀疑人生这篇文章应该能帮你少走很多弯路。1. 先画一遍链路BLE在小程序里到底怎么工作1.1 BLE不是普通蓝牙很多人第一次做小程序蓝牙第一反应是拿它当普通蓝牙设备来理解。实际上小程序走的是BLE那套协议也就是低功耗蓝牙。两者的使用场景完全不同经典蓝牙适合音频传输这种大数据量场景BLE则是为小数据量、低功耗、低频次通信设计的。我们身边的温湿度计、智能开关、心率手环、串口透传模块基本都是BLE设备。这里有个理解模型很关键BLE通信里设备端叫外设手机端叫中心设备。外设通过广播包不断喊自己的存在中心设备听到广播后发起连接。蓝牙模块不会主动往手机里推数据连接建立后手机和模块之间是一种类似一问一答加订阅通知的协作模式。正因如此小程序里才会有一长串API要按顺序调用少任何一步后面的读写都不会有结果。1.2 小程序蓝牙调用顺序一步都不能跳小程序蓝牙API的调用顺序大概就是下面这张流程wx.openBluetoothAdapter() 打开蓝牙适配器wx.startBluetoothDevicesDiscovery() 开始扫描外设wx.onBluetoothDeviceFound() 获取扫描结果wx.createBLEConnection() 与外设建立连接wx.getBLEDeviceServices() 获取外设的服务列表wx.getBLEDeviceCharacteristics() 获取指定服务下的特征值列表通过 writeBLECharacteristicValue / readBLECharacteristicValue / notifyBLECharacteristicValueChange 进行读写整个链路看起来不复杂难就难在每一步都是异步回调而且彼此有依赖关系。你必须在创建连接成功之后才能去拿服务列表拿到服务列表之后才能拿特征值拿到特征值之后才能判断这个通道能不能写、能不能收数据。我之前在项目里用Promise把它们串起来核心是避免后面步骤访问到空值function openBluetooth() { return new Promise((resolve, reject) { wx.openBluetoothAdapter({ success: resolve, fail: (err) reject(err) }) }) } function discoverDevices() { return new Promise((resolve, reject) { wx.startBluetoothDevicesDiscovery({ services: [], allowDuplicatesKey: false, success: resolve, fail: reject }) }) }这里有个容易踩的坑有些人会把 getBLEDeviceServices 放在 createBLEConnection 的 success 外面直接调用因为 createBLEConnection 的 success 只是在系统层面表示连接请求被接受了不代表设备已经准备好可以通信。最稳的做法是连接成功后先稍等一下比如300ms再取服务列表真机上这样处理成功率会高很多。1.3 适配器初始化之后的隐藏状态openBluetoothAdapter 是最容易被轻视的一步。它成功之后系统的蓝牙状态不一定就稳定。比如用户后来在控制中心把蓝牙关了此时再调用其他API返回的错误码直接就是10001或者10011很容易让人误以为是设备的问题。小程序提供了 wx.onBluetoothAdapterStateChange 来监听适配器状态变化wx.onBluetoothAdapterStateChange(res { console.log(适配器状态变化, res.available, res.discovering) if (!res.available) { // 蓝牙被关闭清理连接状态 } })我在设计里会把 adapterAvailable 作为一个全局标志位。只要 available 变成 false立刻停止当前所有扫描和连接操作并清掉页面的连接状态。否则等蓝牙重新打开后界面还停留在已连接的假象里后续读写全都失败用户体验很糟。另外注意尽量不要在 onLoad 里立刻 openBluetoothAdapter。很多系统会弹权限框用户可能还没准备好。更合理的方式是放在连接设备按钮的点击事件里用户主动触发弹窗时机自然成功率也高。2. 搜索设备为什么搜不到、或者搜到连不上2.1 广播是设备的自我介绍搜索阶段微信给我们返回的核心信息是 device 对象里面有几个字段值得关注name、deviceId、RSSI、advertisData、advertisServiceUUIDs。很多情况下我们的目标是识别出哪个设备是我的模块。最简单粗暴的方式就是用 name 去匹配但现实是很多BLE模块为了省电根本不广播名字name 字段是空的或者广播名是设备型号这类不便于用户理解的字符串。这时候就要看 advertisData。它是设备广播包里带的一段原始数据不同设备的含义千差万别。有的设备会把设备类型、电量、自定义标识存在里面。我处理的方式是先把 advertisData 转成16进制字符串打印出来跟设备厂商给的协议文档比对确认唯一标识后再做匹配过滤。这一步能在项目早期省掉大量的无效连接。转成十六进制字符串的代码很简单function bufferToHex(buffer) { const bytes new Uint8Array(buffer) return Array.from(bytes).map(b b.toString(16).padStart(2, 0)).join().toUpperCase() }2.2 安卓要开定位、iOS要授权搜不到设备十有八九不是代码问题而是权限问题。在 Android 上6.0 之后的系统要求扫描蓝牙必须申请定位权限。更麻烦的是部分国产机型就算开了定位权限定位服务本身没开依然搜不到。我用的某台测试机就是这样明明权限全开把位置信息一打开立刻就能扫到设备。所以排障的时候定位服务和定位权限要一起确认。iOS 侧则比较简单直接第一次调用蓝牙相关API会弹授权框。如果用户点了拒绝之后 openBluetoothAdapter 会一直失败。遇到这种情况我们在界面上给用户一个引导让用户去系统设置里打开蓝牙权限。没权限之前硬调API没有意义。2.3 搜索参数的细节与去重startBluetoothDevicesDiscovery 有一个 services 参数很多人喜欢把目标设备的服务UUID填进去想精确搜索。这个思路本身没问题但实际使用中有个反直觉的坑有些模块广播的UUID和连接后 getBLEDeviceServices 拿到的UUID并不完全一致填写了这个参数反而什么都搜不到。我后来统一改成 services: []也就是搜所有BLE设备再用 name 或 advertisData 二次过滤。另一个问题是重复上报。onBluetoothDeviceFound 回调会把同一个设备多次吐出来尤其当 allowDuplicatesKey 设为 true 时。所以需要自己做去重我常用的写法let deviceMap new Map() wx.onBluetoothDeviceFound(res { res.devices.forEach(device { if (!device.deviceId) return // 用RSSI绝对值做简单信号判断合适的再入列表 deviceMap.set(device.deviceId, device) let devices Array.from(deviceMap.values()) devices.sort((a, b) b.RSSI - a.RSSI) // 更新界面列表 }) })注意 RSSI 是负值比如 -50 比 -80 信号强。如果列表里很多同型号设备按 RSSI 排序选离自己最近的能在一定程度上减少连接失败率。3. 连接设备并拿到读写句柄从deviceId到特征值3.1 createBLEConnection不是终点点选设备列表里的某个设备接下来就是 createBLEConnection。这里有个几乎人人都会踩的坑iOS 上如果扫描没有停止直接发起连接经常报 10004 连接失败。Android 部分机型不停止扫描也能连上但为了一致性和稳定性连接前统一先 stopBluetoothDevicesDiscovery再延时几十毫秒发起连接。我封装后的连接函数大致长这样function connectDevice(deviceId) { return new Promise((resolve, reject) { wx.stopBluetoothDevicesDiscovery({ complete: () { wx.createBLEConnection({ deviceId, timeout: 10000, success: resolve, fail: reject }) } }) }) }连接成功之后系统层面通道是建立了但你还不知道这个设备有哪些服务、哪些特征值。所以紧接着要 getBLEDeviceServices。这里我也加了延时实测比较稳async function connectAndInit(deviceId) { await connectDevice(deviceId) await sleep(300) const services await getServices(deviceId) return services }这里多解释一句为什么要 sleep。createBLEConnection 的 success 只代表 BLE 链路层握手完成但设备的协议栈可能还在准备GATT表。马上读服务列表偶尔会拿到不完整的结果。加300ms缓冲能规避掉很大一部分偶现的服务列表为空问题。3.2 找到正确的服务UUID和特征UUIDgetBLEDeviceServices 返回的 services 数组里既有标准服务比如 Generic Access 对应的 1800、Generic Attribute 对应的 1801也有厂商自定义的服务。我们要找的透传通道通常在厂商自定义服务里。厂商协议的UUID一般有两种写法完整128位比如 49535343-xxxx-xxxx-xxxx-xxxxxxxxxxxx或者简写为 0xFFE0 这类16位短UUID。小程序返回的UUID基本都是完整的128位短UUID会被自动补成 0000FFE0-0000-1000-8000-00805F9B34FB 这种格式。所以匹配的时候不能直接做相等比较。我习惯把 serviceId 和 characteristicId 转成大写然后用 includes(FFE0) 这种方式去找。以经典的串口透传模块为例通常是 FFE0 服务下的 FFE1 特征支持 write 和 notifyasync function findTargetCharacteristic(deviceId) { const services await getBLEDeviceServices(deviceId) for (const svc of services) { if (!svc.uuid.toUpperCase().includes(FFE0)) continue const chars await getBLEDeviceCharacteristics(deviceId, svc.uuid) for (const ch of chars) { if (ch.uuid.toUpperCase().includes(FFE1)) { return { serviceId: svc.uuid, characteristicId: ch.uuid } } } } return null }这段逻辑放在一个异步函数里服务遍历和特征遍历都用 for 循环一旦找到目标通道就 return避免在非目标服务上做无谓的等待。3.3 先看properties再决定用哪种方式通信拿到特征值之后有个信息非常关键就是 properties 字段。它是个对象包含 read、write、writeNoResponse、notify、indicate 这些布尔值。不同设备的属性组合差别很大支持 write 的特征可以用 writeBLECharacteristicValue 写入支持 notify 或 indicate 的特征可以订阅通知支持 read 的特征可以主动读取我见过只支持 read 不支持 write 的传感器特征也见过只支持 writeNoResponse 的透传通道。如果你不看 properties闷头用 writeBLECharacteristicValue在部分机型上会一直报该特征值不支持此操作。正确的做法是拿到特征后先检查 properties再决定是否注册进管理器。判断逻辑很简单function canWrite(ch) { return ch.properties.write || ch.properties.writeNoResponse } function canNotify(ch) { return ch.properties.notify || ch.properties.indicate } function canRead(ch) { return ch.properties.read }4. 真正动手读写发送、接收、分包与解析4.1 写数据ArrayBuffer转换与写入时序BLE 通信的数据载体是二进制小程序里 wx.writeBLECharacteristicValue 的 value 参数要求是 ArrayBuffer。如果你直接传字符串编译都过不了。所以写数据函数要先做一次格式转换function stringToBuffer(str) { const buffer new ArrayBuffer(str.length) const view new Uint8Array(buffer) for (let i 0; i str.length; i) { view[i] str.charCodeAt(i) 0xff } return buffer } function writeData(deviceId, serviceId, characteristicId, data) { return new Promise((resolve, reject) { wx.writeBLECharacteristicValue({ deviceId, serviceId, characteristicId, value: data, success: resolve, fail: (err) reject(err) }) }) }这个 stringToBuffer 只适合ASCII字符。如果要发中文字符串的每个字符占多个字节直接 0xff 会丢失高位导致乱码。生产环境我一般会先对字符串做 encodeURIComponent或者直接让协议层使用二进制/十六进制字符串传输避开文本编码的坑。还有一个容易忽略的细节写入要有节奏感。对大部分BLE透传模块来说你连续快速写设备端的接收缓冲很容易溢出。所以要么控制发送间隔要么等设备返回ACK后再发下一包。后一种最稳但需要设备协议支持不支持ACK时固定间隔50ms是比较常见的折中方案。4.2 收数据notify为主、read为辅接收数据有两种方式notify 订阅通知和 read 主动读取。绝大多数设备采用 notify 模式。也就是设备有数据变化时主动推给你手机上只需要订阅一次之后靠 wx.onBLECharacteristicValueChange 回调接收。开启 notify 的代码function enableNotify(deviceId, serviceId, characteristicId) { return new Promise((resolve, reject) { wx.notifyBLECharacteristicValueChange({ deviceId, serviceId, characteristicId, state: true, success: resolve, fail: reject }) }) }很多人会忽略的一个细节是wx.readBLECharacteristicValue 读取到的设备返回数据走的也是同一个 onBLECharacteristicValueChange 回调而不是 read 的 success。也就是说如果你在回调里做了设备上报数据的业务解析主动读取返回的数据也会混进来需要根据 characteristicId 或者你自己在设备协议里分辨。有些设备只支持 read 不支持 notify这种情况下流程就是先注册 onBLECharacteristicValueChange再调 readBLECharacteristicValue收到数据后按需再次读取。4.3 20字节分包为什么必须做怎么做BLE 在协议层有个经典限制传统单包最多传20字节。虽然新版手机和蓝牙模块可能通过协商MTU让单包变大但这个能力依赖系统组件和硬件小程序并没有统一暴露设置MTU的接口。为了兼容性通用的做法还是老老实实按20字节分包。分包发送的核心思路就是把一条长数据切成多个小于等于20字节的小块依次发送。切分时要注意别把ArrayBuffer的视图搞乱我常用的实现function sendWithChunk(deviceId, serviceId, characteristicId, buffer, chunkSize 20) { return new Promise((resolve, reject) { const totalChunks Math.ceil(buffer.byteLength / chunkSize) let index 0 const sendNext () { if (index totalChunks) { resolve() return } const start index * chunkSize const end Math.min(start chunkSize, buffer.byteLength) const chunk buffer.slice(start, end) // slice返回新的ArrayBuffer wx.writeBLECharacteristicValue({ deviceId, serviceId, characteristicId, value: chunk, success: () { index setTimeout(sendNext, 50) }, fail: reject }) } sendNext() }) }这里用 buffer.slice 而不是拿到视图再截断就是为了避免共享底层的 ArrayBuffer 导致数据错乱。每一包间隔设置50ms实测大多数设备都能稳定接收。如果设备端要求更严格的流控可以改成 ACK 驱动也就是收到设备确认后再发下一包源码结构差不多只是把 setTimeout 换成在接收回调里触发 sendNext。4.4 数据粘包与协议解析的简化方案notify 回调的粒度不代表设备协议帧的粒度。设备可能一次发一帧也可能把两帧合在一次回调里发出来。如果直接按收到的每段数据做解析协议稍复杂一点就会出现莫名其妙的丢数据或错位。我的做法是维护一个接收缓冲区把每次回调的数据追加进去然后按协议格式一帧一帧地扣出来。假设设备协议是包头加长度加数据加校验的结构可以这样处理let receiveBuffer [] wx.onBLECharacteristicValueChange(res { const bytes new Uint8Array(res.value) for (let i 0; i bytes.length; i) receiveBuffer.push(bytes[i]) // 不断尝试从缓冲区里解析完整帧 while (true) { if (receiveBuffer.length 2) return // 示例固定帧头 0xAA if (receiveBuffer[0] ! 0xAA) { receiveBuffer.shift() continue } const frameLen receiveBuffer[1] if (receiveBuffer.length 2 frameLen) return const frame receiveBuffer.slice(0, 2 frameLen) receiveBuffer receiveBuffer.slice(2 frameLen) handleFrame(new Uint8Array(frame)) // 业务处理 } })这只是个模板里面的帧头和长度字段的具体规则一定以你自己的设备协议为准。但思路是通用的先找帧头、再按长度取帧、解析完后把已消费的字节移出缓冲区。这样做之后粘包、半包问题基本都能解决。5. 真机上的稳定性断连、重连和生命周期5.1 断线检测与自动重连真机上蓝牙连接保活情况比模拟器复杂得多用户走远了、系统回收、模块掉电、另一台手机抢占了连接都可能导致断开。好在小程序提供了 onBLEConnectionStateChange 监听连接状态变化。一般策略是在连接管理模块这个层级做自动重连而且要做好重连锁和次数限制否则会变成无限重连死循环既耗电又影响体验let reconnectTimes 0 let isReconnecting false wx.onBLEConnectionStateChange(res { if (res.deviceId ! currentDeviceId) return if (res.state disconnected) { if (isReconnecting) return isReconnecting true reconnectTimes if (reconnectTimes 3) { setTimeout(() { connectDevice(currentDeviceId).then(() { reconnectTimes 0 }).finally(() { isReconnecting false }) }, 1000 * reconnectTimes) // 退避间隔1s、2s、3s递增 } else { isReconnecting false // 提示用户手动重连 } } })注意reconnectTimes 在连接成功后才清零在断线状态里不能清零。另外如果在等待重连期间用户主动离开了页面要能取消这次重连否则页面都销毁了重连成功后回调还会继续执行很容易出Bug。5.2 页面生命周期与蓝牙资源释放蓝牙资源不是无限可用的尤其是页面切换频繁的小程序。onHide 时最好停止扫描并关闭连接这个要看业务场景如果用户只是临时切后台回来还要继续通信就不该直接 closeBLEConnection。但至少要在 onUnload 时清理干净onUnload() { if (this.deviceId) { wx.closeBLEConnection({ deviceId: this.deviceId }) } wx.closeBluetoothAdapter({}) }closeBluetoothAdapter 会把所有已连接设备都断开适用于页面完全销毁的场景。如果只是从A页面跳到B页面、B页面还要用同一个连接就需要在全局或页面公共模块里共享连接状态而不是每个页面各自创建连接。之前我把连接逻辑直接写在页面里来回切换页面时重复打开适配器结果就是把正在通信的通道打断用户反馈过好几次。后来才把连接管理抽到独立的模块里页面只负责展示状态和收发数据。5.3 常见错误码和解决方向真机上报错别急着搜索文档先把错误码记下来对照。这里列几个我遇到最多的错误码含义解决方向10000未初始化蓝牙适配器先调 openBluetoothAdapter10001蓝牙状态不可用检查系统蓝牙是否开启10002未连接目标设备重新走 createBLEConnection10003找不到服务或特征核对 serviceId、characteristicId10004连接失败停止扫描后再连接、检查信号强度10005找不到设备确认 deviceId 是否过期重新搜索10006连接已断开做断线重连逻辑10011系统蓝牙已关闭引导用户打开蓝牙遇到 10004 这类连接失败我的排障顺序是先停扫描、再查信号、最后查设备是否仍在正常广播。真实原因往往是三者之一跟代码逻辑关系不大。6. 复盘把连接管理做成一个独立的小模块6.1 状态管理与防重复调用经历过几次所有回调看起来都正常但设备没反应之后我坚持把蓝牙逻辑收拢成一个独立的管理器。核心是几个全局状态adapterReady、deviceId、serviceId、writeCharId、notifyCharId以及几个标志位防止重复调用。比如 notify 开启过一次就不要每次都调 notifyBLECharacteristicValueChange。有的同学在每次收到数据后都重新开启一次 notify这种操作在部分Android机型上反而会断开订阅导致设备不再上报。正确做法是订阅一次记住已订阅状态只有连接断开重连后才需要重新订阅。防重复连接也一样createBLEConnection 如果已经处于连接中再点一次按钮又发起一次连接某些模块会被搞懵甚至直接不响应。按钮点击时先判断当前状态handleConnect() { if (this.isConnecting || this.isConnected) return this.isConnecting true // 执行连接流程 }这种锁在设计时一定要考虑清楚什么状态下能解锁连接成功、连接失败、超时、断线都要有对应的清理逻辑否则一个锁卡死用户会彻底无法操作。6.2 我调试时最常用的三个手段说实话蓝牙功能在微信开发者工具里几乎没法完整调试必须真机。我自己调试主要靠三件事第一vConsole日志。真机调试模式下把每次API调用的成功、失败、回调数据全部打日志尤其是 errMsg 和 errCode。绝大多数问题一眼就能从错误码里定位。第二第三方蓝牙调试工具。先用它确认设备本身能不能连、能不能读写。如果第三方工具能正常收发问题基本在小程序侧如果第三方工具也一样异常那就是模块或协议问题。这一招能把排查范围快速缩小一半以上。第三硬件端打印或串口日志。设备接收到数据后是否打印、是否回ACK能帮助判断问题是出在数据没到设备还是设备没回数据。因为小程序端偶尔会出现写入成功回调到了但数据实际没发送成功的情况。6.3 几个容易被忽略的细节最后补充几个我在实际项目中反复踩的细节不要在页面 onLoad 里自动打开蓝牙适配器真机上弹权限框时机不对用户体验会很差而且部分设备授权失败后再引导很麻烦。advertisData 不是所有设备都有值不能依赖它做唯一的设备识别。最稳妥的方案是能匹配 name 就匹配 name匹配不到就把设备列表展示给用户手动选。部分透传模块有工作模式之分有的上电默认是AT命令模式需要先发特定指令切换到透传模式否则你写的业务数据全被模块当成AT命令处理了。这个必须先看模块文档再动手写代码。大数据量传输时别只依赖分包发送接收端也要有缓冲区机制发送端最好设计成收到ACK再发下一包否则容易出现首包和尾包丢失。连接成功后让用户界面和真实连接状态保持同步。比如显示已连接之前先确认 getBLEDeviceServices 已经成功而不是只看 createBLEConnection 的 success 回调。这篇关于微信小程序蓝牙连接与读写数据的实战记录就讲到这儿。如果你正在折腾类似功能建议从链路梳理开始把每一步都用Promise封装好再慢慢完善重连和协议解析。我最后再分享一个小技巧蓝牙功能调通之后记得把调试时用到的设备名、服务UUID、特征UUID这些常量集中放到一个配置文件里后面换设备型号或者交给别人维护时能省掉非常多来回翻代码的时间。

关于恒美微站

恒美微站专注于为个体商户、工作室提供极简自助建站服务,让每个人都能轻松拥有专业网站。

快速链接

  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心

服务项目

  • 可视化建站
  • 拖拽编辑
  • 主题定制
  • SEO 优化
  • 网站托管

联系方式

  • 📍 地址:北京市朝阳区建国路 88 号
  • 📞 电话:400-888-8888
  • ✉️ 邮箱:info@hmyw.cn
  • 🕐 时间:周一至周日 9:00-18:00

© 2024 恒美微站 hmyw.cn 版权所有 | 京 ICP 备 12345678 号