一、引言
在许多物联网(IoT)应用场景中,如智能手环、蓝牙耳机、智能家居、传感器设备等,通过蓝牙与设备通信是必不可少的环节。微信/支付宝小程序如果想与 BLE(Bluetooth Low Energy)设备交互,需要借助对应平台提供的蓝牙 API。uniapp 作为一个跨端开发框架,将这些 API 进行了统一封装,让你能用一套代码同时支持多端环境。
本指南将带你从零开始,学习如何在 uniapp 小程序中:
- 初始化蓝牙模块(检查适配器状态)
- 扫描附近可用设备
- 连接指定蓝牙设备
- 发现设备服务与特征
- 开启特征消息订阅并读写数据
- 断开与销毁连接
- 处理异常与边界情况
内容同时配备ASCII 流程图与详细代码示例,让你更容易理解蓝牙流程背后的原理。在开始之前,请确保你的蓝牙设备为 BLE(低功耗蓝牙)协议,且已正确打开,并与手机配对或处于可被扫描状态。
二、蓝牙通信基础
2.1 BLE(Bluetooth Low Energy)概念
- BLE(低功耗蓝牙):主要用于短距离、低功耗的数据交换,适合物联网设备。
- BLE 设备由 服务(Service) 和 特征(Characteristic) 组成: - Service:一组相关特征的集合,比如“心率服务”中包含多个“心率测量特征”。
- Characteristic:可读或可写的具体数据项,比如“心率值”、“电池电量”等。
 
