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個字節需要分包發送。