微信小程序 - 藍牙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)及其服務,特性與屬性介紹

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章