在小程序中,常见的 BLE 流程为:
- 打开蓝牙模块 → 2. 扫描设备 → 3. 连接指定设备 → 4. 获取服务列表 → 5. 获取特征列表 → 6. 读写/订阅特征 → 7. 断开连接 → 8. 关闭蓝牙模块(可选)。
2.2 小程序蓝牙适配与 uniapp 封装
不同平台(微信小程序、支付宝小程序、百度小程序等)对蓝牙的原生 API 稍有差异,但 uniapp 在运行时会映射到对应平台。本文所有示例均以微信小程序为主,支付宝小程序模式下也基本一致,只需将 uni 替换为 my(支付宝)或 swan(百度)即可。
在 uniapp 中,一些常用核心方法包括:
- uni.openBluetoothAdapter()
- uni.onBluetoothAdapterStateChange(callback)
- uni.startBluetoothDevicesDiscovery(options)
- uni.onBluetoothDeviceFound(callback)
- uni.createBLEConnection(options)
- uni.getBLEDeviceServices(options)
- uni.getBLEDeviceCharacteristics(options)
- uni.notifyBLECharacteristicValueChange(options)
- uni.onBLECharacteristicValueChange(callback)
- uni.writeBLECharacteristicValue(options)
- uni.closeBLEConnection(options)
- uni.closeBluetoothAdapter()
本指南后续会依序介绍每个步骤的使用方法与细节。
三、环境准备与权限配置
3.1 app.json / manifest.json 配置
在小程序中使用蓝牙,需要在 app.json(或对应页面的 json 配置)里声明使用蓝牙模块权限。以微信小程序为例,app.json 中应该包含:
// app.json
{
  "pages": [
    "pages/index/index",
    "pages/bluetooth/bluetooth"
  ],
  "window": {
    "navigationBarTitleText": "蓝牙示例"
  },
  // 在 "permission" 节点中声明蓝牙权限(仅微信小程序 2.10.0+ 支持)
  "permission": {
    "scope.userLocation": {
      "desc": "您的地理位置将用于搜索附近的蓝牙设备" 
    }
  }
}说明:
- 扫描蓝牙 可能需要打开设备定位权限,尤其是在 iOS 设备上,否则无法扫描到 BLE 设备。
- 在支付宝/百度小程序,可参考它们的权限要求,无需额外在
app.json中声明,但用户会在首次使用时被弹窗授权。
3.2 兼容性检查
在真正调用蓝牙 API 前,需要检查当前环境是否支持蓝牙。常见做法:
// utils/bluetooth.js
export function checkBluetoothAdapter() {
  return new Promise((resolve, reject) => {
    uni.openBluetoothAdapter({
      success(res) {
        console.log('蓝牙适配器已启动', res);
        resolve(res);
      },
      fail(err) {
        console.error('打开蓝牙适配器失败', err);
        uni.showToast({ title: '请检查手机蓝牙或系统版本是否支持', icon: 'none' });
        reject(err);
      }
    });
  });
}- 调用时机:建议在页面 onLoad或用户点击“连接蓝牙”按钮时调用,以免小程序启动即打开蓝牙,影响性能。
四、蓝牙扫描与发现设备
4.1 打开蓝牙适配器
- 在页面的 methods或onLoad里调用uni.openBluetoothAdapter(),启动本机蓝牙模块。
- 监听蓝牙状态变化,若用户关闭蓝牙或设备离线,可及时提示。
<script>
export default {
  data() {
    return {
      isAdapterOpen: false
    };
  },
  onLoad() {
    this.initBluetooth();
  },
  methods: {
    initBluetooth() {
      uni.openBluetoothAdapter({
        success: (res) => {
          console.log('openBluetoothAdapter success', res);
          this.isAdapterOpen = true;
          // 监听蓝牙适配器状态变化
          uni.onBluetoothAdapterStateChange((adapterState) => {
            console.log('adapterState changed', adapterState);
            this.isAdapterOpen = adapterState.available;
            if (!adapterState.available) {
              uni.showToast({ title: '蓝牙已关闭', icon: 'none' });
            }
          });
        },
        fail: (err) => {
          console.error('openBluetoothAdapter fail', err);
          uni.showToast({ title: '请先打开手机蓝牙', icon: 'none' });
        }
      });
    }
  }
};
</script>- uni.onBluetoothAdapterStateChange(callback)会实时回调蓝牙模块的- available(是否可用)和- discovering(是否正在扫描)等状态。
- 如果用户在小程序后台或其他地方关闭蓝牙,需要通过该监听及时更新 UI 并停止相关操作。
4.2 开始扫描蓝牙设备
- 在确认适配器可用后,调用 uni.startBluetoothDevicesDiscovery()开始扫描。
- 可通过传入 services(要搜索的服务 UUID 列表)参数进行定向扫描;如果想搜索所有设备则无需传入。
- 监听 uni.onBluetoothDeviceFound(callback),在回调里获取到附近每个新发现的设备信息。
<template>
  <view class="container">
    <button @click="startScan">开始扫描</button>
    <text v-if="isScanning">扫描中...</text>
    <view class="device-list">
      <view 
        v-for="(dev, index) in devices" 
        :key="dev.deviceId" 
        class="device-item"
        @click="connectDevice(dev)"
      >
        <text>{{ dev.name || '未知设备' }} ({{ dev.deviceId }})</text>
        <text>RSSI: {{ dev.RSSI }}</text>
      </view>
    </view>
  </view>
