android 藍芽4.0(BLE)開發
最近剛好專案需要手機與藍芽模組通訊,基於藍芽4.0,網上資料較少也有些小坑,故作一下總結。
關鍵術語和概念
- 藍芽有傳統藍芽(3.0以下)和低功耗藍芽(BLE,又稱藍芽4.0)之分,而藍芽4.0開發需要android4.3版本(API 18)及以上才支援BLE API。相比傳統的藍芽,BLE更顯著的特點是低功耗。這一優點使android App可以與具有低功耗要求的BLE裝置通訊,如近距離感測器、心臟速率監視器、健身裝置等。
- BLE 全稱 Bluetooth Low Energy
- Generic Attribute Profile(GATT)—GATT配置檔案是一個通用規範,用於在BLE鏈路上傳送和接收被稱為“屬性”的資料塊。目前所有的BLE應用都基於GATT。 藍芽SIG規定了許多低功耗裝置的配置檔案。配置檔案是裝置如何在特定的應用程式中工作的規格說明。注意一個裝置可以實現多個配置檔案。例如,一個裝置可能包括心率監測儀和電量檢測。
- Attribute Protocol(ATT)—GATT在ATT協議基礎上建立,也被稱為GATT/ATT。ATT對在BLE裝置上執行進行了優化,為此,它使用了儘可能少的位元組。每個屬性通過一個唯一的的統一識別符號(UUID)來標識,每個String型別UUID使用128 bit標準格式。屬性通過ATT被格式化為characteristics和services。
- Characteristic 一個characteristic包括一個單一變數和0-n個用來描述characteristic變數的descriptor,characteristic可以被認為是一個型別,類似於類。
- Descriptor Descriptor用來描述characteristic變數的屬性。例如,一個descriptor可以規定一個可讀的描述,或者一個characteristic變數可接受的範圍,或者一個characteristic變數特定的測量單位。
- Service service是characteristic的集合。例如,你可能有一個叫“Heart Rate Monitor(心率監測儀)”的service,它包括了很多characteristics,如“heart rate measurement(心率測量)”等。你可以在bluetooth.org 找到一個目前支援的基於GATT的配置檔案和服務列表。
藍芽4.0的結構
簡單的說,就是BLE是基於GATT實現的,BLE分為三個部分Service、Characteristic、Descriptor,每個部分都擁有不同的 UUID來標識。一個BLE裝置可以擁有多個Service,一個Service可以包含多個Characteristic, 一個Characteristic包含一個Value和多個Descriptor,一個Descriptor包含一個Value。 通訊資料一般儲存在Characteristic內,目前一個Characteristic中儲存的資料最大為20 byte。 與Characteristic相關的許可權欄位主要有READ、WRITE、WRITE_NO_RESPONSE、NOTIFY。 Characteristic具有的許可權屬性可以有一個或者多個。
用一張圖來說明藍芽4.0的組成:
開發流程
1. 獲取相關許可權
在AndroidManifest.xml宣告相關許可權
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<!-- 似乎android M需要位置許可權才能掃描 -->
<uses-permission
android:name="android.permission.WRITE_SETTINGS" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
2. 獲取BluetoothManager和BluetoothAdapter
BluetoothManager manager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
BluetoothAdapter bleAdapter = manager.getAdapter();
// 也可以通過以下方式獲取BluetoothAdapter
BluetoothAdapter bleAdapter = BluetoothAdapter.getDefaultAdapter();
3. 檢測藍芽是否開啟或可用
public boolean check() {
return (null != bleAdapter && bleAdapter.isEnabled() && !bleAdapter.isDiscovering());
}
4. 掃描藍芽並實現回撥介面LeScanCallback
// 開始掃描
public void startScan(long scanDelay) {
if(!check()) return; // 檢測藍芽
BleScanCallback leScanCallback = new BleScanCallback(); // 回撥介面
if (bleAdapter.startLeScan(leScanCallback)) {
timer.schedule(new TimerTask() { // 掃描一定時間"scanDelay"就停止。
@Override
public void run() {
stopScan();
}
}, scanDelay);
}
}
// 實現掃描回撥介面
private class BleScanCallback implements BluetoothAdapter.LeScanCallback {
// 掃描到新裝置時,會回撥該介面。可以將新裝置顯示在ui中,看具體需求
@Override
public void onLeScan(BluetoothDevice device, int rssi, byte[] scanRecord) {
Log.e(TAG, "found device name : " + device.getName() + " address : " + device.getAddress());
}
}
有一個地方可以注意下,在API 21及以上,不推薦使用BluetoothAdapter.startLeScan和stopLeScan。而是新引入了BluetoothLeScanner類和ScanCallback回撥介面。由於需要相容API 19,所以這裡不作過多的闡述,有需要可以檢視官方文件瞭解。
5. 連線藍芽裝置並實現連接回調介面
// 連線藍芽裝置,device為之前掃描得到的
public void connect(BluetoothDevice device) {
if(!check()) return; // 檢測藍芽
if (null != bleAdapter) {
bleAdapter.stopLeScan(leScanCallback);
}
if (bleGatt.connect()) { // 已經連線了其他裝置
// 如果是先前連線的裝置,則不做處理
if (TextUtils.equals(device.getAddress(), bleGatt.getDevice().getAddress())) {
return;
} else {
disconnect(); // 否則斷開連線
}
}
// 連線裝置,第二個引數為是否自動連線,第三個為回撥函式
bleGatt = device.connectGatt(context, false, bleGattCallback);
}
// 實現連接回調介面[關鍵]
private class BleGattCallback extends BluetoothGattCallback {
// 連線狀態改變(連線成功或失敗)時回撥該介面
@Override
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
if (newState == BluetoothGatt.STATE_CONNECTED) { // 連線成功
Intent intent = new Intent(ActionBle.CONNECTED);
context.sendBroadcast(intent); // 這裡是通過廣播通知連線成功,依各自的需求決定
gatt.discoverServices(); // 則去搜索裝置的服務(Service)和服務對應Characteristic
} else { // 連線失敗
Intent intent = new Intent(ActionBle.CONNECT_FAIL);
context.sendBroadcast(intent);
}
}
// 發現裝置的服務(Service)回撥,需要在這裡處理訂閱事件。
@Override
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
if (status == BluetoothGatt.GATT_SUCCESS) {
services = gatt.getServices();
characteristics = new ArrayList<>();
descriptors = new ArrayList<>();
for (BluetoothGattService service : services) {
Log.e(TAG, "-- service uuid : " + service.getUuid().toString() + " --");
for (BluetoothGattCharacteristic characteristic : service.getCharacteristics()) {
Log.e(TAG, "---- characteristic uuid : " + characteristic.getUuid() + " ----");
characteristics.add(characteristic);
if (characteristic.getUuid().toString().equals(Command.READ_UUID)) {
// 訂閱資訊,否則接收不到資料「關鍵!」
setCharacteristicNotification(characteristic, true);
}
for (BluetoothGattDescriptor descriptor : characteristic.getDescriptors()) {
Log.e(TAG, "-------- descriptor uuid : " + characteristic.getUuid() + " --------");
descriptors.add(descriptor);
}
}
}
Intent intent = new Intent(ActionBle.DISCOVER_SERVICES_SUCCESS);
context.sendBroadcast(intent);
} else {
Intent intent = new Intent(ActionBle.DISCOVER_SERVICES_FAIL);
context.sendBroadcast(intent);
}
}
// 傳送訊息結果回撥
@Override
public void onCharacteristicWrite(BluetoothGatt gatt,
BluetoothGattCharacteristic characteristic,
int status) {
Intent intent;
if (BluetoothGatt.GATT_SUCCESS == status) { // 傳送成功
intent = new Intent(ActionBle.WRITE_DATA_SUCCESS);
} else { // 傳送失敗
intent = new Intent(ActionBle.WRITE_DATA_FAIL);
}
context.sendBroadcast(intent);
}
// 當訂閱的Characteristic接收到訊息時回撥
@Override
public void onCharacteristicChanged(BluetoothGatt gatt,
BluetoothGattCharacteristic characteristic) {
// 資料為 characteristic.getValue())
Log.e(TAG, "onCharacteristicChanged: " + Arrays.toString(characteristic.getValue()));
}
}
// 訂閱特徵集!「關鍵,才能接收到資料」
// public static final String DESCRIPTORS_UUID = "00002902-0000-1000-8000-00805f9b34fb";
// 這個uuid一般是固定的,不一樣視情況改變
private boolean setCharacteristicNotification(BluetoothGattCharacteristic characteristic, boolean enabled) {
if (bleGatt == null) {
return false;
}
BluetoothGattDescriptor localBluetoothGattDescriptor = characteristic.getDescriptor(UUID.fromString(DESCRIPTORS_UUID));
if (enabled) {
localBluetoothGattDescriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
} else {
localBluetoothGattDescriptor.setValue(BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE);
}
bleGatt.writeDescriptor(localBluetoothGattDescriptor);
return bleGatt.setCharacteristicNotification(characteristic, enabled);
}
連線裝置的回撥函式很關鍵,有幾個點需要注意:
1. 當裝置連線狀態改變(連線成功或失敗)時,會回撥onConnectionStateChange介面,需要在連線成功的時候呼叫gatt.discoverServices();去搜索裝置的服務Service集合。
2. 當裝置搜尋服務狀態(成功找到服務或失敗)時,會回撥onServicesDiscovered介面,這裡面很關鍵,需要處理訂閱相關特徵Characteristic:
bleGatt.setCharacteristicNotification(characteristic, true);
這個需要依據藍芽模組協議而定,只有在這裡訂閱了,才能接收到藍芽模組傳送過來的資料。
這裡還有個大坑,具體看setCharacteristicNotification。
3. 訂閱的特徵Characteristic接收到訊息,也就是藍芽模組傳送資料過來時,會回撥onCharacteristicChanged介面,資料的處理就在該介面處理。
6. 向藍芽模組傳送資料
其實也很簡單,根據協議往相關特徵寫入資料即可。
public boolean writeCharacteristic(String uuid, byte[] bytes) {
if (null == characteristics) {
return false;
}
Log.e(TAG, "writeCharacteristic to " + uuid + " : " + Arrays.toString(bytes));
if (null != bleGatt) {
// characteristics儲存了藍芽模組所有的特徵Characteristic
for (BluetoothGattCharacteristic characteristic : characteristics) {
// 判斷是否為協議約定的特徵Characteristic
if (TextUtils.equals(uuid, characteristic.getUuid().toString())) {
// 找到特徵,設定要寫入的資料
characteristic.setValue(bytes);
// 寫入資料,藍芽模組就接收到啦
return bleGatt.writeCharacteristic(characteristic);
}
}
}
return false;
}
注意這裡會回撥上面實現的BluetoothGattCallback.onCharacteristicWrite介面
這裡我用的藍芽模組約定傳送和接收的特徵uuid如下:
String READ_UUID = "6e400003-b5a3-f393-e0a9-e50e24dcca9e"; // 讀
String WRITE_UUID = "6e400002-b5a3-f393-e0a9-e50e24dcca9e"; // 寫
具體藍芽模組看具體的協議而定。
7. 斷開連線
public void disconnect() {
if (null != bleGatt) {
bleGatt.disconnect();
}
Log.e(TAG, "disconnect");
}
8. 資料的轉換
與藍芽模組的通訊一般都是採用16進位制,byte[]傳輸,因此需提供幾個格式轉換的方法。
// byte轉十六進位制字串
public static String bytes2HexString(byte[] bytes) {
String ret = "";
for (byte item : bytes) {
String hex = Integer.toHexString(item & 0xFF);
if (hex.length() == 1) {
hex = '0' + hex;
}
ret += hex.toUpperCase(Locale.CHINA);
}
return ret;
}
// 將16進位制的字串轉換為位元組陣列
public static byte[] getHexBytes(String message) {
int len = message.length() / 2;
char[] chars = message.toCharArray();
String[] hexStr = new String[len];
byte[] bytes = new byte[len];
for (int i = 0, j = 0; j < len; i += 2, j++) {
hexStr[j] = "" + chars[i] + chars[i + 1];
bytes[j] = (byte) Integer.parseInt(hexStr[j], 16);
}
return bytes;
}
以上就是android 藍芽4.0的基本操作啦,可以實現基本的通訊啦,瑟瑟發抖。