android BLE系列:
一、藍芽基礎協議
想了解藍芽通訊之前,需要先了解藍芽兩個最基本的協議:GAP 和 GATT。
1、GAP(Generic Access Profile)簡介
GAP是通用訪問配置檔案的首字母縮寫,主要控制藍芽連線和廣播。GAP使藍芽裝置對外界可見,並決定裝置是否可以或者怎樣與其他裝置進行互動。
GAP定義了多種角色,但主要的兩個是:中心裝置 和 外圍裝置。
中心裝置:可以掃描並連線多個外圍裝置,從外設中獲取資訊。
外圍裝置:小型,低功耗,資源有限的裝置。可以連線到功能更強大的中心裝置,併為其提供資料。
2、GAP廣播資料
GAP 中外圍裝置通過兩種方式向外廣播資料:廣播資料 和 掃描回覆。
每種資料最長可以包含 31 byte。
廣播資料是必需的,因為外設必需不停的向外廣播,讓中心裝置知道它的存在。
掃描回覆是可選的,中心裝置可以向外設請求掃描回覆,這裡包含一些裝置額外的資訊。
3. 廣播的網路拓撲結構
外設通過廣播自己讓中心裝置發現自己,並建立 GATT 連線,從而進行更多的資料交換。但有些情況是不需要連線的,只要外設廣播自己的資料即可。目的是讓外圍裝置,把自己的資訊傳送給多箇中心裝置。因為基於 GATT 連線的方式的,只能是一個外設連線一箇中心裝置。
4. GATT(Generic Attribute Profile)簡介
GATT配置檔案是一個通用規範,用於在BLE鏈路上傳送和接收被稱為“屬性”的資料塊。目前所有的BLE應用都基於GATT。
BLE裝置通過叫做 Service 和 Characteristic 的東西進行通訊
GATT使用了 ATT(Attribute Protocol)協議,ATT 協議把 Service, Characteristic對應的資料儲存在一個查詢表中,次查詢表使用 16 bit ID 作為每一項的索引。
GATT 連線是獨佔的。也就是一個 BLE 外設同時只能被一箇中心裝置連線。一旦外設被連線,它就會馬上停止廣播,這樣它就對其他裝置不可見了。當外設與中心裝置斷開,外設又開始廣播,讓其他中心裝置感知該外設的存在。而中心裝置可同時與多個外設進行連線。
5.GATT 通訊
中心裝置和外設需要雙向通訊的話,唯一的方式就是建立 GATT 連線。
GATT 通訊的雙方是 C/S 關係。外設作為 GATT 服務端(Server),它維持了 ATT 的查詢表以及 service 和 characteristic 的定義。中心裝置是 GATT 客戶端(Client),它向 外設(Server) 發起請求來獲取資料。
6.GATT 結構
Profile:並不是實際存在於 BLE 外設上的,它只是一個被 Bluetooth SIG 或者外設設計者預先定義的 Service 的集合。例如心率Profile(Heart Rate Profile)就是結合了 Heart Rate Service 和 Device Information Service。
Service:包含一個或者多個 Characteristic。每個 Service 有一個 UUID 唯一標識。
Characteristic: 是最小的邏輯資料單元。一個Characteristic包括一個單一value變數和0-n個用來描述characteristic變數的Descriptor。與 Service 類似,每個 Characteristic 用 16 bit 或者 128 bit 的 UUID 唯一標識。
實際開發中,和 BLE 外設打交道,主要是通過 Characteristic。可以從 Characteristic 讀取資料,也可以往 Characteristic 寫資料,從而實現雙向的通訊。
UUID 有 16 bit 、32bit 和 128 bit 的。16 bit 的 UUID 是官方通過認證的,需要花錢購買。
Bluetooth_Base_UUID定義為 00000000-0000-1000-8000-00805F9B34FB
若16 bit UUID為xxxx,轉換為128 bit UUID為0000xxxx-0000-1000-8000-00805F9B34FB
若32 bit UUID為xxxxxxxx,轉換為128 bit UUID為xxxxxxxx-0000-1000-8000-00805F9B34FB
二、中心裝置與外設通訊
簡單介紹BLE開發當中各種主要類和其作用:
BluetoothDeivce:藍芽裝置,代表一個具體的藍芽外設。
BluetoothGatt:通用屬性協議,定義了BLE通訊的基本規則和操作
BluetoothGattCallback:GATT通訊回撥類,用於回撥的各種狀態和結果。
BluetoothGattService:服務,由零或多個特徵組構成。
BluetoothGattCharacteristic:特徵,裡面包含了一組或多組資料,是GATT通訊中的最小資料單元。
BluetoothGattDescriptor:特徵描述符,對特徵的額外描述,包括但不僅限於特徵的單位,屬性等。
1、連線
對掃描到的藍芽可以用集合形式進行快取,也可只儲存其mac地址,儲存到字符集合中,用於後續的連線。
1.1、根據mac地址獲取到BluetoothDeivce用於連線
BluetoothManager bluetoothmanager = (BluetoothManager)context.getSystemService(Context.BLUETOOTH_SERVICE);
mBluetoothAdapter = bluetoothmanager.getAdapter();
//獲取藍芽裝置物件進行連線
mBluetoothDevice = mBluetoothAdapter.getRemoteDevice(macAddressStr)
複製程式碼
1.2、藍芽gatt回撥:
實現BluetoothGattCallBack類,監聽藍芽連線過程中各種回撥的監聽。
藍芽Gatt回撥方法中都不可以進行耗時操作,需要將其方法內進行的操作丟進另一個執行緒,儘快返回。
//定義子執行緒handle,用於在BluetoothGattCallback中回撥方法中的操作拋到該執行緒工作。
private Handler mHandler;
//定義handler工作的子執行緒
private HandlerThread mHandlerThread;
初始化handler
mHandlerThread = new HandlerThread("daqi");
mHandlerThread.start();
//將handler繫結到子執行緒中
mHandler = new Handler(mHandlerThread.getLooper());
//定義藍芽Gatt回撥類
public class daqiBluetoothGattCallback extends BluetoothGattCallback{
//連線狀態回撥
@Override
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
super.onConnectionStateChange(gatt, status, newState);
// status 用於返回操作是否成功,會返回異常碼。
// newState 返回連線狀態,如BluetoothProfile#STATE_DISCONNECTED、BluetoothProfile#STATE_CONNECTED
//操作成功的情況下
if (status == BluetoothGatt.GATT_SUCCESS){
//判斷是否連線碼
if (newState == BluetoothProfile.STATE_CONNECTED) {
}else if(newState == BluetoothProfile.STATE_DISCONNECTED){
//判斷是否斷開連線碼
}
}else{
//異常碼
}
}
//服務發現回撥
@Override
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
super.onServicesDiscovered(gatt, status);
}
//特徵寫入回撥
@Override
public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
super.onCharacteristicWrite(gatt, characteristic, status);
}
//外設特徵值改變回撥
@Override
public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
super.onCharacteristicChanged(gatt, characteristic);
}
//描述寫入回撥
@Override
public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
super.onDescriptorWrite(gatt, descriptor, status);
}
}
複製程式碼
1.3、連線裝置:
呼叫BluetoothDevice#connectGatt()進行ble連線,第二個引數預設選擇false,不自動連線。並定義BluetoothGatt變數,儲存BluetoothDevice#connectGatt()返回的物件。
//定義Gatt實現類
private BluetoothGatt mBluetoothGatt;
//建立Gatt回撥
private BluetoothGattCallback mGattCallback = new daqiBluetoothGattCallback();
//連線裝置
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
mBluetoothGatt = mBluetoothDevice.connectGatt(mContext,
false, mGattCallback, BluetoothDevice.TRANSPORT_LE);
} else {
mBluetoothGatt = mBluetoothDevice.connectGatt(mContext, false, mGattCallback);
}
複製程式碼
1.4、連線異常處理:
藍芽連線時,不一定百分百連線成功。連線出錯時,會返回異常碼進行錯誤描述。
對於大多數異常碼,可以通過重連來達到連線成功的目的。
錯誤程式碼:
133 :連線超時或未找到裝置。
8 : 裝置超出範圍
22 :表示本地裝置終止了連線
//定義重連次數
private int reConnectionNum = 0;
//最多重連次數
private int maxConnectionNum = 3;
public class daqiBluetoothGattCallback extends BluetoothGattCallback{
//連線狀態回撥
@Override
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
super.onConnectionStateChange(gatt, status, newState);
// status 用於返回操作是否成功,會返回異常碼。
//操作成功的情況下
if (status == BluetoothGatt.GATT_SUCCESS){
}else{
//重連次數不大於最大重連次數
if(reConnectionNum < maxConnectionNum){
//重連次數自增
reConnectionNum++
//連線裝置
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
mBluetoothGatt = mBluetoothDevice.connectGatt(mContext,
false, mGattCallback, BluetoothDevice.TRANSPORT_LE);
} else {
mBluetoothGatt = mBluetoothDevice.connectGatt(mContext, false, mGattCallback);
}
}else{
//斷開連線,返回連線失敗回撥
}
}
}
//其他回撥方法
}
複製程式碼
2、發現服務
連線成功後,觸發BluetoothGattCallback#onConnectionStateChange()方法。
2.1、發現服務:
public class daqiBluetoothGattCallback extends BluetoothGattCallback{
//連線狀態回撥
@Override
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
super.onConnectionStateChange(gatt, status, newState);
// status 用於返回操作是否成功,會返回異常碼。
//操作成功的情況下
if (status == BluetoothGatt.GATT_SUCCESS){
//判斷是否連線碼
if (newState == BluetoothProfile.STATE_CONNECTED) {
//可延遲發現服務,也可不延遲
mHandler.post(() ->
//發現服務
mBluetoothGatt.discoverServices();
);
}else if(newState == BluetoothProfile.STATE_DISCONNECTED){
//判斷是否斷開連線碼
}
}
}
//其他回撥方法
}
複製程式碼
當發現服務成功後,會觸發BluetoothGattCallback#onServicesDiscovered()回撥:
//定義需要進行通訊的ServiceUUID
private UUID mServiceUUID = UUID.fromString("0000xxxx-0000-1000-8000-00805f9b34fb");
//定義藍芽Gatt回撥類
public class daqiBluetoothGattCallback extends BluetoothGattCallback{
//服務發現回撥
@Override
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
super.onServicesDiscovered(gatt, status);
if (status == BluetoothGatt.GATT_SUCCESS) {
mHandler.post(() ->
//獲取指定uuid的service
BluetoothGattService gattService = mBluetoothGatt.getService(mServiceUUID);
//獲取到特定的服務不為空
if(gattService != null){
}else{
//獲取特定服務失敗
}
);
}
}
}
複製程式碼
2.2、發現服務失敗:
發現服務時,會存在發現不了特定服務的情況。或者說,整個BluetoothGatt物件中的服務列表為空。
BluetoothGatt類中存在一個隱藏的方法refresh(),用於重新整理Gatt的服務列表。當發現不了服務時,可以通過反射去呼叫該方法。
3、修改特徵值並監聽外設特徵值改變
3.1、讀寫特定特徵:
//定義需要進行通訊的ServiceUUID
private UUID mServiceUUID = UUID.fromString("0000xxxx-0000-1000-8000-00805f9b34fb");
//定義需要進行通訊的CharacteristicUUID
private UUID mCharacteristicUUID = UUID.fromString("0000yyyy-0000-1000-8000-00805f9b34fb");
//定義藍芽Gatt回撥類
public class daqiBluetoothGattCallback extends BluetoothGattCallback{
//服務發現回撥
@Override
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
super.onServicesDiscovered(gatt, status);
if (status == BluetoothGatt.GATT_SUCCESS) {
mHandler.post(() ->
//獲取指定uuid的service
BluetoothGattService gattService = mBluetoothGatt.getService(mServiceUUID);
//獲取到特定的服務不為空
if(gattService != null){
//獲取指定uuid的Characteristic
BluetoothGattCharacteristic gattCharacteristic = gattService.getCharacteristic(mCharacteristicUUID);
//獲取特定特徵成功
if(gattCharacteristic != null){
//寫入你需要傳遞給外設的特徵值(即傳遞給外設的資訊)
gattCharacteristic.setValue(bytes);
//通過GATt實體類將,特徵值寫入到外設中。
mBluetoothGatt.writeCharacteristic(gattCharacteristic);
//如果只是需要讀取外設的特徵值:
//通過Gatt物件讀取特定特徵(Characteristic)的特徵值
String readValue = mBluetoothGatt.readCharacteristic(gattCharacteristic);
}
}else{
//獲取特定服務失敗
}
);
}
}
}
複製程式碼
當成功讀取特徵值時,會觸發BluetoothGattCallback#onCharacteristicRead()回撥。
@Override
public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
super.onCharacteristicRead(gatt, characteristic, status);
if (status == BluetoothGatt.GATT_SUCCESS) {
//獲取讀取到的特徵值
characteristic.getValue()
}
}
複製程式碼
當成功寫入特徵值到外設時,會觸發BluetoothGattCallback#onCharacteristicWrite()回撥。
@Override
public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
super.onCharacteristicWrite(gatt, characteristic, status);
if (status == BluetoothGatt.GATT_SUCCESS) {
//獲取寫入到外設的特徵值
characteristic.getValue()
}
}
複製程式碼
當寫入完特徵值後,外設會修改自己的特徵值,手機會觸發BluetoothGattCallback#onCharacteristicChanged()方法,進行雙向通訊。
@Override
public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
if (status == BluetoothGatt.GATT_SUCCESS) {
//獲取外設修改的特徵值
String value = characteristic.getValue()
//對特徵值進行解析
}
}
複製程式碼
4、斷開連線
斷開連線的操作分為兩步:
1、mBluetoothGatt.disconnect();
2、mBluetoothGatt.close();
呼叫disconnect()後,會觸發手機會觸發BluetoothGattCallback#onConnectionStateChange()的回撥,回撥斷開連線資訊,newState = BluetoothProfile.STATE_DISCONNECTED。但接著馬上呼叫close(),會終止BluetoothGattCallback#onConnectionStateChange()的回撥,可以看情況將兩個進行拆分呼叫,但一般都是成對出現。
例如:
需要在外設修改特徵值觸發BluetoothGattCallback#onCharacteristicChanged()時,斷開連線。可以先在BluetoothGattCallback#onCharacteristicChanged()中呼叫disconnect(),並等呼叫BluetoothGattCallback#onConnectionStateChange()回撥,返回斷開連線資訊後,再呼叫close()對Gatt資源進行關閉。
當和外設進行ble通訊時,如出現任何意外情況,馬上呼叫斷開連線操作。