</template>
<script>
export default {
  data() {
    return {
      isAdapterOpen: false,
      isScanning: false,
      devices: [] // 已发现设备列表
    };
  },
  onLoad() {
    this.initBluetooth();
  },
  methods: {
    initBluetooth() {
      uni.openBluetoothAdapter({
        success: () => {
          this.isAdapterOpen = true;
          uni.onBluetoothAdapterStateChange((state) => {
            this.isAdapterOpen = state.available;
            this.isScanning = state.discovering;
          });
        },
        fail: () => {
          uni.showToast({ title: '请先打开手机蓝牙', icon: 'none' });
        }
      });
    },
    startScan() {
      if (!this.isAdapterOpen) {
        uni.showToast({ title: '蓝牙未初始化', icon: 'none' });
        return;
      }
      this.devices = [];
      uni.startBluetoothDevicesDiscovery({
        // allowDuplicatesKey: false, // 微信小程序可选,是否重复上报同一设备
        success: (res) => {
          console.log('start discovery success', res);
          this.isScanning = true;
          // 监听新设备发现事件
          uni.onBluetoothDeviceFound((res) => {
            // res.devices 为数组
            res.devices.forEach((device) => {
              // 过滤已经存在的设备
              const exists = this.devices.findIndex((d) => d.deviceId === device.deviceId) !== -1;
              if (!exists) {
                this.devices.push(device);
              }
            });
          });
        },
        fail: (err) => {
          console.error('start discovery fail', err);
          uni.showToast({ title: '扫描失败', icon: 'none' });
        }
      });
    },
    stopScan() {
      uni.stopBluetoothDevicesDiscovery({
        success: () => {
          this.isScanning = false;
          console.log('停止扫描');
        }
      });
    },
    connectDevice(device) {
      // 点击设备后停止扫描
      this.stopScan();
      // 跳转到连接页面或执行连接逻辑
      uni.navigateTo({
        url: `/pages/bluetoothDetail/bluetoothDetail?deviceId=${device.deviceId}&name=${device.name}`
      });
    }
  },
  onUnload() {
    // 页面卸载时停止扫描以节省资源
    this.stopScan();
  }
};
</script>
<style>
.container {
  padding: 20px;
}
button {
  margin-bottom: 10px;
}
.device-list {
  margin-top: 10px;
}
.device-item {
  padding: 10px;
  border-bottom: 1px solid #eee;
}
</style>核心说明:
uni.startBluetoothDevicesDiscovery():开始扫描附近 BLE 设备。
uni.onBluetoothDeviceFound(callback):监听到新设备时回调,返回如{ devices: [{ deviceId, name, RSSI, advertisData, advertisServiceUUIDs }] }。
device.deviceId:唯一标识每个 BLE 设备,用于后续连接。- 过滤重复设备:微信小程序会多次上报同一设备,需自行去重(如示例中使用
deviceId)。
4.3 停止扫描
- 一旦找到目标设备并准备连接,应及时调用 uni.stopBluetoothDevicesDiscovery()停止扫描,否则会一直消耗手机资源和电量。
- 在页面 onUnload或用户后退时,也应调用停止扫描,避免扫码界面卸载后仍在后台扫描。
五、连接蓝牙设备并发现服务
扫描到目标设备后,接下来要与该设备建立 BLE 连接,然后发现其提供的服务和特征。
5.1 创建 BLE 连接
进入设备详情页(例如 bluetoothDetail.vue),在页面 onLoad 中获取从列表页传来的 deviceId 和 name,然后调用 uni.createBLEConnection() 建立连接。
<template>
  <view class="container">
    <text>连接设备:{{ name }}</text>
    <text v-if="connected">已连接</text>
    <text v-else>正在连接...</text>
    <button v-if="connected" @click="getServices">获取服务</button>
    <view v-for="svc in services" :key="svc.uuid" class="service-item">
      <text>服务 UUID:{{ svc.uuid }}</text>
      <button @click="getCharacteristics(svc.uuid)">获取特征</button>
      <view v-for="char in characteristicsList[svc.uuid] || []" :key="char.uuid" class="char-item">
        <text>特征 UUID:{{ char.uuid }}</text>
        <text>properties:{{ JSON.stringify(char.properties) }}</text>
        <!-- 可根据 properties 选择读写/订阅 -->
        <button v-if="char.properties.read" @click="readCharacteristic(svc.uuid, char.uuid)">读取</button>
        <button v-if="char.properties.write" @click="writeCharacteristic(svc.uuid, char.uuid)">写入</button>
        <button v-if="char.properties.notify" @click="notifyCharacteristic(svc.uuid, char.uuid)">订阅通知</button>
      </view>
    </view>
  </view>
