简介:微信小程序蓝牙连接全网最完整版教程及配套源码资料包,面向需要在微信生态内实现低功耗蓝牙(BLE)硬件互动的开发者,覆盖蓝牙权限声明、适配器状态检查、设备扫描、停止扫描、发起连接、监听 GATT 服务、发现特征值并读取数据的完整链路。资料压缩包共 2 个文件,以 md 格式的实战笔记为主,辅助 xml 工程配置文件,整体仅 4KB,内容精炼,便于直接对照学习。配套 blueDevice-master 示例项目展示了可在真实场景中迁移的标准写法,包括通过 wx.getBluetoothAdapterStatus 检测蓝牙是否开启、用 wx.startBluetoothDevicesDiscovery 扫描设备、根据 service UUID 过滤目标、连接后回调监听与特征值获取等细节;同时针对蓝牙未开启、扫描失败、连接失败、读取特征值异常等常见问题给出日志定位与处理思路,适合健康监测、智能家居、外设控制等物联网项目快速落地。已有 3376 人学习下载,适合有小程序基础、希望短时间掌握蓝牙硬件连调流程的开发者。
1. 微信小程序蓝牙连接:为什么说“完整版”是指踩坑路径
微信小程序蓝牙连接,大概是物联网类小程序里最让人头疼的一块。很多开发者一开始以为只是“调几个API的事”,结果真机一跑,iOS上连上了收不到数据,Android上压根扫不到设备,同一个蓝牙模块换个手机厂商又是一种表现。这个标题说“全网最完整版”,其实指的不是API文档的完整,而是把蓝牙从初始化到断线重连这条路上所有的坑都趟一遍。这篇文章不是贴一份官方代码让你复制,而是按一条真实可落地的链路来讲:微信小程序怎么调起蓝牙、怎么扫描、怎么连上、怎么读写数据、怎么排查问题。适合正准备做蓝牙硬件对接的开发者,也适合已经在做但被兼容性折磨的人。你不需要懂底层射频协议,但需要理解GATT这层概念,否则后面代码写到一半会卡住。
2. 微信小程序蓝牙连接前必须理解的 BLE 协议与 API 边界
2.1 BLE 里真正和你相关的是 GATT 这层
微信小程序里的蓝牙连接,指的是低功耗蓝牙(BLE),不是经典蓝牙。BLE 的协议栈分很多层,但小程序开发者基本只需要关心最上层的 GATT(通用属性协议)。GATT 定义了设备之间数据交互的模型:外围设备(比如温湿度传感器、体脂秤、智能锁)作为服务端,暴露出一组服务(Service),每个服务下面挂若干特征值(Characteristic)。你的小程序作为中心设备,去发现这些服务,然后读写特征值,或者订阅特征值的通知。
理解这个模型是写代码的前提。你拿到的硬件文档通常会写“服务 UUID 为 FFE0,特征值 UUID 为 FFE1,写 FFE1 发送开锁指令”。这句话翻译成 GATT 语言就是:这个设备有一个主服务,UUID 是 FFE0,服务里有一个可写的特征值,UUID 是 FFE1。小程序要做的第一步不是直接写,而是先连上设备,再从设备那里拉取服务列表,找到 FFE0,再找到 FFE0 底下的 FFE1,这时才具备读写条件。
很多人在这一步就翻车了。硬件文档里写的 UUID 是 16 位的短格式,比如 FFE0,但小程序拿到的是完整的 128 位 UUID。这是因为蓝牙规范里有一个基础 UUID,短格式需要拼成完整格式才能比对。常见的基础 UUID 是0000XXXX-0000-1000-8000-00805F9B34FB,所以 FFE0 在设备端返回的完整 UUID 是0000FFE0-0000-1000-8000-00805F9B34FB。你如果直接用 FFE0 去比对,永远匹配不上,这是新手最常见的困惑之一。
2.2 微信小程序蓝牙 API 调用链与基础兼容差异
微信小程序的蓝牙 API 是一组异步调用链,核心流程是固定的:初始化适配器 → 开始扫描 → 监听发现设备 → 停止扫描 → 连接设备 → 获取服务 → 获取特征值 → 读写或订阅。整条链路每一步都是异步回调,没有 Promise 封装,所以在业务代码里通常会自己封装一层 Promise,否则会陷入回调地狱。
function openBluetooth() { return new Promise((resolve, reject) => { wx.openBluetoothAdapter({ success: resolve, fail: (err) => reject(err) }) }) }这段代码是初始化蓝牙适配器的最小封装。wx.openBluetoothAdapter的作用是打开小程序对蓝牙功能的控制权,注意它不负责打开手机系统的蓝牙开关,只是初始化适配器。如果手机蓝牙本身是关着的,这个 API 会失败。在 iOS 上,首次调用会弹出系统授权框,用户拒绝后调用会失败,后续需要引导用户去设置页打开授权。
兼容性差异从第一步就开始了。Android 上如果系统蓝牙没有开启,调用wx.openBluetoothAdapter会直接走 fail 回调,错误信息通常是code: 10001, message: system bluetooth not available。iOS 上行为略有区别,系统蓝牙关闭时同样会失败,但错误信息可能不同。所以初始化之前的判断逻辑不能只依赖一个返回值,最好在业务里做两层检查:先通过wx.getSystemInfoSync()拿平台信息,Android 平台下先判断系统蓝牙状态,再做适配器初始化。
还有一个经常被忽视的点:wx.openBluetoothAdapter是全局的,整个小程序生命周期内只需要调用一次。有些开发者在每个页面 onLoad 里都调一次,这会导致 iOS 上重复弹授权框。正确的做法是在 App 启动时或者蓝牙工具模块里做单例初始化,后续页面直接复用。
3. 微信小程序蓝牙连接主流程:从初始化到建立连接的完整代码
3.1 初始化适配器与权限处理
完整的初始化不能只调用wx.openBluetoothAdapter,还需要注册两个全局监听器:wx.onBluetoothAdapterStateChange和wx.onBluetoothDeviceFound。前者监听系统蓝牙开关状态和适配器状态的变化,后者监听扫描到的设备。这两个监听器必须在连接流程开始之前注册,否则会漏掉设备发现事件。
function initBluetooth() { return new Promise((resolve, reject) => { wx.openBluetoothAdapter({ success: () => { wx.onBluetoothAdapterStateChange((res) => { console.log('adapter state change:', res.available, res.discovering) if (!res.available) { // 系统蓝牙被关闭,需要提示用户 } }) resolve() }, fail: (err) => reject(err) }) }) }这里的关键是注册wx.onBluetoothAdapterStateChange的时机。如果你在连接过程中系统蓝牙被用户关掉,适配器状态会变成不可用,此时需要主动断开已建立的连接并清理状态。如果不监听这个事件,就会出现一种诡异的情况:界面显示已连接,但设备已经断开了,所有写入指令全部失败,且没有任何提示。
权限方面,Android 6.0 以上扫描蓝牙设备需要定位权限,iOS 不需要。如果目标用户主要是 Android,必须在调用扫描之前检查wx.getSetting里是否已经授予定位权限。没有定位权限时,Android 上表现为wx.startBluetoothDevicesDiscovery直接 fail,错误信息里没有任何关于权限的提示,这是新手最容易误判的地方。
3.2 扫描设备:广播数据与设备名过滤策略
扫描设备的核心 API 是wx.startBluetoothDevicesDiscovery,设备发现通过wx.onBluetoothDeviceFound回调返回。这里有几个参数需要认真理解。allowDuplicatesKey决定是否重复上报同一设备,Android 上设置为 false 时设备只上报一次,iOS 上这个参数对结果的影响不同,建议设为 true,然后自己在代码里做去重,因为同一个设备在扫描期间可能会多次上报,而每次上报时广播数据可能不一样。
function startScan(timeout = 5000) { return new Promise((resolve) => { const devices = new Map() wx.onBluetoothDeviceFound((res) => { res.devices.forEach((device) => { const key = device.deviceId if (!devices.has(key)) { devices.set(key, device) } }) }) wx.startBluetoothDevicesDiscovery({ allowDuplicatesKey: true, interval: 0, success: () => { setTimeout(() => { wx.stopBluetoothDevicesDiscovery() resolve([...devices.values()]) }, timeout) } }) }) }这段代码用 Map 以deviceId为 key 做去重,设备发现回调不断往 Map 里塞数据,超时后停止扫描并返回设备列表。interval参数是上报间隔,单位毫秒,设为 0 表示尽快上报。实际使用中,Android 上如果设备上报频率很高,interval设置太小会导致回调过于频繁,UI 层卡顿;但如果设太大,设备列表出现会变得很慢。一般建议 Android 上设 20 到 50 毫秒,iOS 上这个参数无效。
扫描结果里有一个坑需要特别注意:设备名称字段localName在 iOS 和 Android 上行为不一致。iOS 上如果设备广播里没有名称字段,返回的设备对象里name可能为空;Android 某些设备返回的是localName而不是name。所以做设备列表时,筛选条件不能只依赖名称字段做完全匹配,否则会出现明明设备在旁边却列表里看不到的情况。
3.3 连接设备:deviceId 与超时控制
扫描到设备后,下一步是通过wx.createBLEConnection建立连接。这里传入的参数只有一个:deviceId。注意这个deviceId不是蓝牙设备的 MAC 地址,而是小程序内部生成的一个标识符。同一台物理设备在不同手机上扫描到的deviceId格式不一样,Android 上通常是 MAC 地址的形式,iOS 上是一个 UUID 字符串。这意味着你不能把deviceId当作用户维度的绑定凭证长期存储,后面会单独讲这个问题。
function connectDevice(deviceId, timeout = 10000) { return new Promise((resolve, reject) => { let isFinished = false const timer = setTimeout(() => { if (!isFinished) { isFinished = true wx.closeBLEConnection({ deviceId }) reject(new Error('连接超时')) } }, timeout) wx.createBLEConnection({ deviceId, success: (res) => { if (!isFinished) { isFinished = true clearTimeout(timer) resolve(res) } }, fail: (err) => { if (!isFinished) { isFinished = true clearTimeout(timer) reject(err) } } }) }) }连接必须有超时控制。真机环境中,蓝牙连接受到距离、信号干扰、设备状态等多方面因素影响,wx.createBLEConnection的 fail 回调有时要十几秒甚至更久才会触发。如果业务侧不做超时处理,用户点击连接后会一直卡在加载状态。常见的做法是 10 秒超时,超过后调用wx.closeBLEConnection释放资源,并提示用户重试。
连接成功后,wx.createBLEConnection的 success 回调里会自动拿到这个设备的服务信息吗?不会。连接成功只代表链路建立了,你还必须调用wx.getBLEDeviceServices去拿服务列表,再通过服务拿特征值,这中间每一层都需要再次发起异步请求。有开发者以为连接返回的 res 里带了服务数据,实际拿到的是一个空对象或基本对象,这是对 API 设计理解偏差导致的。
4. 服务发现与数据交互:把微信小程序蓝牙连接落到业务读写
4.1 获取服务与特征值:UUID 格式转换是关键
连接成功后,第一件事是获取设备的所有服务列表。调用wx.getBLEDeviceServices传入deviceId,返回的服务对象里有一个isPrimary字段,表示是否主服务。多数硬件设备的数据交互在主服务里,但也有一些设备把数据放在次服务中,所以建议不要过滤,直接遍历所有服务。
function getServiceAndCharacteristic(deviceId, targetServiceUUID, targetCharUUID) { return new Promise((resolve, reject) => { wx.getBLEDeviceServices({ deviceId, success: (res) => { const services = res.services || [] const fullTargetService = targetServiceUUID.includes('-') ? targetServiceUUID : `0000${targetServiceUUID}-0000-1000-8000-00805F9B34FB` const service = services.find((item) => { return item.uuid.toUpperCase() === fullTargetService.toUpperCase() }) if (!service) { reject(new Error(`未找到服务 ${targetServiceUUID}`)) return } wx.getBLEDeviceCharacteristics({ deviceId, serviceId: service.uuid, success: (charRes) => { const chars = charRes.characteristics || [] const fullTargetChar = targetCharUUID.includes('-') ? targetCharUUID : `0000${targetCharUUID}-0000-1000-8000-00805F9B34FB` const char = chars.find((item) => { return item.uuid.toUpperCase() === fullTargetChar.toUpperCase() }) if (!char) { reject(new Error(`未找到特征值 ${targetCharUUID}`)) return } resolve({ service, char }) }, fail: reject }) }, fail: reject }) }) }这段代码做了两件事:把 16 位短格式 UUID 拼接成完整 128 位 UUID,然后在返回的列表里做大小写不敏感匹配。注意硬件厂商给的 UUID 文档里经常是小写,而小程序返回的是大写,如果不做大小写归一化,匹配会失败。
拿到特征值对象后,它的properties字段标明了这个特征值支持哪些操作。常见的值有read、write、notify、indicate。你要根据硬件的用途来决定后续调用哪个 API。如果特征值只支持 notify,你却调用wx.writeBLECharacteristicValue,会直接报错property not support write。这一步的检查很像网络接口的鉴权校验,不做就是等着踩坑。
4.2 开启 notify 订阅并监听数据上报
硬件设备主动上报数据,是通过 notify 通知机制实现的。比如一个温湿度传感器,每秒钟通过特征值推送一次温湿度数据。小程序这边需要做两步:先调用wx.notifyBLECharacteristicValueChange开启订阅,再注册wx.onBLECharacteristicValueChange全局监听器接收数据。
function enableNotify(deviceId, serviceId, characteristicId) { return new Promise((resolve, reject) => { wx.notifyBLECharacteristicValueChange({ deviceId, serviceId, characteristicId, state: true, success: resolve, fail: reject }) }) } wx.onBLECharacteristicValueChange((res) => { const value = res.value const buffer = value.buffer || value const dataView = new Uint8Array(buffer) console.log('收到设备数据:', dataView) })wx.onBLECharacteristicValueChange注册后会对所有已连接设备的通知生效,不是针对某一个设备。如果你同时连接了多个设备,回调里拿到的res.deviceId可以用来区分是哪台设备发来的数据。很多开发者忽略了这一点,在多设备场景下把设备 A 的数据当成设备 B 的处理,业务逻辑全乱。
这里还有一个时序问题非常关键:必须先调用wx.notifyBLECharacteristicValueChange并确认 success,再去注册wx.onBLECharacteristicValueChange。有开发者反过来操作,先注册监听再开启 notify,结果前几帧数据丢失。因为 notify 开关生效后设备端立刻开始推送,而你的监听器还没注册好,数据从系统层溜走了。正确做法是回调里再用一个标志位,确保订阅成功后再开始处理业务数据。
4.3 写入指令:write 的返回值与分包策略
向设备写入指令需要把字符串转成 ArrayBuffer。小程序的wx.writeBLECharacteristicValue接收的是 ArrayBuffer,不是字符串,这是新手最常见的报错点。
function writeCommand(deviceId, serviceId, characteristicId, hexString) { const buffer = new ArrayBuffer(hexString.length / 2) const dataView = new DataView(buffer) for (let i = 0; i < hexString.length; i += 2) { dataView.setUint8(i / 2, parseInt(hexString.substr(i, 2), 16)) } return new Promise((resolve, reject) => { wx.writeBLECharacteristicValue({ deviceId, serviceId, characteristicId, value: buffer, success: resolve, fail: reject }) }) }这段代码把十六进制字符串转成 ArrayBuffer 再写入。写入失败时先看返回的错误码,最常见的报错是code: 10006,表示设备连接已断开;另一个常见错误是code: 10007,表示特征值不支持写操作。错误码里10006出现的频率最高,通常是因为设备在低功耗模式下主动断开了连接,不是你的代码问题。
写入长数据时要考虑 MTU 限制。微信小程序默认的 MTU 不是 20 字节,而是和系统协商后的值,一般 iOS 上是 185 字节左右,Android 上常见的厂商实现是 23 字节或更大。如果你一次写入超过 MTU 的数据,某些设备会直接丢弃,没有任何错误提示。常见做法是把数据切成 20 字节一包,逐包发送,包间延迟 20 到 50 毫秒。还有一个容易被忽略的点:wx.writeBLECharacteristicValue的 success 回调只代表数据到了系统蓝牙协议栈,不代表设备端已经收到并处理完成。依赖 success 回调去判断业务完成,在数据量大的场景下会有偏差。
5. 微信小程序蓝牙连接避坑指南:兼容性、时序与真机调试
5.1 设备名称拿不到:iOS 与 Android 的差异
现象:扫描列表里一半设备名称为空,显示“未知设备”,用户无法区分要连哪个。原因:iOS 上设备名称来自广播包里的kCBAdvDataLocalName字段,如果硬件设备广播时没把名称塞进去,iOS 上拿不到。Android 上部分厂商会额外返回蓝牙适配器缓存里的名称,所以同一台设备 Android 显示有名称、iOS 显示为空。解决:连接成功后,再从wx.getBLEDeviceServices或系统蓝牙信息里补一次名称查询。另一个补救手段是让硬件固件把设备名称写进广播字段,而不是只依赖经典的设备信息查询。
5.2 连接成功却收不到数据:notify 时序和特征值权限
现象:设备已经连上,服务列表也拿到了,但设备上报的数据一条都收不到。原因:没有先检查特征值的properties是否包含notify或indicate。某些设备支持 notify 但需要先向设备的控制特征值写入一条“开启上报”指令,硬件才会开始推送。也就是说,开启 notify 是必要条件,但不是充分条件。解决:先读硬件文档,确认是否需要先通过 write 发送开启指令,再调用wx.notifyBLECharacteristicValueChange。另外检查一下订阅的时机,尽量保证 notify 开启成功后再注册监听回调。
5.3 Android 上扫描不到设备:定位权限与蓝牙开关状态
现象:Android 真机上点击扫描,永远只有一个手机自身模拟的蓝牙设备,实际硬件看不到,或者直接走 fail 回调。原因:Android 6.0 以上扫描 BLE 设备需要定位权限,没有授权的话扫描直接失败。部分 Android 机型还需要打开定位服务开关,只授权权限但定位开关关闭,同样扫不到。解决:扫描前通过wx.getSetting检查权限,wx.authorize申请定位权限;如果授权被拒绝,引导用户去设置页手动打开。另外注意部分定制系统(某国产品牌)的蓝牙扫描偶尔需要用户打开系统定位服务,这属于厂商行为,只能提示用户手动确认。
5.4 断线重连与 deviceId 缓存策略
现象:App 退出再进入,连接历史设备时提示“设备不存在”或直接连接超时。原因:我们在 3.3 提到过,deviceId在 iOS 上每次扫描可能变化,Android 上虽然通常是 MAC 形式,但系统版本升级后也可能改变。持久化存储deviceId再拿来重连,在 iOS 上大概率会失败。解决:不要只存deviceId,把设备名称、连接时间一起存。重连时先重新扫描,根据设备名称匹配目标设备,拿到新的deviceId再建立连接。如果设备支持,更可靠的做法是让硬件端暴露一个固定的 MAC 地址,Android 上可以直接用 MAC 匹配,iOS 上依然需要扫描后匹配名称。
5.5 多设备并发连接:数量上限与通信串扰
现象:同时连接两个设备做数据采集,第二个设备连接成功后,第一个设备的数据不再上报,或者两个设备的数据在回调里混淆。原因:微信小程序对同时连接的 BLE 设备数量有限制,Android 上部分机型同时只能稳定维持 2 到 3 个连接。共享的wx.onBLECharacteristicValueChange回调如果不按deviceId做分发,数据就会串。解决:维护一个设备管理器,以deviceId为 key 存储每个设备的 serviceId、characteristicId 和业务处理器,监听回调里按deviceId路由到对应逻辑。超过 2 个设备时,优先考虑业务改造成逐个连接,而不是长期保持多连接。
6. 把微信小程序蓝牙连接做得更稳:日志埋点与断开状态机的进阶收尾
最后聊一个真正能让线上问题少一半的习惯:给蓝牙连接链路做一段结构化的日志埋点。因为蓝牙调试最大的痛点是“真机上出了问题,开发者不在旁边”,看不了调试器里的 console。我的做法是在蓝牙工具模块里维护一个环形日志数组,把每一次关键事件的耗时、参数、错误码记录下来,再用一个测试页面的接口导出给开发者,或者上报到自己的日志服务。
const logQueue = [] const MAX_LOG_COUNT = 200 function track(action, params, duration) { logQueue.push({ action, params, duration, ts: Date.now() }) if (logQueue.length > MAX_LOG_COUNT) { logQueue.shift() } }日志记录的内容至少包括:初始化耗时、扫描开始到发现目标设备的耗时、连接耗时、服务发现耗时、每次写入的指令及返回结果、每次 notify 数据的长度与间隔。通过这组数据,你能快速判断问题出在哪个阶段。比如用户反馈连不上,你看到日志里连接耗时超过 10 秒触发了超时,说明是信号问题;如果连接耗时只有 300 毫秒但之后没有任何服务发现,说明设备端的 GATT 服务没有按预期暴露,需要找硬件方核对固件。
断开状态机的设计同样重要。蓝牙连接不是二态,而是四态:未初始化、扫描中、已连接、断开重连中。每个状态对应一套 UI 表现和可操作动作。比如扫描中的状态下用户再次点击扫描,应该先调用wx.stopBluetoothDevicesDiscovery再重新开始;已连接状态下用户点了某个成员设备的操作按钮,应该先检查该设备是否还在连接列表里。我现在的习惯是,所有蓝牙操作入口都先过一遍状态机校验,不合法就 toast 提示,而不是直接调 API。这个习惯让我线上因为误操作产生的蓝牙异常减少了大概七成。
另外提一个日常调试的取巧方式:微信开发者工具里用蓝牙调试器插件配合模拟数据,只能验证基础流程是否通顺,真实设备上的兼容性问题还是要在真机上跑。我通常准备两台手机,一台 iOS 一台 Android,每次都跑通一遍完整流程再发版。蓝牙是典型的“真机见真章”的开发场景,模拟器里跑得再好,真机上可能连名称都读不到。这些习惯都是从一次次翻车里沉淀下来的,希望能帮到你以后少走一点弯路。
本文还有配套的精品资源,点击获取