apicloud的ble模块使用记录

5 阅读3分钟

背景

使用hybrid框架apicloud,2022年12月,开发物联网领域的蓝牙交互功能时,使用了apicloud的官方ble插件,本文记录了使用逻辑和一些注意点。

可用于第三方蓝牙设备交互,必须要支持蓝牙 4.0。 iOS上:硬件至少是 iphone4s,系统至少是 iOS6。 android上:系统版本至少是 android4.3。

蓝牙 4.0 以低功耗著称,一般也叫 BLE(BluetoothLowEnergy)。目前应用比较多的案例:运动手坏、嵌入式设备、智能家居

蓝牙通讯原理

概述

​ 在蓝牙通讯中有两个主要的部分,Central 和 Peripheral,有一点类似Client Server。 一般手机是客户端, 设备(比如手环)是服务器,因为是手机去连接手环这个服务器。以下称两个部分为手机和设备。

服务和特征

​ 设备可以广播数据、提供服务,手机可以扫描附近的设备,一旦建立连接,就可以交换数据。 ​ 特征是与外界交互的最小单位。蓝牙4.0设备通过服务(Service)、特征(Characteristics)和描述符(Descriptor)来形容自己,同一台设备可能包含一个或多个服务,每个服务下面又包含若干个特征,每个特征下面有包含若干个描述符(Descriptor)。比如某台蓝牙4.0设备,用特征A来描述设备信息、用特征B和描述符b来收发数据等。而每个服务、特征和描述符都是用 UUID 来区分和标识的。

封装

这次封装了蓝牙(我们公司的设备)使用的流程,并暴露出生命周期,供处理数据、显示页面等操作,小程序也是同样思路。以下是伪代码,完整代码见git(私有仓库)。 const $ble = { ... }

1、检测手机蓝牙状态,权限

// 位置, Android 6.0以后需要定位权限,否则无法正常使用

 async init() {
  if (isAndroid) {
    const { granted } = confirmPermission('location')
    if (!granted) {
      dispatchEvent('bleStatus', { type: 'location', status: false })
      return
    }
  }
}

​ // 安卓12及以上和鸿蒙(非next)需要打开‘附近设备’权限 ​

if (AndroidVersion >= 12) {
      const { granted } = confirmPermission('ble-scan')
      if (!granted) {
        dispatchEvent('bleStatus', { type: 'ble-scan', status: false })
        return
      }
    }

​ // 蓝牙

 initManager({ "single": true }).then(res => {
      if (res.state === "poweredOn") dispatchEvent('bleStatus', { status: true })
      else dispatchEvent('bleStatus', { type: 'ble', status: false })
    }).catch(() => {
      dispatchEvent('bleStatus', { type: 'ble', status: false })
    })
  }

2、监听生命周期的各个节点,和处理方法

addBleListener(type, handler) {
    console.log('监听到', type)
    if (!(type in $ble.data.handlers)) $ble.data.handlers[type] = []
    $ble.data.handlers[type].push(handler)
  },

3、发布事件两个参数(事件名,参数)

dispatchEvent(type, ...params) {
    console.log('触发了', type)
    if (!(type in $ble.data.handlers)) return
    $ble.data.handlers[type].forEach(handler => {
      handler(...params)
    })
  },

4、监听蓝牙是否一直连接

 listenConnected() {
    $ble.utils.clearTimer($ble.data.listenConnectTimer, 'interval')
    $ble.data.listenConnectTimer = setInterval(function () {
      $ble.getPrivacy().isConnected({
        peripheralUUID: $ble.data.uuid
      }, function (ret) {
        console.log('--------------listenconnected', ret.status)
        if (!ret.status) {
          $ble.utils.clearTimer($ble.data.listenConnectTimer, 'interval')
          $ble.data.lastStatus = $ble.data.checkStatus
          $ble.dispatchEvent('disconnected')
          $ble.reset()
        }
      })
    }, 2000)
  },

5、连接蓝牙

