1

注意事项

最近在重构微信小程序项目,因为业务涉及蓝牙连接相关,就重新整理一遍蓝牙连接的封装方法。

需要注意到是:

  1. 蓝牙配适器开启与初始化会在对象实例化的构造函数中执行,如果需要单独控制初始化时间点(涉及蓝牙授权弹窗的初次出现时机),可以将initialize()方法从构造函数中移除并改为public方法,并在外部独立控制实例化的时间点;
  2. 连接蓝牙设备的方法,是在实例化后使用connect()方法传入蓝牙设备名,再在wx.onBluetoothDeviceFound中不断匹配,再用匹配的deviceId连接。这种方法会有诸多隐患,包括但不限于蓝牙设备重名、搜索方法功耗较大等,这部分可以按照自己的需求自行调整优化;
  3. 获取特征值、发送握手、校验位计算等方法各不相同,可以按照自己板子的需求调整内容与规则。

公共方法

// utils/bluetooth.ts
export default class Bluetooth {
  /** 是否初始化 */
  #initialized = false;
  /** 连接状态 */
  #connection = false;
  /** 设备ID */
  #deviceId = '';
  /** 服务ID */
  #serviceId = '';
  /** 特征值 */
  #characteristics = [] as WechatMiniprogram.BLECharacteristic[];

  constructor () {
    // 初始化蓝牙模块
    this.initialize()
  }

  /**
   * @private 开启蓝牙模块
   */
  private openBluetoothAdapter () {
    return new Promise((resolve, reject) => {
      wx.openBluetoothAdapter({
        success: () => {
          this.#initialized = true;
          resolve(true);
        },
        fail: () => {
          this.#initialized = false;
          reject(false);
        },
        complete: (res) => {
          console.log('[Bluetooth] 开启蓝牙模块', res.errMsg)
        }
      })
    })
  }

  /**
   *@private  获取蓝牙模块状态
   */
  private getBluetoothAdapterState () {
    return new Promise((resolve, reject) => {
      wx.getBluetoothAdapterState({
        success: () => {
          this.#initialized = true;
          resolve(true);
        },
        fail: () => {
          this.#initialized = false;
          reject(false);
        },
        complete: (res) => {
          console.log('[Bluetooth] 获取蓝牙模块状态', res.errMsg)
        }
      })
    })
  }

  /**
   * @private 搜索设备
   * @returns {Array} 设备列表
   */
  private getBluetoothDevicesList (deviceName: string): Promise<string> {
    console.log('[Bluetooth] 开始搜索蓝牙设备')
    return new Promise((resolve, reject) => {
      wx.startBluetoothDevicesDiscovery({
        services: [],
        success: () => {
          wx.onBluetoothDeviceFound((res) => {
            if (res.devices[0].localName === deviceName) {
              wx.stopBluetoothDevicesDiscovery()
              resolve(res.devices[0]['deviceId'])
            }
          })
        },
        fail: (err) => {
          console.log('[Bluetooth] 蓝牙设备搜索失败', err)
          reject()
        }
      })
    })
  }

  /**
   * @private 连接设备
   * @param deviceId
   * @returns 
   */
  private createBLEConnection (deviceId: string) {
    wx.showLoading({ title: '设备连接中' })
    return new Promise((resolve, reject) => {
      wx.createBLEConnection({
        deviceId,
        success: () => {
          console.log('[Bluetooth] 设备连接成功')
          resolve(true);
        },
        fail: (err) => {
          wx.showToast({ title: '设备连接失败', icon: 'none' })
          console.log('[Bluetooth] 设备连接失败', err.errMsg)
          reject(false);
        },
        complete: () => {
          wx.stopBluetoothDevicesDiscovery()
          wx.hideLoading()
        }
      })
    })
  }

  /**
   * @private 获取蓝牙设备所有服务
   * @param {string} deviceId 蓝牙设备 id
   */
  private getBLEDeviceServices (deviceId: string): Promise<string> {
    const _this = this
    return new Promise((resolve, reject) => {
      wx.getBLEDeviceServices({
        deviceId,
        success: (res) => {
          _this.#serviceId = res.services[1].uuid
          resolve(res.services[1].uuid)
        },
        fail: () => {
          reject(null)
        },
        complete: (res) => {
          console.log('[Bluetooth] 获取蓝牙设备的所有服务', res.errMsg)
        }
      })
    })
  }

  /**
   * @private 获取所有特征值
   * @param deviceId
   * @param serviceId
   * @returns 
   */
  private getBLEDeviceCharacteristics (deviceId: string, serviceId: string): Promise<WechatMiniprogram.BLECharacteristic[]> {
    return new Promise((resolve, reject) => {
      wx.getBLEDeviceCharacteristics({
        deviceId,
        serviceId,
        success: (res) => {
          resolve(res.characteristics)
        },
        fail: () => {
          reject(null)
        },
        complete: (res) => {
          console.log('[Bluetooth] 获取设备特征值', res.errMsg)
        }
      })
    })
  }

  /**
   * @private 启用蓝牙低功耗设备特征值变化时的 notify 功能
   * @param deviceId 蓝牙设备 id
   * @param serviceId 蓝牙服务 uuid
   * @returns 
   */
  private notifyBLECharacteristicValueChange (deviceId: string, serviceId: string): Promise<boolean> {
    const characteristicId = this.#characteristics[1]?.uuid || ''
    return new Promise((resolve, reject) => {
      wx.notifyBLECharacteristicValueChange({
        deviceId,
        serviceId,
        characteristicId,
        state: true,
        success: () => {
          resolve(true)
        },
        fail: () => {
          reject(false)
        },
        complete: (res) => {
          console.log('[Bluetooth] 订阅特征值变化特征', res.errMsg)
        }
      })
    })
  }

