原生微信小程序蓝牙通信

752 阅读10分钟

蓝牙通信

简介

  • 近期公司遇到的新需求,之前没有太了解过这一方面,捣鼓了两天终于写完了,主要需求是封装一个蓝牙工具包提供给小程序从而实现反复调用,需要注意的一点是接收方那边需要的数据类型。

本例是蓝牙通信功能

本例使用到的小程序原生api:

  • 打开蓝牙适配器:wx.openBluetoothAdapter,后续所有蓝牙模块功能都需要先打开适配器才能进行
  1. 搜寻蓝牙设备:
  • 开始搜寻:wx.startBluetoothDevicesDiscovery,此功能比较消耗性能,如果搜索到特定设备可即时停止
  • 发现设备事件:wx.onBluetoothDeviceFound,在这儿添加实时更新设备列表业务代码
  • 停止扫描:wx.onBluetoothDeviceFound,停止扫描新的蓝牙设备,当蓝牙扫描到指令设备时,需要即时关闭扫描保证性能
  • 关闭发现设备事件监听:wx.offBluetoothDeviceFound
  • 连接蓝牙设备: wx.createBLEConnection,通过传入蓝牙设备deviceId进行设备直连。这里的deviceId可通过上面扫描时wx.onBluetoothDeviceFound响应值获取
  • 监听蓝牙设备连接状态:wx.onBLEConnectionStateChange: 包括开发者主动连接或断开连接,设备丢失,连接异常断开等等
  1. 获取蓝牙服务
  • 获取蓝牙低功耗设备所有服务: wx.getBLEDeviceServices,通过
  • 根据特定服务UUID查询所有特征:[wx.getBLEDeviceCharacteristics](wx.getBLEDeviceCharacteristics),
  1. 监听蓝牙数据(实时获取蓝牙传回的信息)
  • 订阅特征变化:wx.notifyBLECharacteristicValueChange,开启订阅后续才能监听到蓝牙数据变化
  • 监听特征值变化:wx.onBLECharacteristicValueChange,通过监听事件触发数据解析业务
  • 发送数据(向蓝牙跳绳下发指令)
  • 下发指令:wx.writeBLECharacteristicValue,通过向蓝牙特定服务的对应特征值写入数据,完成交互。注意:要对应支持“write"属性的特征值
  1. 关闭蓝牙活动
  • wx.stopBluetoothDevicesDiscovery(): 停止扫描新设备
  • wx.offBluetoothDeviceFound():关闭扫描新设备监听事件
  • wx.offBLECharacteristicValueChange():关闭特征值变化监听(数据监听)
  • wx.offBLEConnectionStateChange():移除蓝牙低功耗连接状态改变事件的监听函数
  • wx.closeBLEConnection: 断开蓝牙连接
  • wx.closeBluetoothAdapter():关闭蓝牙适配器

正式开发

1.初始化蓝牙设备

// 初始化蓝牙设备
function initBluetooth() {
  // 版本过低不兼容
  console.log('wx.openBluetoothAdapter', wx.openBluetoothAdapter)
  if (!wx.openBluetoothAdapter) {
    wx.showModal({
      title: '提示',
      showCancel: false,
      content: '当前微信版本过低,无法使用该功能,请升级到最新微信版本后重试。',
    })
    return
  }
  console.log('1.打开蓝牙适配器')

  wx.openBluetoothAdapter({
    success: res => {
      console.log('2.初始化蓝牙模块')
      findExistList()
    },
    fail: (err) => {
      console.log('2.1 初始化蓝牙模块 - 失败')
      watchBluetoothStateChange()
      if (err.errCode == 10001) {
        wx.showToast({
          title: '蓝牙未开启',
          icon: 'none'
        })
      }
    }
  })
}

2.查找已存在的设备(tips:安卓多次连接会导致搜索不到,同时避免直接从手机蓝牙连接设备,如连接则先从手机蓝牙中删除设备)

