相關概念
BR
Basic Rate,早期的傳統藍牙技術 V1.1, V1.2 版本,傳輸速率為748~810kb/s。EDR
Enhanced Data Rate,傳統藍牙技術 V2.0, V2.1 版本,優化傳輸速率,減少耗電,速率為1.8M/s~2.1M/s。AMP
GenericAlternate MAC/PHY,高速藍牙技術,V3.0版本。
采用交替射頻技術,藍牙模塊僅創建設備間的配對,數據傳輸通過WIFI射頻來完成以達到高速率。
假如設備某一方沒有內建WIFI模塊,速率將降至 EDR 速率。BLE
Bluetooth Low Energy,低耗藍牙技術,V4.0版本的新規范,通過三個方式實現超低功耗:
1.大幅度削減掃描信道
2.極短的鏈路連接時間
3.采用長度很短的數據包
低耗藍牙的芯片有單模和雙模,前者只支持LE技術,后者兼容BR/EDR技術。
1:GATT 協議
GATT概述
GATT(Generic Attributes,通用屬性協議),定義了一種面向 BLE設備 的分層數據結構。
GATT建立在ATT( Attribute Protocol,通用訪問協議)之上,ATT使用GATT數據定義兩個BLE設備間收發標準消息的方式。
由于 GATT 是面向 LE 技術的協議,所以在只支持 BR/EDR 技術的設備上無法使用。-
GATT分層數據結構的層次
GATT定義了用于BLE設備傳輸數據的標準數據結構,結構主要包括了如上圖所示的:
1.服務(Service)
2.特征(Characteristic)
3.描述符(Descriptor)。 配置文件(Profile):
配置文件,GATT頂層,該由滿足 配置實例 需要的一個或多個服務組成。-
服務(Service):
服務 由 特征 和 其他服務的引用 組成,擁有固定的 UUID 作為標記值。
設備的功能主要體現在服務上,每種服務都對應著某一種功能。可以到官網上查看服務列表 GATT Services。
通過服務列表中的 Assigned Numbers 可以獲取服務的UUID。Assigned Numbers轉換成可用的服務UUID 的方法于文檔 Service Discovery。
簡單來說,就是:"服務的Assigned Numbers"-0000-1000-8000-00805F9B34FB
-
特征(Characteristic):
特征是BLE通信的主體,是一個服務端和客戶端共享的讀寫空間。
主機在從機上獲取所需的信息,實際就是通過獲取對應的特征的內容進行的。特征由屬性值和描述符組成:
- 屬性值
屬性值包括聲明(Declaration),值(Value),一個屬性值最少包括一個聲明和一個值,即是屬性值是特征必選的條目。 - 描述符
特征可以包括零到若干個描述符,可選條目。
特征信息列表可以查看官方文檔 GATT Characteristics。
- 屬性值
-
描述符(Descriptors)
用于表達 特征 的其他附加信息,如特征值的有效范圍,可讀性描述等信息。其中包含了特殊的 CCCD(Client Characteristic Configuration Descriptor, Assigned Number : 0x2902):
CCCD 可以設置 服務端 在對應特征值發生變化時,是否對 客戶端 進行信息 推送(直接發送信息) 或 提示(發送一個提示并等待回復)。
當特征包含通知能力時,CCCD為必選項。描述符列表可以查看官方文檔 GATT Descriptors。
2:Android BLE 相關 API
-
BluetoothAdapter
藍牙適配器:本地設備藍牙適配器,提供基本藍牙功能的工具,例如開啟藍牙發現,查詢配對設備,實例化藍牙設備鏈接,監聽連接請求,掃描設備等。
基本上說,藍牙適配器是進行藍牙操作的起點。獲取BluetoothAdapter實例,在 API 18 及以上的設備,使用:
BluetoothManager.getAdapter
在API18以下設備使用以下API獲取:
BluetoothAdapter.getDefaultAdapte
本類線程安全。涉及到的權限為:
<uses-permission android:name="android.permission.BLUETOOTH"/> <uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
-
BluetoothDevice
遠程藍牙設備:提供了遠程藍牙設備的基本信息,如名稱,地址,類別,綁定狀態等。
本質上只是對藍牙硬件地址的簡單包裝。該類的實例不可修改。一般來說,通過掃描設備的掃描結果回調中獲取。
也可以直接通過以下方式獲取:/* 使用已知的物理地址作為參數進行連接 */ BluetoothAdapter.getRemoteDevice(address); /* 獲取已適配的藍牙記錄列表 */ BluetoothAdapter.getBondedDevices();
-
BluetoothGatt
GATT客戶端,GATT協議的公共API,提供了GATT的基本功能,如實現藍牙設備的通信。
通過掃描支持LE技術的藍牙設備,獲取到 BluetoothDevice,然后通過:/* GATT連接操作的回調 */ BluetoothGattCallback mCallback; BluetoothDevice.connectGatt(content, autoConnect, mCallback);
通過設置 BluetoothGattCallback 回調,可以從回調中得到 BluetoothGatt 實例。
-
BluetoothGattCallback
GATT狀態回調,大部分GATT操作的結果都會通過該類實例回調,包括:/* 連接狀態回調,包括連接到服務器 / 從服務器斷開連接 */ onConnectionStateChange(); /* 遠程設備發現新服務 */ onServicesDiscovered(); /* 特征相關操作的回調 */ onCharacteristicRead(); onCharacteristicWrite(); onCharacteristicChanged();
同時,掃描設備 和 停止掃描 的操作,都需要用到該類的實例。
-
BluetoothGattService
GATT服務,根據服務的 UUID,嘗試獲取服務實例。/* 如果對應的設備支持該服務,則返回一個服務的實例,否則返回空 */ BluetoothGatt.getService(uuid);
-
BluetoothGattCharacteristic
GATT特征,實際通信中的數據信息主體。通過以下方法獲取:/* 獲取對應UUID的特征 */ BluetoothGattService.getCharacteristic(uuid); /* 獲取服務的特征列表 */ BluetoothGattService.getCharacteristics();
3:Android BLE 開發示例
-
聲明權限
一個聲明和兩個基本權限:
<uses-feature android:name"android.permission.BLUETOOTH_ADMIN"/> <uses-permission android:name="android.permission.BLUETOOTH"/> <uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
執行搜索BLE設備的時候,需要使用定位權限。
而在5.0及以上的版本,需要手動聲明GPS硬件模塊功能的權限:<uses-feature android:name="android.hardware.location.gps"/>
而在6.0及以上版本,掃描設備還需要 動態申請 以下權限:
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
-
檢查設備支持性
如果設備不支持BLE,可以跳過BLE相關操作了。boolean checkSupport() { return getPackageManager() .hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE); }
-
初始化BluetoothAdapter
private BluetoothAdapter mAdapter; BluetoothManager bluetoothManager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE); mAdapter = bluetoothManager.getAdapter();
然后檢查藍牙的支持性,及是否已打開藍牙。
if (mAdapter == null) { return; } ... private final static int REQUEST_ENABLE_BT = 1; if (!mAdapter.isEnabled()) { Intent intent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE); startActivityForResult(intent, REQUEST_ENABLE_BT); }
-
啟動設備掃描
創建LeScanCallback實例:
首先需要實現一個 LeScanCallback 實例,掃描結果會通過實例的 onLeScan 方法返回:LeScanCallback mCallBack = new LeScanCallback (){ @Override public void onLeScan(BluetoothDevice device, int rssi, byte[] scanRecord) {} }
啟動掃描與停止掃描:
··· static int SCAN_TIME = 5_000; ··· Handler mHandler = new Handler(); /* 開始掃描: 由于掃描消耗電量,所以不能一直處于掃描狀態, 設置掃描一段時間后關閉掃描 */ mAdapter.startLeScan(mCallBack); mHandler.postDelay(()->{ /* 關閉掃描: * 注意需要傳入啟動掃描時的 callback對象,否則無效 */ mAdapter.stopLeScan(mCallBack); }, SCAN_TIME);
在 API 21 及以上時,掃描操作應使用 BluetoothLeScanner:
final ScanCallback callback = new ScanCallback() {}; final BluetoothLeScanner scanner = mAdapter.getBluetoothLeScanner(); scanner.startScan(new ScanCallback(){}); mHandler.postDelay(()->{ scanner.stopScan(scanCallback); }, SCAN_TIME);
-
獲取掃描結果
以 LeScanCallback 的回調方法 onLeScan 分析:/** * @param device: 識別到的遠程設備 * * @param rssi: 信號強度指示,計數為dB。可以通過: d = 10^((abs(RSSI) - A) / (10 * n)) 計算出距離。A和n根據環境改變,需經實驗測出, 給出兩個網上的經驗值: <1> A: 50 n: 2.5 <2> A: 59 n: 2.0 * * @param scanRecord:廣播數據和掃描應答數據數據 BLE設備在對外廣播中,廣播中會攜帶一些有用的信息。 其中包含了 廣播數據 和 掃描應答數據, 兩者有效荷載最大都為 31字節(藍牙4), 以十六進制格式存儲,可通過 bytesToHex 轉換成可用的字符串。 */ void onLeScan(BluetoothDevice device, int rssi, byte[] scanRecord) {}
注意相同的 BluetoothDevice 會重復出現在回調中,所以如果要記錄藍牙列表,需要自行 過濾 重復出現的設備,或更新對應重復出現的設備的信息。
bytesToHex 參考 -
連接外圍設備
通過 BluetoothDevice 的 connectGatt 方法獲取一個 BluetoothGatt 實例。
connectGatt 有多個重載方法,這里介紹其中最復雜的重載方法:/** * 以客戶端的身份連接到該設備托管的GATT服務器 * * @param autoConnect:自動連接,設備不可用時會不斷嘗試重連。 * * @param callback: BluetoothGattCallback實例,用于接收異步回調 * * @param transport: GATT連接到雙模設備的首選傳輸模式: * 1:TRANSPORT_AUTO 自動選擇 (默認值) * 2:TRANSPORT_BREDR BR/EDR 傳統藍牙 * 3:TRANSPORT_LE LE 低耗藍牙 * * @param phy: PHY物理層的模式選擇: * 1:PHY_LE_1M_MASK: * 默認值,LE設備強制要求支持的模式, * 符號速率為1M/s,未編碼。 * 2:PHY_LE_2M_MASK: * 符號速率為2M/s,未編碼, * 用于 藍牙5 的 "2x speed" 2倍速率。 * 3:PHY_LE_CODED_MASK: * 在數據包中增加糾錯編碼以實現更遠的傳輸范圍, * 以實現 藍牙5 的 "4x range" 4倍范圍。 * 使用FEC編碼,根據方案又分為: * LE Coded S=2:2個編碼位代替原來一個數據位, * 速率降為 500K/s,傳輸范圍增大2倍; * LE Coded S=8:8個編碼位代替原來一個數據位, * 速率降為 125K/s,傳輸范圍增大4倍; * 設置 autoConnect 自動連接時,該項無效 * * @param handler: 傳入一個Handler,以指定回調發生的線程, * 傳入null時,回調將會在一個未指定的后臺線程上進行。 */ BluetoothGatt connectGatt(Context context, boolean autoConnect, BluetoothGattCallback callback, int transport, int phy, Handler handler) { ··· }
一般情況下使用默認值既可,
注意必須傳入非空的callback,否則會拋出 IllegalArgumentException:BluetoothDevice.connectGatt(content, autoConnect, callback);
當連接成功時,會回調 callback 的 onConnectionStateChange 方法
/** * GATT客戶端的連接狀態回調 * * @param gatt: GATT客戶端。 * @param status: 連接或斷開操作的執行結果, 成功返回 GATT_SUCCESS * @param newState:當前的連接狀態:STATE_CONNECTED / STATE_DISCONNECTED */ void onConnectionStateChange(BluetoothGatt gatt, int status, int newState);
status 表示連接操作的結果,只有status為 GATT_SUCCESS 時,newState才是有效值。
注意一臺安卓設備最多同時連接6個左右的藍牙設備,超出時可能出現:
status == 133 連接錯誤,
所以需要注意調用 BluetoothGatt.close() 方法進行資源釋放。
可參考:Android中BLE連接出現“BluetoothGatt status 133”的解決方法當 status == GATT_SUCCESS,且 newState == STATE_CONNECTED 時,表示已成功連接設備,可以進行下一步操作。
-
發現服務
在建立連接之后,就可以通過 BluetoothGatt實例 進行發現服務操作,查找設備支持的服務。/** * 異步操作,發現服務完成時,會回調onServicesDiscovered()方法。 * 假如發現服務已在啟動狀態中,則返回true */ boolean discoverService();
等待 BluetoothGattCallback 的 onServicesDiscovered() 被回調:
/** * @param gatt: 執行發現服務后的GATT客戶端。 * @param status: 發現服務的執行結果, 成功返回 GATT_SUCCESS */ void onServicesDiscovered(BluetoothGatt gatt, int status) ;
當 status 返回GATT_SUCCESS,表示與外部設備成功建立 可通信連接,
意味著可以執行如:寫入數據,讀取藍牙設備的數據等 藍牙通信操作了。
先把獲取到的 BluetoothGatt實例 記錄為 mGatt:··· BluetoothGatt mGatt; void onServicesDiscovered(BluetoothGatt gatt, int status) { mGatt = gatt; }
-
獲取服務
發現服務成功之后,可以通過以下的方法嘗試獲取 BluetoothGattService 實例:/* 獲取遠程設備提供的服務列表, * 如果未執行發現服務,會返回一個空列表 */ mGatt.getServices(); /* 通過服務的UUID,獲取指定的服務, * 如果遠程設備不支持給定UUID的服務,返回null, * 如果遠程設備存在多個給定UUID的服務實例,則返回第一個實例 */ mGatt.getService(UUID);
獲取到 BluetoothGattService 之后,就可以通過獲取服務的特征進行讀寫。
-
特征的讀寫數據
前面介紹了,通信主體實際上是 特征,要進行讀寫操作,其實就是在操作特征里的屬性詞條,所以要先通過 服務 獲取 特征:/* 假設 service 是從上一步獲取到的一個 BluetoothGattService 實例*/ ··· BluetoothGattService service; /* 獲取該服務的特征列表 */ service.getCharacteristics(); /* 通過特征的UUID,獲取指定的特征, * 如果沒有找到給定UUID的特征,返回null, * 如果服務中存在多個給定UUID的特征,則返回第一個實例 */ service.getCharacteristic(UUID);
獲取到了特征之后,就可以通過上面獲取到的 mGatt 讀寫信息:
/* 上一步獲取的 BluetoothGattCharacteristic 實例 */ ··· BluetoothGattCharacteristic characteristic; /* 從關聯的遠程設備讀取請求的特征, * 異步操作,請求發起成功則返回true,讀取完成會回調: * BluetoothGattCallback.onCharacteristicRead() */ mGatt.readCharacteristic(characteristic); /* 將給定的特征及其值寫入關聯的遠程設備, * 異步操作,請求發起成功則返回true,寫入完成會回調: * BluetoothGattCallback.onCharacteristicWrite() */ mGatt.writeCharacteristic(characteristic);
讀寫操作都是異步操作,方法返回的是請求是否成功,請求結果都會回調 BluetoothGattCallback 的方法:
/** * 讀操作的回調 * @param characteristic: 讀取后的特征 * @param status: 讀取結果,成功為 GATT_SUCCESS */ void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { ··· } /** * 寫操作的回調 * @param characteristic: 寫入后的特征 * 注意:這里返回的特征,為設備當前的特征, * 應該在該回調中,應對比該特征的內容是否符合期望值, * 如果與期望值不同,應該選擇重發或終止寫入。 * * @param status: 寫入結果,成功為 GATT_SUCCESS */ void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { ··· }
寫數據的時候要注意,需要對比返回的特征和寫入的特征,判斷是否寫入成功或者產生了異常,選擇繼續寫入或者重寫,或者放棄操作。
-
描述符的讀寫數據
讀寫方式與 特征 的 讀寫方式基本一致,不再過多描述 :/* 獲取描述符 */ ··· BluetoothGattCharacteristic characteristic; characteristic.getDescriptors(); characteristic.getDescriptor(UUID); /* 通過 mGatt 讀寫數據 * 同樣,寫操作需要做寫入結果校驗 */ ··· BluetoothGattDescriptor descriptor; mGatt.readDescriptor(descriptor); mGatt.writeDescriptor(descriptor); /* 結果回調 */ void onDescriptorRead(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) { ··· } void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) { ··· }
-
讀寫數據需要注意的問題
寫入數據量:
每次寫操作的時候,無論是 特征 或者 描述符,一般來說最大只能設置 20個字節 的數據。
這是因為ATT協議中,最大傳輸單元MTU的默認大小為23字節,其中3字節用于ATT協議的控制數據,所以GATT可用的數據大小默認為剩余的20字節。ATT的MTU最大值為512,在API 21及以上的安卓平臺,可以通過以下方法嘗試改變MTU的大小:
··· int mMtu; /* 請求變更MTU的大小 */ BluetoothGatt.requestMtu(mMtu); /* 請求結果通過 BluetoothGattCallback 回調 * 當statue返回為 GATT_SUCCESS 時,表示變更成功 * 變更成功后,可以使用(mMtu - 3)的大小傳輸數據*/ public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) {}
無法改變的時候,超過20字節的數據,進行分包發送(BLE服務端需要支持)。
讀寫間隔:
讀寫操作都是隊列操作,需要等待操作結果返回后,才能進行下次操作,若當次操作未完成,下次操作調用時,將直接返回操作啟用失敗。寫入操作時,需等待服務器的確認信息,即寫入回調,再進行下次寫入操作。
當寫入類型設置為 不需要接收服務器確認信息(PROPERTY_WRITE_NO_RESPONSE)以加快傳輸速度時,兩次操作之間應保留 80ms ~ 100ms 或以上的延時。 -
數據變更通知
前面說到ATT支持通知,一些特征在值發生變化時,可以主動向申請了監聽數據變化的客戶端推送通知或指示(不帶數據)。
開啟特征的監聽,需要進行兩步操作:設置特征信息推送:
/** * 啟用或禁用給定特征的通知或指示 * @param characteristic: 需要進行操作的特征 * @param enable : 開啟或關閉 */ BluetoothGatt.setCharacteristicNotification(BluetoothGattCharacteristic characteristic, boolean enable);
寫入CCCD:
雖然開啟了特征的信息推送,但假如特征本身禁用了通知和指示,則不會有更新推送。
前面提到了一個特殊的標識符CCCD,用于控制特征的消息推送。需要對特征的CCCD描述符進行操作,將其值置為 1 / 2,才能開啟對應的 通知 / 指示 功能。/* 設置特征信息推送 */ ··· BluetoothGattCharacteristic characteristic; mGatt.setCharacteristicNotification(characteristic,true); /* CCCD 的UUID */ private UUID ID_CCCD = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"); /* 獲取CCCD */ BluetoothGattDescriptor cccd = characteristic.getDescriptor(ID_CCCD); /* 設置推送通知,參考值為: * BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE: 通知 * BluetoothGattDescriptor.ENABLE_INDICATION_VALUE: 指示 * BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE: 關閉 */ cccd.setValue(參考值); /* 寫入CCCD */ mGatt.writeDescriptor(descriptor);
以上操作完成后,即開啟對應特征的更新推送了。
接收推送:
更新推送會回調BluetoothGattCallback的onCharacteristicChanged()方法:/** * 特征變更推送觸發的回調 * @param gatt: 特征 關聯的 BluetoothGatt 實例 * @param characteristic: 更新后的 特征 */ void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic)
-
關閉客戶端
用完的東西總是要收拾好。斷開連接:
/* 斷開當前連接,如果正在連接中,則取消連接操作 */ BluetoothGatt.disconnect();
斷開連接操作后,結果回調 onConnectionStateChange() 方法,應該通過回調返回的結果 status 和 newState 判斷是否成功斷開。
關閉Gatt客戶端:
成功斷開連接之后(甚至是斷開失敗),應該調用 BluetoothGatt 的close() 方法關閉客戶端釋放資源。
安卓同時連接遠程設備的資源極其有限,在所以任何情況不再需要連接遠程設備時,都要使用BluetoothGatt 的 close() 方法釋放資源。
參考文章:
藍牙技術基礎知識學習
藍牙核心技術概述
GATT協議及藍牙核心系統結構
Android BLE的總結
Android BLE 藍牙開發入門
更具體的藍牙技術說明請查看官方網站
Bluetooth Technology Website
歡迎留言,歡迎關注,會持續更新 安卓開發 中遇到的問題和技術上的一些自我總結。
如有錯誤,歡迎指出。