// 连接整体控制  
connectBle() {
    $ble.connect()
    $ble.utils.clearTimer($ble.data.connectTimer, 'interval')
    $ble.data.connectTimer = setInterval(() => {
      $ble.connect()
    }, 3500)
    $ble.utils.clearTimer($ble.data.connectOutTimer)
    $ble.data.connectOutTimer = setTimeout(() => {
      if (!$ble.data.isConnected) {
        $ble.data.isConnectOut = true
        $ble.bleCheckFail('连接超时', '14')
      }
    }, 30 * 1000)
  },
    
  // 调用连接
  connect() {
    $ble.data.connectTimes++
    if ($ble.data.isConnected) return
    console.log('第' + $ble.data.connectTimes + '次连接', $ble.data.uuid)
    $ble.getPrivacy().connect({
      peripheralUUID: $ble.data.uuid
    }, function (ret) {
      if (ret.status) {
        if ($ble.data.isConnectOut) {	// 超时之后连接成功
          return false
        }
        $ble.dispatchEvent('bleProcessNode', { name: 'connecting', status: 'success' })
        $ble.dispatchEvent('connectSuccess')
        $ble.data.isConnected = true
        $ble.utils.clearTimer($ble.data.connectOutTimer)
        $ble.listenConnected()
        // 连接成功后获取服务
        $ble.getService()
        $ble.utils.clearTimer($ble.data.connectTimer, 'interval')
        $ble.data.connectTimes = 0
      }
    })
    if ($ble.data.connectTimes > 4) {
      $ble.utils.clearTimer($ble.data.connectTimer, 'interval')
      $ble.data.connectTimes = 0
    }
  }

5、获取服务

 getService() {
    console.log('当前状态-------------->', 'serviceGetting')
    $ble.data.checkStatus = 'serviceGetting'
    $ble.utils.clearTimer($ble.data.getServiceTimer)
    $ble.data.getServiceTimer = setTimeout(() => {
      if (!$ble.data.notifyServiceId) {
        $ble.bleCheckFail('获取服务超时', '29')
      }
    }, 5 * 1000)
    $ble.getPrivacy().discoverService({
      peripheralUUID: $ble.data.uuid
    }, function (ret, err) {
      if (ret.status) {
        let service = ret["services"]
        $ble.data.notifyServiceId = $ble.data.writeServiceId = ''
        for (let i = 0; i < service.length; i++) {
          let UUID_slice = service[i].length > 4 ? service[i].slice(4, 8) : service[i]   //截取4到8位
          /* 判断是否是我们需要的服务*/
          if (UUID_slice.toUpperCase() == $ble.data.notify) $ble.data.notifyServiceId = service[i]
          if (UUID_slice.toUpperCase() == $ble.data.write) $ble.data.writeServiceId = service[i]
        }
        if ($ble.data.notifyServiceId) {
          $ble.dispatchEvent('getServiceSuccess')
          // 获取特征
          $ble.getNotifyChara()
        }
      } else {
        $ble.bleCheckFail('获取服务失败', '21')
      }
    })
  },

6、获取特征

getNotifyChara() {
    console.log('当前状态-------------->', 'charaGetting')
    $ble.data.checkStatus = 'charaGetting'
    $ble.utils.clearTimer($ble.data.getCharaTimer)
    $ble.data.getCharaTimer = setTimeout(() => {
      if (!$ble.data.notifyCharacterId) {
        $ble.bleCheckFail('获取特征超时')
      }
    }, 5 * 1000)
    $ble.getPrivacy().discoverCharacteristics({
      serviceUUID: $ble.data.notifyServiceId,
      peripheralUUID: $ble.data.uuid
    }, function (ret) {
      if (ret.status) {
        $ble.dispatchEvent('getNotifyCharaSuccess')
        let characteristic = ret["characteristics"]
        $ble.data.notifyCharacterId = characteristic[1] ? characteristic[1].uuid : ''
        $ble.data.writeCharacterId = characteristic[0] ? characteristic[0].uuid : ''
        if ($ble.data.notifyCharacterId && (!$ble.data.ywBleTab || $ble.data.ywBleTab==='reading')) {
          $ble.startRegister()
        }
      } else {
        $ble.bleCheckFail('获取特征失败', '31')
      }
    });
  },

7、注册、配对、数据交互, 都是获取数据包,发送数据包的过程

  startRegister() {},
  pair() {},
  read() {},