function findExistList() {
  wx.getBluetoothDevices({
    success: res => {
      console.log(res, '进入查找事件')
      if (res && res.devices && res.devices.length > 0) {
        console.log('name', name)
        res.devices.forEach(device => {
          console.log('蓝牙名', device.name)
          if ((device.name && device.name.indexOf(name) > -1)) {
            console.log('5.2 查询已经搜索的列表,目标设备为:', device.name)
            deviceId = device.deviceId
            // 链接成功,需要断开蓝牙搜索
            stopSearchBluetooth()
            connectBluetooth()
          } else {
            console.log('查找失败', name)
            watchBluetoothStateChange()
            searchBluetooth()
          }
        })
      } else {
        console.log('查找失败', name)
        watchBluetoothStateChange()
        searchBluetooth()
      }
    },
    fail: err => {
      console.log('搜索蓝牙设备失败')
      watchBluetoothStateChange()
      searchBluetooth()
    }
  })
}

3.监听蓝牙适配器状态变化事件

function watchBluetoothStateChange() {
  wx.onBluetoothAdapterStateChange((result) => {
    console.log('3.获取蓝牙适配器状态改变', result)

    // 搜索状态
    if (discovering != result.discovering) {
      discovering = result.discovering
      // 移除蓝牙适配器状态变化的所有监听函数
      wx.offBluetoothAdapterStateChange()
      searchBluetooth()
    }

    // 蓝牙状态

    if (available != result.available) {
      available = result.available
      if (!result.available) {
        wx.showToast({
          title: '蓝牙未开启',
          icon: 'none'
        })
        console.log('init - 蓝牙适配器不可用')
      } else {
        if (!result.discovering && !devices.length) {
          wx.offBluetoothAdapterStateChange()
          searchBluetooth()
        }
      }
    }
  })
}

4.查找设备

function searchBluetooth() {
  wx.startBluetoothDevicesDiscovery({
    allowDuplicatesKey: false,
    success: res => {
      console.log('4.开始查找设备', res)
      watchBluetoothFound()
      // 30s停止搜索
      let timer = setTimeout(() => {
        stopSearchBluetooth()
        clearTimeout(timer)
        timer = null
      }, 30000)
    },
    fail: err => {
      console.log('4.1 查找设备失败- err', err)
    }
  })
}

5.监听寻找新设备

function watchBluetoothFound() {
  wx.onBluetoothDeviceFound((res) => {
    let device = res.devices[0]
    if (device.localName && device.localName.indexOf(name) > -1) {
      console.log('5. 搜索成功,目标设备为:', device.localName, res)
      deviceId = device.deviceId
      // 链接成功 断开蓝牙搜索
      stopSearchBluetooth()
      connectBluetooth()
    }
  })
}

6.停止查找

function stopSearchBluetooth() {
  wx.stopBluetoothDevicesDiscovery({
    success: res => {
      console.log('6.蓝牙停止查找', res)
    },
    fail: (err) => {
      console.log('6.1 蓝牙查找失败', err)
    }
  })
}

7.连接设备(此处为避免连接失败,反复执行连接,多次尝试)

let reconnectCounts = 0

function connectBluetooth() {
  reconnectCounts++
  wx.createBLEConnection({
    deviceId,
    success: (res) => {
      console.log('7.建立设备连接', res)
      reconnectCounts = null
      getBluetoothServers()
    },
    fail: err => {
      console.log('7.1 蓝牙连接失败', err)
      console.log('重新建立蓝牙通讯第:', reconnectCounts, '次')
      if (reconnectCounts <= 3) {
        connectBluetooth()
      } else {
        reject({
          deviceId
        })
        let tt = setTimeout(() => {
          clearTimeout(tt)
          tt = null
          showToastSimple('蓝牙异常')
        }, 200)
        closeFromAirtu({
          deviceId
        })
      }
    }
  })
}

8.获取设备服务

function getBluetoothServers() {
  wx.getBLEDeviceServices({
    deviceId,
    success: res => {
      if (res.services.length > 0) {
        res.services.forEach(item => {
          console.log('8.获取设备服务', item)
          serviceId = item.uuid
          services = res.services
          getBluetoothCharacteristics()
        })
      } else {
        console.log('8-1.获取设备服务', res)
        serviceId = res.services[0].uuid
        services = res.services
        getBluetoothCharacteristics()
      }
    },
    fail: err => {
      console.log('8.1 获取设备服务失败', err)
      reject({
        deviceId
      })
    }
  })
}