</template>
<script>
export default {
  data() {
    return {
      deviceId: '',
      name: '',
      connected: false,
      services: [],
      characteristicsList: {} // 以 serviceUUID 为 key 存储特征列表
    };
  },
  onLoad(options) {
    // options.deviceId 和 options.name 来自扫描页
    this.deviceId = options.deviceId;
    this.name = options.name || '未知设备';
    this.createConnection();
  },
  methods: {
    createConnection() {
      uni.createBLEConnection({
        deviceId: this.deviceId,
        success: (res) => {
          console.log('createBLEConnection success', res);
          this.connected = true;
          // 监听连接状态变化
          uni.onBLEConnectionStateChange((data) => {
            console.log('连接状态 change:', data);
            if (!data.connected) {
              this.connected = false;
              uni.showToast({ title: '设备已断开', icon: 'none' });
            }
          });
        },
        fail: (err) => {
          console.error('createBLEConnection fail', err);
          uni.showToast({ title: '连接失败', icon: 'none' });
        }
      });
    },
    getServices() {
      uni.getBLEDeviceServices({
        deviceId: this.deviceId,
        success: (res) => {
          console.log('getBLEDeviceServices', res);
          this.services = res.services;
        },
        fail: (err) => {
          console.error('getBLEDeviceServices fail', err);
        }
      });
    },
    getCharacteristics(serviceId) {
      uni.getBLEDeviceCharacteristics({
        deviceId: this.deviceId,
        serviceId,
        success: (res) => {
          console.log('getBLEDeviceCharacteristics', res);
          this.$set(this.characteristicsList, serviceId, res.characteristics);
        },
        fail: (err) => {
          console.error('getBLEDeviceCharacteristics fail', err);
        }
      });
    },
    readCharacteristic(serviceId, charId) {
      uni.readBLECharacteristicValue({
        deviceId: this.deviceId,
        serviceId,
        characteristicId: charId,
        success: (res) => {
          console.log('read success', res);
          // 监听数据返回
          uni.onBLECharacteristicValueChange((charRes) => {
            console.log('characteristic change', charRes);
            // charRes.value 为 ArrayBuffer
            const data = this.ab2hex(charRes.value);
            console.log('读取到的数据(16进制)', data);
          });
        },
        fail: (err) => {
          console.error('read fail', err);
        }
      });
    },
    writeCharacteristic(serviceId, charId) {
      // 示例:写入一个 0x01 0x02 的 ArrayBuffer 数据到特征
      const buffer = new ArrayBuffer(2);
      const dataView = new DataView(buffer);
      dataView.setUint8(0, 0x01);
      dataView.setUint8(1, 0x02);
      uni.writeBLECharacteristicValue({
        deviceId: this.deviceId,
        serviceId,
        characteristicId: charId,
        value: buffer,
        success: (res) => {
          console.log('write success', res);
        },
        fail: (err) => {
          console.error('write fail', err);
        }
      });
    },
    notifyCharacteristic(serviceId, charId) {
      // 开启低功耗设备特征 notifications
      uni.notifyBLECharacteristicValueChange({
        state: true, // true: 启用通知;false: 关闭通知
        deviceId: this.deviceId,
        serviceId,
        characteristicId: charId,
        success: (res) => {
          console.log('notify change success', res);
        },
        fail: (err) => {
          console.error('notify change fail', err);
        }
      });
      // 需监听 onBLECharacteristicValueChange 事件
      uni.onBLECharacteristicValueChange((charRes) => {
        console.log('notify char change', charRes);
        const data = this.ab2hex(charRes.value);
        console.log('notify 数据(16进制)', data);
      });
    },
    // ArrayBuffer 转 hex 字符串,便于调试
    ab2hex(buffer) {
      const hexArr = Array.prototype.map.call(
        new Uint8Array(buffer),
        (byte) => byte.toString(16).padStart(2, '0')
      );
      return hexArr.join(' ');
    },
    disconnect() {
      uni.closeBLEConnection({
        deviceId: this.deviceId,
        success: () => {
          console.log('已断开连接');
          this.connected = false;
        }
      });
    }
  },
  onUnload() {
    // 页面卸载时断开连接
    if (this.connected) {
      this.disconnect();
    }
  }
};
</script>
<style>
.container {
  padding: 20px;
}
.service-item, .char-item {
  margin-top: 10px;
  padding: 10px;
  border: 1px solid #eee;
}
button {
  margin-top: 5px;
}
</style>关键说明
- uni.createBLEConnection({ deviceId }):对指定- deviceId建立 BLE 连接,连接成功后才能读写。
- uni.onBLEConnectionStateChange(callback):实时监听设备连接状态,如果对方设备断电或超出范围会触发此回调。
- uni.getBLEDeviceServices({ deviceId }):获取该设备上所有公开的服务(返回- services: [{ uuid, isPrimary }])。
- uni.getBLEDeviceCharacteristics({ deviceId, serviceId }):获取指定服务下的所有特征(返回- characteristics: [{ uuid, properties: { read, write, notify, indicate } }])。
- 读写特征: - 读取:调用 uni.readBLECharacteristicValue({ deviceId, serviceId, characteristicId })后,需要再使用uni.onBLECharacteristicValueChange(callback)回调才能拿到数据。
- 写入:调用 uni.writeBLECharacteristicValue({ value: ArrayBuffer });注意写入数据必须是ArrayBuffer。
 