  /**
   * @private 计算校验位
   * @param arr 
   * @returns 
   */
  private handleCheckBit (arr: any[]) {
    // ...
    return arr
  }

  private hexStringToArrayBuffer (arr: Array<any>) {
    // ArrayBuffer 对象代表原始的二进制数据,不能直接读写,只能通过视图(TypeArray和DataView)
    // DataView视图 用来读写复杂类型的二进制数据
    let str = this.handleCheckBit(arr)
    let buffer = new ArrayBuffer(str.length / 2)
    let dataView = new DataView(buffer)
    let ind = 0
    for (var i = 0, len = str.length; i < len; i += 2) {
      let code = parseInt(str.substr(i, 2), 16)
      dataView.setUint8(ind, code)
      ind++
    }
    return buffer
  }

  private handshake () {
    return setTimeout(() => {
      this.sendBleData(...)
    }, 300)
  }

  
  /**
   * @private 字符串转为数组
   * @param str 
   * @param num 
   * @returns 
   */
  private handleSplitStr (str: string, num: number) {
    let arr = []
    for (let i = 0, len = str.length / num; i < len; i++) {
      let subStr = str.substr(0, num)
      arr.push(subStr)
      str = str.replace(subStr, '')
    }
    return arr
  }

  /**
   * @private ArrayBuffer解析出16进制
   * @param buffer 
   * @returns 
   */
  private ab2hex (buffer: ArrayBuffer) {
    let hexArr = Array.prototype.map.call(
      new Uint8Array(buffer),
      function (bit) {
        return ('00' + bit.toString(16)).slice(-2)
      }
    )
    return hexArr.join('')
  }

  /** 蓝牙初始化 */
  private async initialize() {
    if (this.#initialized) return;
    if (await this.openBluetoothAdapter() && await this.getBluetoothAdapterState()) {
      this.#initialized = true;
      console.log('[Bluetooth] 蓝牙初始化成功')
    } else {
      wx.showToast({ title: '蓝牙初始化失败', icon: 'none' })
    }
  }

  /** 连接蓝牙 */
  public async connect(deviceName: string) {
    // 0. 初始化蓝牙
    if (!this.#initialized) await this.initialize();
    // 1. 获取设备列表
    this.#deviceId = await this.getBluetoothDevicesList(deviceName)
    // 2. 连接设备
    if (!await this.createBLEConnection(this.#deviceId)) return
    // 3. 获取服务
    this.#serviceId = await this.getBLEDeviceServices(this.#deviceId)
    // 4. 获取特征值
    this.#characteristics = await this.getBLEDeviceCharacteristics(this.#deviceId, this.#serviceId)
    // 5. 监听特征值
    this.#connection = await this.notifyBLECharacteristicValueChange(this.#deviceId, this.#serviceId)
    // 6. 开始握手
    this.handshake()
  }
  
  /** 断开蓝牙连接 */
  public disconnect(): void {
    if (!this.#connection) return
    setTimeout(() => {
      wx.closeBLEConnection({
        deviceId: this.#deviceId,
        success: () => {
          this.#connection = false
          console.log('[Bluetooth] 蓝牙断开连接成功')
        },
        fail: (err) => {
          console.log('[Bluetooth] 蓝牙断开连接失败', err)
        }
      })
    }, 300);
  }

  /**
   * 发送蓝牙数据
   * @param msg 
   * @returns 
   */
  public sendBleData (msg: any[]) {
    const deviceId = this.#deviceId
    const serviceId = this.#serviceId
    const characteristicId = this.#characteristics[0].uuid || ''
    const value = this.hexStringToArrayBuffer(msg)
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        wx.writeBLECharacteristicValue({
          deviceId,
          serviceId,
          characteristicId,
          value,
          success: () => {
            resolve(true)
          },
          fail: (err) => {
            reject(err)
          },
          complete: (res) => {
            console.log('[Bluetooth] 发送蓝牙消息', msg, res)
          }
        })
      }, 300);
    })
  }

  /**
   * 接收蓝牙数据
   * @param {function} callback 回调函数
   */
  public receiveBleData (callback?: Function) {
    wx.onBLECharacteristicValueChange(res => {
      let data = this.handleSplitStr(this.ab2hex(res.value).toUpperCase(), 2)
      callback && callback(data)
      console.log('[Bluetooth] 接收蓝牙消息', data)
    })
  }

  /** 关闭蓝牙 */
  public async close () {
    if (!this.#initialized) return
    if (await this.getBluetoothAdapterState()) {
      wx.closeBluetoothAdapter({
        success: () => {
          console.log('[Bluetooth] 蓝牙配适器已关闭')
        }
      })
    }
  }
}

使用方法

// index.ts
import Bluetooth from '/utils/bluetooth'
const bluetooth = new Bluetooth()

// ...

// 连接蓝牙
await bluetooth.connect(deviceName)

// 发送消息
bluetooth.sendBleData([...])

// 接收消息
bluetooth.receiveBleData(msg => {
  // ...
})

// 断开连接
bluetooth.disconnect()

// 关闭蓝牙
bluetooth.close()

Andarinz
15 声望4 粉丝