9.获取设备某个服务特征值列表

function getBluetoothCharacteristics() {
  wx.getBLEDeviceCharacteristics({
    deviceId: deviceId,
    serviceId,
    success(res) {
      console.log('9.获取设备服务特征值', res)
      res.characteristics.forEach(item => {
        if (item.properties && item.properties.write && item.properties.notify) {
          console.log('9-1.获取服务特征值', item)
          characteristicId = item.uuid
          notifyBluetoothCharacteristicValueChange(item.uuid)
            .then(() => {
              let timer = setTimeout(() => {
                console.log('================设备连接成功================')
                resolve({
                  deviceId,
                  serviceId,
                  characteristicId
                })
                clearTimeout(timer)
                timer = null
                // 字段重置
                devices = []
                available = false //设备可用
                discovering = false //搜索状态
                serviceId = '' //服务ID
                characteristicId = '' //特征值
                deviceId = '' // mac 地址 (ios的mac地址是UUID,由于隐私保护的原因引起的)
              }, 20)
              return
            })
            .catch(() => {
              console.error('启用通知失败,尝试下一个特征值或停止')
            })
         return;
        }
      })
    },
    fail: err => {
      console.log('9.1 获取设备服务特征值失败', err)
      reject({
        deviceId
      })
    }
  })

}

10.启用设备特征值变化时的notify功能(主要用来接收蓝牙设备返回的回调)

function notifyBluetoothCharacteristicValueChange(characteristicIds) {
  return new Promise((resolve, reject) => {
    wx.notifyBLECharacteristicValueChange({
      characteristicId: characteristicIds,
      deviceId,
      serviceId,
      state: true,
      success: () => {
        console.log('10.启用设备特征值变化提醒')
        resolve()
      },
      fail: err => {
        console.log('10.1启用设备特征值失败', err)
        reject(err)
      }
    })
  })
}

11.发送指令给蓝牙(这一部分需根据实际情况修改发送的数据类型)

export async function writeBLECharacteristicValue({
  deviceId,
  serviceId,
  characteristicId,
  value
}) {
  return new Promise((resolve, reject) => {

    let hex = value
    // 向蓝牙发送16进制的数据
    let typedArray = new Uint8Array(hex.match(/[\da-f]{2}/gi).map(function (h) {
      return parseInt(h, 16)
    }))
    let buffer1 = typedArray.buffer

    console.log('准备发送的指令:',value,typedArray,database.ab2hex(typedArray))
    wx.writeBLECharacteristicValue({
      characteristicId,
      deviceId,
      serviceId,
      value:buffer1,
      success(res) {
        console.log('写入成功', res, value)
        // 监听设备的特征值变化
        wx.onBLECharacteristicValueChange((result) => {
          // resolve(ab2hex(result.value))
          console.log('特征值变化',result,database.ab2hex(result.value))
        })
      },
      fail(err) {
        console.log('写入失败,结束', err)
        reject()
      }
    })
  })

}

12.断开蓝牙

export async function closeFromAirtu({
  deviceId
}) {
  return new Promise((resolve, reject) => {
    // wx.hideLoading()
    if (deviceId) {
      wx.closeBLEConnection({
        deviceId,
        success: () => {
          console.log('断开与低功耗蓝牙设备的连接')
          // 断开蓝牙的连接 (初始化所有的状态)
          wx.closeBluetoothAdapter({
            success: () => {
              console.log('关闭蓝牙模块')
              resolve()
            }
          })
        },
        fail: err => {
          console.log('断开与低功耗蓝牙设备的连接--err', err)
        }
      })
    } else {
      resolve()
    }
  })
}

一些数据处理方法

// 数据处理的公共方法
function inArray(arr, key, val) {
  for (let i = 0; i < arr.length; i++) {
      if (arr[i][key] === val) {
          return i;
      }
  }
  return -1;
}