- 读取:调用 
- 订阅特征通知:调用 uni.notifyBLECharacteristicValueChange({ state: true, ... }),然后在uni.onBLECharacteristicValueChange中获得服务器推送的变化。
- 断开连接:uni.closeBLEConnection({ deviceId }),断开后需要调用uni.closeBluetoothAdapter()释放蓝牙模块资源(可选)。
六、完整蓝牙流程 ASCII 图解
┌─────────────────────────────────────────────────┐
│               用户打开“蓝牙页”                  │
└─────────────────────────────────────────────────┘
                           ↓
┌─────────────────────────────────────────────────┐
│   1. uni.openBluetoothAdapter()                │
│   └──> 初始化蓝牙模块,开启本机 BLE 适配器      │
└─────────────────────────────────────────────────┘
                           ↓
┌─────────────────────────────────────────────────┐
│   2. uni.startBluetoothDevicesDiscovery()      │
│   └──> 开始扫描附近 BLE 设备                   │
└─────────────────────────────────────────────────┘
                           ↓
┌─────────────────────────────────────────────────┐
│   3. uni.onBluetoothDeviceFound(callback)      │
│   └──> 回调返回扫描到的设备列表 devices[]       │
│       devices 包含 deviceId、name、RSSI 等      │
└─────────────────────────────────────────────────┘
                           ↓
┌─────────────────────────────────────────────────┐
│       用户从列表中点击 “连接” 某设备            │
│       → 调用 uni.stopBluetoothDevicesDiscovery │
└─────────────────────────────────────────────────┘
                           ↓
┌─────────────────────────────────────────────────┐
│   4. uni.createBLEConnection({ deviceId })     │
│   └──> 与目标设备建立 BLE 连接                  │
└─────────────────────────────────────────────────┘
                           ↓
┌─────────────────────────────────────────────────┐
│   5. uni.onBLEConnectionStateChange(callback)  │
│   └──> 监听连接状态,如断开会触发                │
└─────────────────────────────────────────────────┘
                           ↓
┌─────────────────────────────────────────────────┐
│   6. uni.getBLEDeviceServices({ deviceId })    │
│   └──> 获取设备所有 Service 列表                │
└─────────────────────────────────────────────────┘
                           ↓
┌─────────────────────────────────────────────────┐
│   7. uni.getBLEDeviceCharacteristics({        │
│         deviceId, serviceId })                │
│   └──> 获取该 service 下的所有 Characteristic  │
│           { uuid, properties: { read, write, notify, ... } } │
└─────────────────────────────────────────────────┘
                           ↓
