【最全】微信支付宝小程序蓝牙API开锁全流程

【最全】微信支付宝小程序蓝牙API开锁全流程

注意:微信小程序蓝牙API与支付宝小程序蓝牙API有略微不同之处,注意闭坑,本文所讲的是自己开发低功耗蓝牙开锁的功能和全流程。所用技术为Taro,具体用法可以去查Taro官网(Taro:https://taro-docs.jd.com/taro/docs/README)。 有写的不好的或者意见和建议,请在评论区留言 。

蓝牙低功耗 (Bluetooth Low Energy, BLE)

蓝牙低功耗是从蓝牙 4.0 起支持的协议,与经典蓝牙相比,功耗极低、传输速度更快,但传输数据量较小。常用在对续航要求较高且只需小数据量传输的各种智能电子产品中,比如智能穿戴设备、智能家电、传感器等,应用场景广泛。

支付宝蓝牙与微信小程序蓝牙区别:

  1. 支付宝部分api名字与微信小程序蓝牙api名字有不一样的,具体见下文
  2. 微信小程序搜索蓝牙设备api,结果返回advertisData和serivceData都是ArrayBuffer类型的,需要自己将ArrayBuffer转为16进制,最后在进行解析数据等操作,而支付宝小程序搜索蓝牙api返回的直接是16进制,不需要再次转换,但是需要把16进制转换为能识别的16进制字符串数组才可以进行接下来的数据解析等操作

支付宝低功耗蓝牙API概览:

【最全】微信支付宝小程序蓝牙API开锁全流程_第1张图片

支付宝传统蓝牙API概览:

【最全】微信支付宝小程序蓝牙API开锁全流程_第2张图片

微信低功耗蓝牙API概览:

【最全】微信支付宝小程序蓝牙API开锁全流程_第3张图片


【重点:如何使用API以及细节】:

1.【第一步】:需要调用Taro.openBluetoothAdapter(),判断蓝牙是否开启(注意事项⚠️:Taro.openBluetoothAdapter支付宝和微信小程序返回的结果不一样,注意区分判断)

 if (Taro.getEnv() === "ALIPAY") {
      res = await Taro.openBluetoothAdapter();
 } else {
      const { errMsg } = await Taro.openBluetoothAdapter();
      res = errMsg;
 }

  //微信小程序蓝牙未打开
 if (Taro.getEnv() === "WEAPP" && res !== "openBluetoothAdapter:ok") {
    // TODO 没有打开蓝牙,需要提示用户打开 
 }
  //支付宝小程序  error: 12, errorMessage: "蓝牙未打开"
 if (Taro.getEnv() === "ALIPAY" && res.error === 12) {
     // TODO 没有打开蓝牙,需要提示用户打开 
 }

2.**【第二步】**开启蓝牙后,需要调用Taro.getLocation()开启地理位置,方便搜索到蓝牙,因android手机不兼容,所以必须要调用定位的方法,以更准确的查到附近的蓝牙设备

 //获取当前的地理位置、速度。当用户离开小程序后,此接口无法调用
 Taro.getLocation({})
        .then(res => {
         //	TODO 通过后端接口获取你需要的设备信息,一般需要uuid,localKey 等等
 })
        .catch(err => {
          console.log("err", err);
 });

3.**【第三步】**通过后端接口获取你需要的设备信息,拿到对应的uuid,localKey,其中,uuid是为了与搜索到的众多的蓝牙设备相匹配

 //TODO 调用接口拿到自己设备的信息
 const { uuid, localKey } = await getBleLocksInfoData(deviceIds);
 const loginKey = localKey.substr(0, 6);
 Taro.setStorageSync("loginKey", loginKey);
 
 //TODO 根据接口返回的uuid进行查找附近的设备并进行匹配
 searchBle(uuid);

4.**【第四步】**开始搜寻附近的蓝牙外围设备。此操作比较耗费系统资源,请在搜索并连接到设备后调用 wx.stopBluetoothDevicesDiscovery 方法停止搜索。 另外,services:要搜索的蓝牙设备主 service 的 uuid 列表。某些蓝牙设备会广播自己的主 service 的 uuid。如果设置此参数,则只搜索广播包有对应 uuid 的主服务的蓝牙设备。建议主要通过该参数过滤掉周边不需要处理的其他蓝牙设备。

参数介绍:
【最全】微信支付宝小程序蓝牙API开锁全流程_第4张图片

//因为我们的services是["A201"],所以这里直接参数写为["A201"]来查询
const data = await Taro.startBluetoothDevicesDiscovery({
      services: ["A201"]
});

//此处,微信小程序和支付宝小程序api返回的data结果不一样,请注意区分
if (data.isDiscovering || data) {

 //TODO:搜索蓝牙设备,并匹配UUID
 onDiscoveryBLE(uuid)
 
 //另注意:可以多次调用搜索蓝牙,但一定要及时关闭定时器
 //多次调用搜索蓝牙
 this.delayTimer = setInterval(() => {
   this.onDiscoveryBLE(uuid);
 }, 1000);
 
 //关闭定时器
 this.setTimer = setTimeout(() => {
        // 关闭蓝牙
        Taro.stopBluetoothDevicesDiscovery();
        // 清理定时
        clearInterval(this.delayTimer);
  }, 15000);
}

5.**【第五步】**调用Taro.getBluetoothDevices获取附近蓝牙设备,并进行筛选匹配过滤(要注意微信和支付宝api返回的结果是不一样的,微信需要转为十六进制,支付宝需要转换为十六进制字符串数组)

 // 搜索蓝牙 -- 微信 支付宝搜索蓝牙设备并连接
  onDiscoveryBLE = async uuid => {
    //获取在蓝牙模块生效期间所有已发现的蓝牙设备。包括已经和本机处于连接状态的设备。
    let isALiPay = Taro.getEnv() === "ALIPAY";
    let { devices } = await Taro.getBluetoothDevices();
    let macAddress = "";
    try {
      // 过滤一些设备
      devices = devices.filter(item => item.name && item.name !== "未知设备");
      //这里打印出设备列表,方便查看设备信息
      console.log("devices", devices);
      // 匹配 uuid 找到对应的设备
      if (devices.length) {
        for (const item of devices) {
          let {
            advertisServiceUUIDs,
            serviceData,
            advertisData,
            deviceId
          } = item;
          if (!advertisServiceUUIDs || !serviceData) return;
          if (isALiPay) { //支付宝
          	//TODO hexStringToArray十六进制字符串转十六进制字符串数组(方法见下文)
            serviceData = utils.hexStringToArray(serviceData[advertisServiceUUIDs]);
            advertisData = utils.hexStringToArray(advertisData);
          } else { //微信 
          	//TODO ab2hex ArrayBuffer转十六进制(方法见下文)
            serviceData = utils.ab2hex(serviceData[advertisServiceUUIDs]);
            advertisData = utils.ab2hex(advertisData);
          }
          //TODO 这里需要根据advertisData, serviceData这两个参数解密出uuid,然后与后端接口返回的uuid匹配,
          //为ture就表示是自己的设备
          const uuids = utils.getUuid(advertisData, serviceData);
          if (uuids === uuid) {
            console.log("数据有了:", item);
            macAddress = deviceId;
            Taro.setStorageSync("macAddress", macAddress);
            break;
          }
        }
        if (macAddress) {
        	//TODO 找到自己的设备后,开始建立建立连接(【第六步】)
          connectBle();
        }
      }
    } catch (error) {
    	//TODO 处理失败的逻辑 可给予弹窗提示
    }
  };

支付宝返回结果:
【最全】微信支付宝小程序蓝牙API开锁全流程_第5张图片
微信返回结果:
【最全】微信支付宝小程序蓝牙API开锁全流程_第6张图片

6.**【第六步】**在connectBle()这个方法内处理建立连接逻辑,拿到macAddress后,调用Taro.createBLEConnection()进行连接

//TODO 要注意支付宝和微信的区别
const deviceId = macAddress;
 
 if (Taro.getEnv() === "ALIPAY") {
        BLEConnection = my.connectBLEDevice
 }else{
        BLEConnection = Taro.createBLEConnection
 }
 
 BLEConnection({ deviceId })
        .then(connect => {
          getBLEDeviceServices(connect,deviceId,resolve)
        })
        .catch(err => {
          console.log("connect error", err);
          //TODO 可以处理连接失败的操作,具体可以重连等
          //思路:先关闭连接,然后再重新初始化蓝牙
          Taro.closeBLEConnection({ deviceId })
                .then(res => {
                  console.log("res", res);
                  initBleConnect();
           })
                .catch(error => {
                  console.log("error", error);
                  initBleConnect();
          });
        });
        
 const initBleConnect = () => {
      Taro.closeBluetoothAdapter()
        .then(ddd => {
          console.log("ddd", ddd);
          // 一秒钟之后再去开启蓝牙
          setTimeout(() => {
          //初始化蓝牙
            Taro.openBluetoothAdapter()
              .then(res => {
                console.log("蓝牙重启", res);
                const { errMsg } = res;
                if (errMsg === "openBluetoothAdapter:ok" || res.error !== 12) {
                  console.log("蓝牙重启 -----===========");
                  connectBluttoth(macAddress)
                    .then(connect => resolve(connect))
                    .catch(err => reject(err));
                } else {
                  reject({ code: 1 });
                }
              })
              .catch(err => {
                reject({ code: 1 });
                console.log("重启失败err", err);
              });
          }, 500);
        })
        .catch(rr => {
          console.log("rr", rr);
        });
    };

7.**【第七步】**在getBLEDeviceServices()处理获取蓝牙低功耗设备所有服务 (service),另外,调用notifyBLECharacteristicValueChange必须先启用notifyBLECharacteristicValueChange 才能监听到设备 characteristicValueChange 事件,所以在页面加载完成后我们需要加载此方法

const getBLEDeviceServices =(connect,deviceId,resolve)=>{
	//因为支付宝返回的{},所以在此做了兼容
  if (Number(connect.errCode) === 0 || JSON.stringify(connect) === '{}') {
  	//处理获取蓝牙低功耗设备所有服务 (service)
    Taro.getBLEDeviceServices({ deviceId }).then(({ services }) => {
      console.log(services);
      if (!services.length) {
        console.log("未找到服务列表");
        return;
      }
      if (services.length) {
        const service = services[0];
        // 获取蓝牙低功耗设备某个服务中所有特征 (characteristic)。
        Taro.getBLEDeviceCharacteristics({
          deviceId,
          serviceId: service.uuid || service.serviceId
        }).then(({ characteristics }) => {
          // 获取设备数据
          if (!characteristics.length) {
            console.log("未找到设备特征值");
            return;
          }
          characteristics.forEach(item => {
            if (item.properties.notify) {
   //启用蓝牙低功耗设备特征值变化时的 notify 功能,订阅特征。注意:必须设备的特征支持 notify 或者 indicate 才可以成功调用。
   //另外,必须先启用 wx.notifyBLECharacteristicValueChange 才能监听到设备 characteristicValueChange 事件
              Taro.notifyBLECharacteristicValueChange({
                deviceId,
                serviceId: service.uuid || service.serviceId,
                characteristicId: item.uuid || item.characteristicId,
                state: true
              });
            } else if (item.properties.write) {
              clearTimeout(connectTimer);
              resolve({
                characteristicIdUuid: item.uuid || item.characteristicId,
                serviceUuid: service.uuid || service.serviceId
              });
            } else {
              console.log("该特征值属性: 其他");
            }
          });
        });
      }
    });
  }
}
 import aesjs from "aes-js"; //需要引入aes-js
 
 async componentDidMount() {
    global["events"].on("onPairResult", this.onPairResult, this);
    global["events"].on("openLockClick", this.openLockClick, this);
    global["events"].emit("initLockState", 1);
    //监听低功耗蓝牙设备的特征值变化事件。
    //必须先启用 notifyBLECharacteristicValueChange 接口才能接收到设备推送的 notification。
    Taro.onBLECharacteristicValueChange(res => {
      // 在开锁中才接受数据
      if (this.isOpenLock) {
        const { value } = res;
        let key = ''
        if (Taro.getEnv() === "ALIPAY") {
          key = utils.HexString2Bytes(value)
        } else {
          const str = utils.ab2hex(value).join("");
          key = utils.HexString2Bytes(str);
        }
        console.log('parseDataReceived -- key:' + key);
        //开锁的逻辑,需要解析数据 -- 根据自己的业务和需求进行解析
        parseDataReceived.call(this, key);
      }
    });
    
     //监听低功耗蓝牙连接状态的改变事件。包括开发者主动连接或断开连接,设备丢失,连接异常断开等等
    if (Taro.getEnv() === "ALIPAY") {
      my.onBLEConnectionStateChanged(res => {
       		// TODO 可以处理失败的弹窗提示等
      });
    } else {
      Taro.onBLEConnectionStateChange(res => {
        // 该方法回调中可以用于处理连接意外断开等异常情况
        // TODO 可以处理失败的弹窗提示等
      });
    }
  } 

8.【工具类】 工具类

/**
 * 字符串转 16 进制
 * @param str 字符串
 */
function HexString2Bytes(str) {
  var pos = 0;
  var len = str.length;
  if (len % 2 != 0) {
    return null;
  }
  len /= 2;
  var arrBytes = new Array();
  for (var i = 0; i < len; i++) {
    var s = str.substr(pos, 2);
    var v = intToByte(parseInt(s, 16));
    arrBytes.push(v);
    pos += 2;
  }
  return arrBytes;
}


//十六进制字符串转换为数组
function hexStringToArray(str) {
  var pos = 0;
  var len = str.length;
  if (len % 2 != 0) {
    return null;
  }
  len /= 2;
  var arrBytes = new Array();
  for (var i = 0; i < len; i++) {
    var s = str.substr(pos, 2);
    arrBytes.push(s);
    pos += 2;
  }
  return arrBytes;
}


/**
 * 把 ArrayBuffer 转换成 16 进制内容
 * @param buffer 二进制内容
 */
const ab2hex = buffer => {
  const arr = [] as any;
  Array.prototype.map.call(new Uint8Array(buffer), function(bit) {
    let item = ("00" + bit.toString(16)).slice(-2);
    arr.push(item);
  });
  return arr;
};

//通过advertisData, serviceData这两个参数获取uuid,与自己设备的uuid相匹配
const getUuid = (advertisData, serviceData) => {
  serviceData = serviceData.slice(1, serviceData.length);
  const pid = serviceData.join("");
  const strByte = aesjs.utils.hex.toBytes(pid);
  const md5Val = md5.array(strByte);

  const uuidDataArr = advertisData.slice(8, advertisData.length);
  const uuidDataStr = uuidDataArr.join("");
  const encryptUuid = aesjs.utils.hex.toBytes(uuidDataStr);
  const aesCbc = new aesjs.ModeOfOperation.cbc(md5Val, md5Val);
  const decryptedBytes = aesCbc.decrypt(encryptUuid);
  const uuid = aesjs.utils.utf8.fromBytes(decryptedBytes);
  return uuid;
};

你可能感兴趣的:(小程序,微信小程序,小程序)