function split_array(arr, len) {
  var a_len = arr.length;
  var result = [];
  for (var i = 0; i < a_len; i += len) {
      result.push(arr.slice(i, i + len));
  }
  return result;
}

function calcCrc(dataView) {
//计算指令的合取最低8位,是因为这个设备的指令是这个需求,实际按设备对接文档来
  let crc = 0;
  for (let i = 0; i < 15; i++) {
      crc += dataView.getUint8(i);
  }
  return crc
}
/**
*ArrayBuffer转16进制字符串
* @param {buffer} buffer 
*/
function ab2hex(buffer) {
  let hexArr = Array.prototype.map.call(
      new Uint8Array(buffer),
      function (bit) {
          return ('00' + bit.toString(16)).slice(-2)
      }
  )
  return hexArr.join('');
}
/**
* 16进制字符串转字节数组
* @param {string} str 
*/
function str2Bytes(str) {
  let pos = 0;
  let len = str.length;
  if (len % 2 != 0) {
      return null;
  }
  len /= 2;
  let hexA = new Array();
  for (let i = 0; i < len; i++) {
      let s = str.substr(pos, 2);
      let v = s; //处理
      hexA.push(v);
      pos += 2;
  }
  return hexA;
}
/**
* 反转
* @param {string} num 
*/
function reverse(num) {
  return num.split(',').reverse().join('');
}


module.exports = {
  inArray,
  split_array,
  calcCrc,
  ab2hex,
  str2Bytes,
  reverse
}

完整代码如下

/**
 * 蓝牙链接公共方法工具包
 */

/**
 * 蓝牙连接流程如下:
 *  1.打开蓝牙适配器
 *  2.查看蓝牙适配器状态
 *  3.根据传入的设备名称搜索附近设备列表并筛选
 *  4.根据列表找到对应项获取对应deviceId创建链接
 *  5.创建链接后获取该设备的详细信息
 *  6.根据不同指令写入对应的指令,同时根据接收的指令做出回馈
 * 微信蓝牙Api:
 * 打开蓝牙模块
 * wx.openBluetoothAdapter 
 * 关闭蓝牙模块
 * wx.closeBluetoothAdapter
 * 获取本机蓝牙适配器状态
 * wx.getBluetoothAdapterState 
 * 监听蓝牙适配器状态变化事件
 * wx.onBluetoothAdapterStateChange 
 * 
 */