┌─────────────────────────────────────────────────┐
│   8. 读/写/订阅 特征                            │
│   ├─ uni.readBLECharacteristicValue(...)      │
│   │     └─ onBLECharacteristicValueChange     │
│   ├─ uni.writeBLECharacteristicValue(...)     │
│   └─ uni.notifyBLECharacteristicValueChange(..│
│         └─ onBLECharacteristicValueChange     │
└─────────────────────────────────────────────────┘
                           ↓
┌─────────────────────────────────────────────────┐
│   9. uni.closeBLEConnection({ deviceId })      │
│   └──> 断开与设备连接                           │
└─────────────────────────────────────────────────┘
                           ↓
┌─────────────────────────────────────────────────┐
│  10. uni.closeBluetoothAdapter() (可选)         │
│  └──> 关闭本机蓝牙模块,释放系统资源            │
└─────────────────────────────────────────────────┘七、常见问题与注意事项
- iOS 扫描需打开定位权限 - iOS 系统要求在使用 BLE 扫描前,必须打开地理位置权限,否则将无法扫描到任何设备。务必在 app.json中声明scope.userLocation并在运行时调用uni.authorize({ scope: 'scope.userLocation' })。
- 示例: - uni.authorize({ scope: 'scope.userLocation', success: () => { // 已授权,继续扫描 this.startScan(); }, fail: () => { uni.showModal({ title: '提示', content: '需要开启定位权限才能扫描蓝牙设备', showCancel: false }); } });
 
- iOS 系统要求在使用 BLE 扫描前,必须打开地理位置权限,否则将无法扫描到任何设备。务必在 
- 重连机制 - 如果设备断开连接,可监听 - uni.onBLEConnectionStateChange,在监听到- connected: false时尝试重连:- uni.onBLEConnectionStateChange((res) => { if (!res.connected) { this.createConnection(); // 最简单的重连策略 } });
- 注意避免无限重连导致阻塞,可做一定次数或时延后重试。
 
- 写入数据长度限制 - BLE 单次写入的数据包长度有限制,通常最大约 20 字节(具体取决于设备 MTU)。如果需要写入更大数据,需要自行分包。
- 示例分包: - function writeInChunks(deviceId, serviceId, charId, dataBuffer) { const mtu = 20; // 一次最大写入 20 字节 let offset = 0; while (offset < dataBuffer.byteLength) { const length = Math.min(mtu, dataBuffer.byteLength - offset); const chunk = dataBuffer.slice(offset, offset + length); uni.writeBLECharacteristicValue({ deviceId, serviceId, characteristicId: charId, value: chunk }); offset += length; } }
 
- 订阅特征通知前必须先启用 notify - 如果在调用 uni.onBLECharacteristicValueChange前未调用uni.notifyBLECharacteristicValueChange({ state: true }),则不会收到变化回调。
 
- 如果在调用 
- 关闭蓝牙时先断开再关闭适配器 - 调用 uni.closeBLEConnection后再调用uni.closeBluetoothAdapter(),否则可能无法正常断开连接。
 
- 调用 
- 不同平台 API 差异 - 支付宝小程序:方法名为 my.openBluetoothAdapter、my.startBluetoothDevicesDiscovery等,与微信小程序一致;
- 百度小程序:对应 swan.openBluetoothAdapter、swan.startBluetoothDevicesDiscovery等;
- 在 uniapp 中使用 uni.*封装后自动映射,通常无需区分。
 
- 支付宝小程序:方法名为 
- 断电、超距断开提醒 - 当设备主动断电或超出 BLE 范围时,会触发 onBLEConnectionStateChange,需及时在 UI 上提示用户重新连接。
 
- 当设备主动断电或超出 BLE 范围时,会触发 
- RSSI(信号强度)过滤 - 在 - onBluetoothDeviceFound返回的- device.RSSI(信号强度)可以进行过滤,只展示接近的设备:- if (device.RSSI > -70) { // 信号较强的设备,才加入列表 this.devices.push(device); }
 
八、总结
本文详细介绍了在 uniapp 小程序 中连接 BLE 设备的完整实战流程,从打开蓝牙适配器、扫描设备、连接设备、发现服务与特征、到读写订阅特征、断开连接的每一个环节,并提供了丰富的代码示例与 ASCII 流程图,帮助你更清晰地理解蓝牙通信的原理与步骤。
关键要点回顾:
- 初始化蓝牙适配器:uni.openBluetoothAdapter()并监听onBluetoothAdapterStateChange,确保蓝牙可用。
- 扫描设备:uni.startBluetoothDevicesDiscovery()+uni.onBluetoothDeviceFound,将多个蓝牙设备信息收集到列表,并去重。
- 建立 BLE 连接:uni.createBLEConnection({ deviceId }),并监听onBLEConnectionStateChange。
- 发现服务与特征:uni.getBLEDeviceServices→uni.getBLEDeviceCharacteristics,拿到可读写、可订阅的特征。
- 数据通信: - 读取:uni.readBLECharacteristicValue()+uni.onBLECharacteristicValueChange;
- 写入:uni.writeBLECharacteristicValue(),注意分包;
- 订阅通知:uni.notifyBLECharacteristicValueChange({ state: true })+uni.onBLECharacteristicValueChange。
 
- 读取:
- 断开与清理:页面卸载或用户退出时,先 uni.closeBLEConnection断开连接,再uni.closeBluetoothAdapter关闭适配器,避免资源泄漏。
- 权限与异常处理:iOS 需授权定位才能扫描,蓝牙关闭或超距会触发回调;写入需要分包,二维码扫描时也同理。