8、开启监听设备响应的数据包

 notify(ret) {
    if (ret.status) {
      var msg = ret["characteristic"]["value"]
      if (msg != $ble.data.not) {
        $ble.data.isNotified = true
        $ble.utils.clearTimer($ble.data.sendTimer)
        // 数据是否是我们想要的
        let isVaild = $ble.data.deviceType=='2' ? true : $ble.utils.vaildReading(msg)
        if (isVaild) {
          if ($ble.data.showLog) $ble.dispatchEvent('rspLog', { msg, status: true, isVaild: true })
          $ble.data.sendTimes = 0
          console.log('--------------监听回调--------------', $ble.data.checkStatus)
          // 非多k的直接上传
          ajax("bleUpload", { device: $ble.data.systemBleId, "bytes": msg }, null, function (res) {
            if($ble.data.checkStatus === "register" && res.type === 'readRsp') {
              // 注册-----------------------------------------------------------------
              handle()
            }else if($ble.data.checkStatus === "assign" && (res.type === 'randomRsp' || res.type === 'pairRsp')) {
              // 配对-----------------------------------------------------------------
              handle()
            }else if ($ble.data.checkStatus == "reading" && res.type === 'reading') {
              handle()
            }
          }, function (desc) {
            $ble.data.packList = []
            console.log('uploadble失败')
            const d = desc || '失败!'
            $ble.bleCheckFail(d, $ble.data.errCode[$ble.data.checkStatus])
          }, function (desc) {
            $ble.data.packList = []
            console.log('uploadble失败2')
            const d = desc || '失败!'
            $ble.bleCheckFail(d, $ble.data.errCode[$ble.data.checkStatus])
          }, 1)
        } else {
          if ($ble.data.showLog) $ble.dispatchEvent('rspLog', { msg, status: true, isVaild: false })
          $ble.reSend()
        }
      }
    } else {
      $ble.reSend()
    }
  },

9、发送数据包

  reSend() {
    $ble.data.isNotified = false
    $ble.data.sendTimes++
    console.log('sendTimes', $ble.data.sendTimes)
    if ($ble.data.sendTimes > 3) {
      $ble.utils.clearTimer($ble.data.sendTimer)
      $ble.bleCheckFail('失败')
      return
    }
    if (!$ble.data.isConnected) {
      $ble.utils.clearTimer($ble.data.sendTimer)
      $ble.bleCheckFail('失败')
      return
    }
    const timeout = (wait = 8000) => {
      $ble.utils.clearTimer($ble.data.sendTimer)
      $ble.data.sendTimer = setTimeout(() => {
        if (!$ble.data.isNotified) {
          // 表具未反馈指令!
          const isSwitch = $ble.data.sendTimes > 2 ? true : false
          $ble.dispatchEvent('rspLog', { msg: '', status: false, isSwitch: isSwitch, name: $ble.data.checkStatus })
          $ble.reSend()
        }
      }, wait)
    }
    if ($ble.data.checkStatus == "reading") {
      timeout()
      $ble.sendMsg($ble.data.readingPack)
    } else {
      timeout()
      $ble.sendMsg($ble.data.sendPack)
    }
  },

10、重写关闭窗口的页面,由于关闭窗口的时候无法监听,从而去关闭蓝牙设备的连接,所以重写api.closeWin,实现最大限度的控制蓝牙状态

 reWriteClose() {
    // 左滑返回处理
    api.addEventListener({
      name: 'keyback'
    }, function (ret, err) {
      cn++
      if (cn == 1)
        toast("再滑一次退出蓝牙")
      else if (cn == 2)
        api.closeWin()
    })
// 直接关闭重写, 关闭时将蓝牙状态重置
api.closeWin = () => {
  loading()
  await handleBle()
  close()
}

}

使用

$ble.init()

$ble.addBleListener('bleStatus', res => {

	res === 'success' && alert('蓝牙正常使用')

	res === 'fail' && alert('蓝牙权限未开启')

})

$ble.addBleListener('connected', res => {

	res === 'success' && alert('连接成功')
  res === 'fail' && alert('连接失败')

})

$ble.addBleListener('pair', res => {

	res === 'success' && alert('配对成功')
  res === 'fail' && alert('配对失败')
  ...

})