import database from './database'
export async function initBLEFormSystem({
  name
}) {
  let devices = []
  let available = false // 设备可用
  let discovering = false // 搜索状态
  let serviceId = '' // 服务Id
  let services = [] //服务ID数组
  let characteristicId = '' // 特征值
  let deviceId = '' // mac 地址 (ios的mac地址是UUID,由于隐私保护的原因引起的)

  return new Promise((resolve, reject) => {
    initBluetooth()

    // 初始化蓝牙设备
    function initBluetooth() {
      // 版本过低不兼容
      console.log('wx.openBluetoothAdapter', wx.openBluetoothAdapter)
      if (!wx.openBluetoothAdapter) {
        wx.showModal({
          title: '提示',
          showCancel: false,
          content: '当前微信版本过低,无法使用该功能,请升级到最新微信版本后重试。',
        })
        return
      }
      console.log('1.打开蓝牙适配器')

      wx.openBluetoothAdapter({
        success: res => {
          console.log('2.初始化蓝牙模块')
          findExistList()
        },
        fail: (err) => {
          console.log('2.1 初始化蓝牙模块 - 失败')
          watchBluetoothStateChange()
          if (err.errCode == 10001) {
            wx.showToast({
              title: '蓝牙未开启',
              icon: 'none'
            })
          }
        }
      })
    }

    // 查找已存在的设备(安卓多次链接会导致搜索不到)
    function findExistList() {
      wx.getBluetoothDevices({
        success: res => {
          console.log(res, '进入查找事件')
          if (res && res.devices && res.devices.length > 0) {
            console.log('name', name)
            res.devices.forEach(device => {
              console.log('蓝牙名', device.name)
              if ((device.name && device.name.indexOf(name) > -1)) {
                console.log('5.2 查询已经搜索的列表,目标设备为:', device.name)
                deviceId = device.deviceId
                // 链接成功,需要断开蓝牙搜索
                stopSearchBluetooth()
                connectBluetooth()
              } else {
                console.log('查找失败', name)
                watchBluetoothStateChange()
                searchBluetooth()
              }
            })
          } else {
            console.log('查找失败', name)
            watchBluetoothStateChange()
            searchBluetooth()
          }
        },
        fail: err => {
          console.log('搜索蓝牙设备失败')
          watchBluetoothStateChange()
          searchBluetooth()
        }
      })
    }

    // 监听蓝牙适配器状态变化事件
    function watchBluetoothStateChange() {
      wx.onBluetoothAdapterStateChange((result) => {
        console.log('3.获取蓝牙适配器状态改变', result)

        // 搜索状态
        if (discovering != result.discovering) {
          discovering = result.discovering
          // 移除蓝牙适配器状态变化的所有监听函数
          wx.offBluetoothAdapterStateChange()
          searchBluetooth()
        }

        // 蓝牙状态

        if (available != result.available) {
          available = result.available
          if (!result.available) {
            wx.showToast({
              title: '蓝牙未开启',
              icon: 'none'
            })
            console.log('init - 蓝牙适配器不可用')
          } else {
            if (!result.discovering && !devices.length) {
              wx.offBluetoothAdapterStateChange()
              searchBluetooth()
            }
          }
        }
      })
    }

    // 查找设备
    function searchBluetooth() {
      wx.startBluetoothDevicesDiscovery({
        allowDuplicatesKey: false,
        success: res => {
          console.log('4.开始查找设备', res)
          watchBluetoothFound()
          // 30s停止搜索
          let timer = setTimeout(() => {
            stopSearchBluetooth()
            clearTimeout(timer)
            timer = null
          }, 30000)
        },
        fail: err => {
          console.log('4.1 查找设备失败- err', err)
        }
      })
    }

    // 监听寻找到新设备
    function watchBluetoothFound() {
      wx.onBluetoothDeviceFound((res) => {
        let device = res.devices[0]
        if (device.localName && device.localName.indexOf(name) > -1) {
          console.log('5. 搜索成功,目标设备为:', device.localName, res)
          deviceId = device.deviceId
          // 链接成功 断开蓝牙搜索
          stopSearchBluetooth()
          connectBluetooth()
        }
      })
    }

    // 停止查找
    function stopSearchBluetooth() {
      wx.stopBluetoothDevicesDiscovery({
        success: res => {
          console.log('6.蓝牙停止查找', res)
        },
        fail: (err) => {
          console.log('6.1 蓝牙查找失败', err)
        }
      })
    }


    // 连接设备
    // 避免蓝牙链接失败,反复执行连接,多次尝试
    let reconnectCounts = 0

    function connectBluetooth() {
      reconnectCounts++
      wx.createBLEConnection({
        deviceId,
        success: (res) => {
          console.log('7.建立设备连接', res)
          reconnectCounts = null
          getBluetoothServers()
        },
        fail: err => {
          console.log('7.1 蓝牙连接失败', err)
          console.log('重新建立蓝牙通讯第:', reconnectCounts, '次')
          if (reconnectCounts <= 3) {
            connectBluetooth()
          } else {
            reject({
              deviceId
            })
            let tt = setTimeout(() => {
              clearTimeout(tt)
              tt = null
              showToastSimple('蓝牙异常')
            }, 200)
            closeFromAirtu({
              deviceId
            })
          }
        }
      })
    }

    // 获取设备服务
    function getBluetoothServers() {
      wx.getBLEDeviceServices({
        deviceId,
        success: res => {
          if (res.services.length > 0) {
            res.services.forEach(item => {
              console.log('8.获取设备服务', item)
              serviceId = item.uuid
              services = res.services
              getBluetoothCharacteristics()
            })
          } else {
            console.log('8-1.获取设备服务', res)
            serviceId = res.services[0].uuid
            services = res.services
            getBluetoothCharacteristics()
          }
        },
        fail: err => {
          console.log('8.1 获取设备服务失败', err)
          reject({
            deviceId
          })
        }
      })
    }

    // 获取设备某个服务特征值列表
    function getBluetoothCharacteristics() {
      wx.getBLEDeviceCharacteristics({
        deviceId: deviceId,
        serviceId,
        success(res) {
          console.log('9.获取设备服务特征值', res)
          res.characteristics.forEach(item => {
            if (item.properties && item.properties.write && item.properties.notify) {
              console.log('9-1.获取服务特征值', item)
              characteristicId = item.uuid
              notifyBluetoothCharacteristicValueChange(item.uuid)
                .then(() => {
                  let timer = setTimeout(() => {
                    console.log('================设备连接成功================')
                    resolve({
                      deviceId,
                      serviceId,
                      characteristicId
                    })
                    clearTimeout(timer)
                    timer = null
                    // 字段重置
                    devices = []
                    available = false //设备可用
                    discovering = false //搜索状态
                    serviceId = '' //服务ID
                    characteristicId = '' //特征值
                    deviceId = '' // mac 地址 (ios的mac地址是UUID,由于隐私保护的原因引起的)
                  }, 20)
                  return
                })
                .catch(() => {
                  console.error('启用通知失败,尝试下一个特征值或停止')
                })
             return;
            }
          })
        },
        fail: err => {
          console.log('9.1 获取设备服务特征值失败', err)
          reject({
            deviceId
          })
        }
      })

    }

    // 启用设备特征值变化时的notify功能
    function notifyBluetoothCharacteristicValueChange(characteristicIds) {
      return new Promise((resolve, reject) => {
        wx.notifyBLECharacteristicValueChange({
          characteristicId: characteristicIds,
          deviceId,
          serviceId,
          state: true,
          success: () => {
            console.log('10.启用设备特征值变化提醒')
            resolve()
          },
          fail: err => {
            console.log('10.1启用设备特征值失败', err)
            reject(err)
          }
        })
      })
    }

  })
}

