微信小程序 - 蓝牙BLE小程序开发

1.前言

         最近领导看我比较闲,安排我开发一个蓝牙BLE微信小程序,刚开始接到这个项目时,我第一反应时,"卧槽“”。老子在公司的岗位是做Windows和Android 软件开发的,看我闲,竟然让我去做小程序,我从来没有接触过。后面领导说给你一个星期,看不看能不能完成,实在没有办法,只能硬着头皮去学习小程序。

2.BLE蓝牙相关知识

2.1 经典蓝牙和蓝牙BLE的区别

蓝牙1.0~3.0都是经典蓝牙,有些人一直认为蓝牙4.0就是蓝牙BLE,是错误的。因为4.0是双模的,既包括经典蓝牙又包括低能耗蓝牙。蓝牙BLE相比于经典蓝牙的优点是搜索、连接的速度更快,关键就是BLE(Bluetooth Low Energy)低能耗,缺点呢就是传输的速度慢,传输的数据量也很小,每次只有20个字节。但是蓝牙BLE低能耗,在智能穿戴设备(手环和各种智能硬件)应用越来越广泛。

2.2 相关概念 (参考 通用属性配置文件(GATT)及其服务,特性与属性介绍 )和 (BLE4.0教程二 蓝牙协议之服务与特征值分析

2.2.1 特性

       一个特性至少包含2个属性:一个属性用于声明,一个属性用于存放特性的值。

所有通过GATT服务传输的数据必须映射成一系列的特性,可以把特性中的这些数据看成是一个个捆绑起来的数据,每个特性就是一个自我包容而独立的数据点。 例如,如果几块数据总是一起变化,那么我们可以把它们集中在一个特性里。

2.2.2 服务

       一个服务包含一个或多个特性,这些特性是逻辑上相关的集合体。

GATT服务一般包含几块具有相关的功能,比如特定传感器的读取和设置,人机接口的输入输出。组织具有相关的特性到服务中既实用又有效,因为它使得逻辑上和用户数据上的边界变得更加清晰,同时它也有助于不同应用程序间代码的重用。GATT基于蓝牙技术联盟(SIG)官方而设计,SIG建议根据它们的规范设计自己的profile。

2.2.3  UUID

 “GATT层”中定义的所有属性都有一个UUID值,UUID是全球唯一的128位的号码,它用来识别不同的特性。

2.3.4 Service和Characteristic  (参考 BLE4.0教程二 蓝牙协议之服务与特征值分析

Service是服务,Characteristic是特征值。蓝牙里面有多个Service,一个Service里面又包括多个Characteristic。

蓝牙4.0是以参数来进行数据传输的,即服务端定好一个参数,客户端可以对这个参数进行读,写,通知等操作,这个东西我们称之为特征值(characteristic,但一个参数不够我们用,比如我们这个特征值是电量的值,另一个特征值是设备读取的温度值。那这时候会有多个特征值,并且我们还会对它们分类,分出来的类我们称之为服务(service)。

一个设备可以有多个服务,每一个服务可以包含多个特征值。为了方便操作,每个特征值都有他的属性,例如长度(size),权限(permission),值(value),描述(descriptor)。

在蓝牙协议里每个蓝牙设备都有多个Service和Characteristic ,同一个Service 有多个Characteristic 。

用什么来区分?就是UUID。每个Service或者Characteristic都有一个 128 bit 的UUID来标识。

3. 微信小程序 - 蓝牙BLE通讯

3.1 需要注意的地方

3.2 使用的API

微信小程序目前有蓝牙 API 共 18 个,其中操作蓝牙适配器的共有 4 个,分别是

wx.openBluetoothAdapter 初始化蓝牙适配器
wx.closeBluetoothAdapter 关闭蓝牙模块
wx.getBluetoothAdapterState 获取本机蓝牙适配器状态
wx.onBluetoothAdapterStateChange 监听蓝牙适配器状态变化事件
其中,扫描和获取周围BLE设备的有4个。

wx.startBluetoothDevicesDiscovery 开始搜寻附近的蓝牙外围设备
wx.stopBluetoothDevicesDiscovery 停止搜寻附近的蓝牙外围设备
wx.getBluetoothDevices 获取所有已发现的蓝牙设备
wx.onBluetoothDeviceFound 监听寻找到新设备的事件
连接BLE设备的2个:

wx.createBLEConnection 连接低功耗蓝牙设备
wx.closeBLEConnection 断开与低功耗蓝牙设备的连接
连接成功后,读写BLE对应特征对象的数据:

wx.getConnectedBluetoothDevices 根据 uuid 获取处于已连接状态的设备
wx.getBLEDeviceServices 获取蓝牙设备所有 service(服务)
wx.getBLEDeviceCharacteristics  获取蓝牙设备所有 characteristic(特征值)
wx.readBLECharacteristicValue  读取低功耗蓝牙设备的特征值的二进制数据值
wx.writeBLECharacteristicValue 向低功耗蓝牙设备特征值中写入二进制数据
wx.notifyBLECharacteristicValueChange  启用低功耗蓝牙设备特征值变化时的 notify 功能
wx.onBLECharacteristicValueChange 监听低功耗蓝牙设备的特征值变化
wx.onBLEConnectionStateChange 监听低功耗蓝牙连接的错误事件
 

3.3 API 操作流程

微信小程序蓝牙API 操作流程,与Android 类似,但是相比于Android 却简化了很多。

3.3.1.首先是要初始化蓝牙

// 蓝牙相关API
  /**
   * 1. 打开蓝牙适配器
   */
function openBle(callback) {
  wx.openBluetoothAdapter({
    success: function (res) {
      writeLogcat('初始化蓝牙适配器成功' + JSON.stringify(res));
      callback(0, null);
    },

    fail: function (res) {
      writeLogcat('初始化蓝牙适配器失败, 失败原因: ' + JSON.stringify(res));
      callback(1, null);
    }
  })
}

3.3.2. 开启蓝牙搜索,并获取蓝牙设备列表 wx.getBluetoothAdapterState --> wx.onBluetoothAdapterStateChange --> wx.startBluetoothDevicesDiscovery --> wx.onBluetoothDeviceFound --> getBluetoothDevices

/**
 * 3. 扫描蓝牙设备
 */
function scanBle(callback) {
  //开始搜寻附近的蓝牙外围设备
  wx.startBluetoothDevicesDiscovery({
    success: function (res) {
      writeLogcat("成功打开,开始搜寻附近的蓝牙外围设备 ..." + JSON.stringify(res));

      wx.getBluetoothDevices({
        success: function (res) {
          writeLogcat("发现外围蓝牙设备, 设备信息 =" + JSON.stringify(res));
          callback(0, res);
        },

        fail: function (res) {
          writeLogcat("发送外围蓝牙设备失败, 失败原因 =" + JSON.stringify(res));
          callback(1, null);
        }
      })
    },

    fail: function (res) {
      writeLogcat("扫描失败蓝牙设备 ..." + JSON.stringify(res));
      callback(1, null);
    }
  })
}


3.3.3. 选择蓝牙设备获取相应的deviceId(对于需要通讯的蓝牙设备和设备的服务UUID 和特征UUID 需要事先知道,到底与那个BLE设备通讯,不然就算蓝牙设备搜索出来,也不知道)
3.3.4. 连接蓝牙设备 wx.createBLEConnection --> wx.getBLEDeviceService(获取设备的ServiceId)

注意:使用wx.notifyBLECharacteristicValueChange(Object object)  对于接收BLE蓝牙设备数据的方式一般的特征有两种(notify 和 indicate ),网上大部分都是使用notify特征的,很少人使用Indicate, 所以需要根据具体使用哪种,从而注册相应特征的通知回调函数,微信的API 也有提到

/**
 * 4. 连接蓝牙设备  -- 适应  IOS 和 Android
 * 
 * devices 成员
 * deviceId   设备MAC地址  
 * serviceUUIdD  设备服务UUID
 * writeCharacteristicsUUID  写特征UUID
 * readCharacteristicsUUID   读特征UUID
 * indicateCharacteristicsUUID  通知特征UUID
 */
function connectBle(devices, callback) {
  //1. 从devices 中获取蓝牙设备mac 地址
  var deviceId = devices.deviceId;
  var serviceUUID = null;

  connectedDeviceId = deviceId;
  writeLogcat("目标蓝牙设备mac地址 = " + deviceId);

  wx.createBLEConnection({
    deviceId: deviceId,
    success: function (res){
      writeLogcat('蓝牙设备连接成功');

      wx.getBLEDeviceServices({
        deviceId: deviceId,
        success: function (res) {
          var deviceService = res.services;
          writeLogcat('获取蓝牙设备Service信息 = ' + JSON.stringify(res));

          //连接设备成功,关闭蓝牙发现
          wx.stopBluetoothDevicesDiscovery();

          //轮询设备服务UUID           
          var isMatch = false;
          for (var ik = 0; ik < deviceService.length; ik++) {
            serviceUUID = deviceService[ik].uuid;

            writeLogcat("目标蓝牙设备 serviceUUID =" + serviceUUID);

            //已经匹配上ServiceUUID, 获取相关特征UUIDS
            if (serviceUUID != devices.serviceUUIdD)
              continue;
            else {
              isMatch = true;
              break;
            }
          }

          if (isMatch == false) 
          {
            writeLogcat("未找到目标服务 ...");

            callback(1, "未找到目标服务 ...");
            return;
          }

          writeLogcat("匹配服务 serviceUUID =" + serviceUUID);

          //获取蓝牙服务成功ID成功,获取特征值
          wx.getBLEDeviceCharacteristics({
            deviceId: deviceId,
            serviceId: serviceUUID,

            //获取特征值成功
            success: function (res) {
              writeLogcat("蓝牙设备特征值信息 = " + JSON.stringify(res));

              //遍历特征值,找到指定的通知特征,读和写特征值
              for (var ik = 0; ik < res.characteristics.length; ik++) {
                var characteristicsUUID = res.characteristics[ik].uuid;

                writeLogcat("res.characteristics[" + ik + "] uuid = " + characteristicsUUID);
                writeLogcat("res.characteristics[" + ik + "] properties = " + JSON.stringify(res.characteristics[ik].properties));

                //保存特征值,如果是通知特征,则需要开启通知服务
                if (res.characteristics[ik].properties.read == true)
                {
                  globalReadCharacteristicsUUID = characteristicsUUID;
                  writeLogcat("读特征UUID = " + characteristicsUUID);
                }

                if (res.characteristics[ik].properties.write == true)
                {
                  globalWriteCharacteristicsUUID = characteristicsUUID;
                  writeLogcat("写特征UUID = " + characteristicsUUID);
                }

                //启用低功耗蓝牙设备特征值变化时的 notify 功能
                if (res.characteristics[ik].properties.indicate == true) {
                  
                  //保存indicate 特征uuid
                  globalIndicateCharacteristicsUUID = characteristicsUUID;
                  writeLogcat("指示特征UUID = " + characteristicsUUID);

                  wx.notifyBLECharacteristicValueChange({
                    deviceId: deviceId,
                    serviceId: serviceUUID,
                    characteristicId: characteristicsUUID,
                    state: true,           //开启通知功能

                    success: function (res) {
                      writeLogcat("启用低功耗蓝牙设备特征值变化时的 notify 功能成功," + JSON.stringify(res));

                      //注册通知特征,回调函数
                      wx.onBLECharacteristicValueChange(receiveBleData);
                      callback(0, res);
                    },

                    fail: function (res) {
                      writeLogcat("启用低功耗蓝牙设备特征值变化时的功能失败,失败原因 = " + JSON.stringify(res));
                      callback(1, res);
                    },
                  });
                }
              }
            },

            fail: function (res) {
              writeLogcat('获取设备特征值失败, 失败原因 =' + JSON.stringify(res));
              callback(1, res);
            },
          });

        },

        fail: function (res) {
          writeLogcat('获取设备服务失败,失败原因 = ' + JSON.stringify(res));
          callback(1, res);
        }

      });
    },

    fail: function (res) {
      writeLogcat('蓝牙设备连接失败,请稍后重试,失败原因 = ' + JSON.stringify(res));
      callback(1, res);
    }
  });
}

3.3.5 关闭蓝牙连接

/**
 * 5. 断开与蓝牙设备连接
 */
function disconnectBle(callback) {
  wx.closeBLEConnection({
    deviceId:connectedDeviceId,
    success: function (res){
      writeLogcat('断开蓝牙设备成功:' + JSON.stringify(res));
      callback(0, res);
    },

    fail: function (res) {
      writeLogcat('断开蓝牙设备失败:' + JSON.stringify(res));
      callback(1, res);
    }
  })
}


3.3.6 ble 发送数据

/**
 * 发送20个字节
 */
function writeBle(devices, cmd, onSuccessCallback, onFailCallback)
{
  writeLogcat("发送第" + globalIndex + "包 = " + cmd);
  
  //需要发送的数据
  var packetBuffer = stringToArrayBuffer(cmd);

  wx.writeBLECharacteristicValue({
    deviceId: devices.deviceId,
    serviceId: devices.serviceUUId,
    characteristicId: devices.writeCharacteristicsUUID,
    value: packetBuffer,

    success: function (res){
      //发送成功
      writeLogcat("蓝牙发送成功");
      onSuccessCallback();
    },

    fail: function (res) {
      writeLogcat("蓝牙发送失败,失败原因: " + JSON.stringify(res));
      //onFailCallback();
    },
  });

3.3.6 读取串口发送的数据 
wx.getBLEDeviceCharacteristics --> success:wx.notifyBLECharacteristicValueChange --> success:wx.readBLECharacteristicValue --> wx.onBLECharacteristicValueChange
 

/**
 * 2. 接收数据处理  -- 打开定时器,200ms 超时表示接收完成
 */
function receiveBleData(res)
{
  var dataStr = arrayBufferToHexString(res.value).toUpperCase();
  writeLogcat("接收长度 = " + (dataStr.length / 2) + ", 数据 = " + dataStr);
  
  globalReceiveBuffer += dataStr;
  globalPacketTotalLength += (dataStr.length / 2);

  writeLogcat("当前接收总长度 = " + globalPacketTotalLength);

  //收到包头,计算微信协议数据包总长度
  if (dataStr.substr(0, 2) == "FE" && globalIsCalwxProtocolLength == false)
  {
    globalIsCalwxProtocolLength = true;

    //计算微信协议数据总长度
    globalwxPacketLength = hex2StringToInt(dataStr.substr(4, 4));
    writeLogcat("微信协议数据总长度 = " + globalwxPacketLength);
  }
  else
  {
    //判断是否接收完整一包的微信协议包,是则调用回调处理
    //接收长度大于等于微信协议长度
    if (globalPacketTotalLength >= globalwxPacketLength && globalIsCalwxProtocolLength == true)
    {
      //处理数据
      writeLogcat("接收完整一包完成,开始处理数据");
      writeLogcat("完整一包数据 = " + globalReceiveBuffer);
      
      sleep(100);

      processwxProtocol(globalReceiveBuffer);
     // wx.onBLECharacteristicValueChange(receiveBleData);

      //等待接收下一包
      globalReceiveBuffer = "";
      globalPacketTotalLength = 0;
      globalwxPacketLength = 0;
      globalIsCalwxProtocolLength = false;
    }
    else
    {
      globalReceiveBuffer = "";
      globalPacketTotalLength = 0;
      globalwxPacketLength = 0;
      globalIsCalwxProtocolLength = false;
    }
  }
}

3.3.7 关闭蓝牙适配器

/***
  * 2. 关闭蓝牙适配器
  */
function closeBle(callback) {
  wx.closeBluetoothAdapter({
    success: function (res) {
      writeLogcat("关闭蓝牙适配器成功," + JSON.stringify(res));
      callback(0, null);
    },

    fail: function (res) {
      writeLogcat("关闭蓝牙适配器失败, 失败原因: " + JSON.stringify(res));
      callback(1, null);
    }
  })
}

问题总结:

1. android手机使用小程序的BLE模块,广播中的deviceId表示设备的mac信息,ios系统则是手机mac和设备mac加密产生的uuid值!连接设备也如此:安卓直接使用mac进行连接操作;但ios使用广播中读取到的UUID进行连接。

简而言之,全是扫描到设备后的deviceId信息。

2. Android手机使用小程序操作BLE设备,连接成功后可以直接进行特征数据的获取;但ios直接调用时,会出现10004的报错  官方文档报错信息连接。

如何解决ios手机使用小程序BLE报错问题。

只需要在连接成功和读设备特征数据之间,进行一项开启通信服务操作即可,按照java android开发ble规范来说,连接成功后是需要优先开启服务的,所以android和ios都统一开启服务!!

3.ISO 系统:

1.1 我使用自己的手机(型号是iPhone XS Max  系统版本:  ios13.1 )测试时,发现怎么初始化蓝牙适配器总是失败,后来怀疑是不是系统太新了,小程序底层没有适配,于是迫不得已我当天晚上在宿舍刷机(o(╥﹏╥)o),刷回ios12.4.1 测试发现可以初始化蓝牙适配器,后来第二天早上在上班路上,看最先ios13 新闻,突然间想起来在ios13 新增蓝牙权限,是不是权限问题呢?于是,我又把手机升级到ios13.0  测试发现真的是权限问题。o(╥﹏╥)o   白白刷了两次手机。

4. 蓝牙BLE 最大支持20个字节发送,因此,超过20个字节需要分包发送。

参考文章

通用属性配置文件(GATT)及其服务,特性与属性介绍

猜你喜欢

转载自blog.csdn.net/gd6321374/article/details/101165973