/**
 * 发送指令给蓝牙
 * @params value String 
 */
export async function writeBLECharacteristicValue({
  deviceId,
  serviceId,
  characteristicId,
  value
}) {
  return new Promise((resolve, reject) => {

    let hex = value
    // 向蓝牙发送16进制的数据
    let typedArray = new Uint8Array(hex.match(/[\da-f]{2}/gi).map(function (h) {
      return parseInt(h, 16)
    }))
    let buffer1 = typedArray.buffer

    console.log('准备发送的指令:',value,typedArray,database.ab2hex(typedArray))
    wx.writeBLECharacteristicValue({
      characteristicId,
      deviceId,
      serviceId,
      value:buffer1,
      success(res) {
        console.log('写入成功', res, value)
        // 监听设备的特征值变化
        wx.onBLECharacteristicValueChange((result) => {
          // resolve(ab2hex(result.value))
          console.log('特征值变化',result,database.ab2hex(result.value))
        })
      },
      fail(err) {
        console.log('写入失败,结束', err)
        reject()
      }
    })
  })

}



/**
 * 断开蓝牙
 * @params deviceId String
 */
export async function closeFromAirtu({
  deviceId
}) {
  return new Promise((resolve, reject) => {
    // wx.hideLoading()
    if (deviceId) {
      wx.closeBLEConnection({
        deviceId,
        success: () => {
          console.log('断开与低功耗蓝牙设备的连接')
          // 断开蓝牙的连接 (初始化所有的状态)
          wx.closeBluetoothAdapter({
            success: () => {
              console.log('关闭蓝牙模块')
              resolve()
            }
          })
        },
        fail: err => {
          console.log('断开与低功耗蓝牙设备的连接--err', err)
        }
      })
    } else {
      resolve()
    }
  })
}

function showToastSimple(error) {
  return new Promise(resolve => {
    wx.showToast({
      title: error,
      icon: 'none',
      duration: 2000,
      success(res) {
        resolve(res)
      }
    })
  })
}