Android?藍牙BLE開發(fā)完全指南
?介紹
1.BLE 是 Bluetooth Low Energy 的縮寫,意思為低功耗藍牙。由藍牙技術聯(lián)盟(Bluetooth SIG)設計的無線通訊技術,主要用于醫(yī)療,健身,安全和家庭娛樂行業(yè)。 與傳統(tǒng)藍牙相比,藍牙低功耗旨在大幅降低功耗和成本,同時也能夠達到相同的通訊效果。
支持多個平臺,包括 IOS,Android,Windows Phone 和 BlackBerry 以及 macOS,Linux,Windows 8 和 Windows 10 在內的移動操作系統(tǒng)本身支持藍牙低功耗。 藍牙 SIG 預測,到 2018 年,超過 90% 的藍牙智能手機將支持藍牙低功耗。
在安卓平臺,
在 Android 4.3 (API level 18) 以后引進來的,通過這些 API 可以掃描藍牙設備、連接設備,查詢 services、讀寫設備的 characteristics(屬性特征),然后通過屬性進行數(shù)據(jù)傳輸。
特點:
低功耗,使用 BLE 與周圍設備進行通訊時,其峰值功耗為傳統(tǒng)藍牙的一半
傳輸距離提升到 100 米
低延時,最短可在3 ms內完成連接并開始進行數(shù)據(jù)傳輸
缺點:
傳輸數(shù)據(jù)量較小,最大 512 個字節(jié),超過 20 個字節(jié)需要分包處理
應用領域:
主要用于智能硬件,像健康護理、運動和健身、設備電源管理等
連接模式
對于BLE單設備來講常見的藍牙模塊的工作模有四種:
主設備模式
從設備模式
廣播模式
Mesh組網模式
主設備模式
可以與一個從設備進行連接。在此模式下可以對周圍設備進行搜索并選擇需要連接的從設備進行連接。同時可以設置默認連接從設備的MAC地址,這樣模塊上電之后就可以查找此模塊并進行連接。
從設備模式
BLE支持從設備模式,在此模式下完全符合BLE4.1協(xié)議,用戶可以根據(jù)協(xié)議自己開發(fā)APP。此模式下包含一個串口收發(fā)的Service,用戶可以通過UUID找到它,里面有兩個通道,分別是讀和寫。用戶可以操作這兩個通道進行數(shù)據(jù)的傳輸。
廣播模式
在這種模式下模塊可以一對多進行廣播。用戶可以通過AT指令設置模塊廣播的數(shù)據(jù),模塊可以在低功耗的模式下持續(xù)的進行廣播,應用于極低功耗,小數(shù)據(jù)量,單向傳輸?shù)膽脠龊?,比如無線抄表,室內定位等功能。
Mesh組網模式
在這種模式下模塊可以實現(xiàn)簡單的自組網絡,每個模塊只需要設置相同的通訊密碼就可以加入到同一網絡當中,每一個模塊都可以發(fā)起數(shù)據(jù),每個模塊可以收到數(shù)據(jù)并且進行回復。并且不需要網關,即使某一個設備出現(xiàn)故障也會跳過并選擇最近的設備進行傳輸。
GATT協(xié)議
GATT generic Attributes的縮寫,中文是通用屬性,是低功耗藍牙設備之間進行通信的協(xié)議。
GATT定義了一種多層的數(shù)據(jù)結構,已連接的低功耗藍牙設備用它來進行通信,GATT層是傳輸真正數(shù)據(jù)所在的層。一個GATT服務器通過一個稱為屬性表的表格組織數(shù)據(jù),這些數(shù)據(jù)就是用于真正發(fā)送的數(shù)據(jù)。
GATT定義的多層數(shù)據(jù)結構簡要概括起來就是服務(service)可以包含多個特征(characteristic),每個特征包含屬性(properties)和值(value),還可以包含多個描述(descriptor)。它形象的結構如下圖:
profile(數(shù)據(jù)配置文件)
一個profile文件可以包含一個或者多個服務,一個profile文件包含需要的服務的信息或者為對等設備如何交互的配置文件的選項信息。設備的GAP和GATT的角色都可能在數(shù)據(jù)的交換過程中改變,因此,這個文件應該包含廣播的種類、所使用的連接間隔、所需的安全等級等信息。
需要注意的是: 一個profile中的屬性表不能包含另一個屬性表。
屬性
一個屬性包含句柄、UUID(類型)、值,句柄是屬性在GATT表中的索引,在一個設備中每一個屬性的句柄都是唯一的。UUID包含屬性表中數(shù)據(jù)類型的信息,它是理解屬性表中的值的每一個字節(jié)的意義的關鍵信息。在一個GATT表中可能有許多屬性,這些屬性能可能有相同的UUID。
個人理解,屬性指的是 Service、Characteristic 這樣的對象
Service
一個低功耗藍牙設備可以定義許多 Service, Service 可以理解為一個功能的集合。設備中每一個不同的 Service 都有一個 128 bit 的 UUID 作為這個 Service 的獨立標志。藍牙核心規(guī)范制定了兩種不同的UUID,一種是基本的UUID,一種是代替基本UUID的16位UUID。所有的藍牙技術聯(lián)盟定義UUID共用了一個基本的UUID:
0x0000xxxx-0000-1000-8000-00805F9B34FB
為了進一步簡化基本UUID,每一個藍牙技術聯(lián)盟定義的屬性有一個唯一的16位UUID,以代替上面的基本UUID的‘x'部分。例如,心率測量特性使用0X2A37作為它的16位UUID,因此它完整的128位UUID為:
0x00002A37-0000-1000-8000-00805F9B34FB
Characteristic
在 Service 下面,又包括了許多的獨立數(shù)據(jù)項,我們把這些獨立的數(shù)據(jù)項稱作 Characteristic。同樣的,每一個 Characteristic 也有一個唯一的 UUID 作為標識符。在 Android 開發(fā)中,建立藍牙連接后,我們說的通過藍牙發(fā)送數(shù)據(jù)給外圍設備就是往這些 Characteristic 中的 Value 字段寫入數(shù)據(jù);外圍設備發(fā)送數(shù)據(jù)給手機就是監(jiān)聽這些 Charateristic 中的 Value 字段有沒有變化,如果發(fā)生了變化,手機的 BLE API 就會收到一個監(jiān)聽的回調。
DesCriptor
任何在特性中的屬性不是定義為屬性值就是為描述符。描述符是一個額外的屬性以提供更多特性的信息,它提供一個人類可識別的特性描述的實例。然而,有一個特別的描述符值得特別地提起:客戶端特性配置描述符(Client Characteristic Configuration Descriptor,CCCD),這個描述符是給任何支持通知或指示功能的特性額外增加的。在CCCD中寫入“1”使能通知功能,寫入“2”使能指示功能,寫入“0”同時禁止通知和指示功能。
使用過程
常采用的模式是主機模式,然后掃描客戶端硬件,然后連接,獲取相關服務和特性,然后進行數(shù)據(jù)傳輸。
掃描
權限獲取
<uses-permission android:name="android.permission.BLUETOOTH"/> 使用藍牙所需要的權限 <uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/> 使用掃描和設置藍牙的權限(申明這一個權限必須申明上面一個權限)
在Android5.0之前,是默認申請GPS硬件功能的。而在Android 5.0 之后,需要在manifest 中申明GPS硬件模塊功能的使用。
<!-- Needed only if your app targets Android 5.0 (API level 21) or higher. --> <uses-feature android:name="android.hardware.location.gps" />
在 Android 6.0 及以上,還需要打開位置權限。如果應用沒有位置權限,藍牙掃描功能不能使用(其它藍牙操作例如連接藍牙設備和寫入數(shù)據(jù)不受影響)。
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
除了上面的設置之外,如果想設置設備只支持 BLE,可以加上下面這句話
<uses-feature android:name="android.hardware.bluetooth_le" android:required="true"/>
同樣,如果不想添加 BLE 的支持,那么可以設置 required="false"
然后可以在運行時判斷設備是否支持 BLE,
// Use this check to determine whether BLE is supported on the device. Then // you can selectively disable BLE-related features. if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) { Toast.makeText(this, R.string.ble_not_supported, Toast.LENGTH_SHORT).show(); finish(); }
初始化
判斷 BLE 在設備上是否支持,如果不支持的話,那么可以不用繼續(xù)后面的操作了;如果支持,但是有可能藍牙被禁掉了,因為開著藍牙比較好點,用戶一般都會關閉藍牙,這時候可以發(fā)送請求,來打開藍牙,可以通過兩個步驟來完成。
1.獲取 BluetoothAdapter
BluetoothAdapter 對于一個設備來說唯一的,整個系統(tǒng)或者應用,對藍牙進行操作時都是需要這個的適配器。它的獲取需要通過系統(tǒng)服務來獲取。
private BluetoothAdapter mBluetoothAdapter; ... // Initializes Bluetooth adapter. final BluetoothManager bluetoothManager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE); mBluetoothAdapter = bluetoothManager.getAdapter();
2.打開藍牙
一般對于用戶來說,在手機上藍牙是關閉,當開啟你的應用時就需要開啟藍牙,有兩種方式,一種是跳轉到設置界面,由用戶自己開啟藍牙;
另外一種時,直接在應用開啟藍牙,不需要用戶打開,而是直接幫用戶開啟手機上的藍牙。
跳轉到設置界面
// Ensures Bluetooth is available on the device and it is enabled. If not, // displays a dialog requesting user permission to enable Bluetooth. if (mBluetoothAdapter == null || !mBluetoothAdapter.isEnabled()) { Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE); startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT); }
直接開啟藍牙
// 打開藍牙 if (!mBluetoothAdapter.isEnabled()) { mBluetoothAdapter.enable(); }
掃描
掃描藍牙設備可以通過startLeScan(),其中有一個參數(shù)是 ScanCallback,通過它返回掃描結果,因為掃描過程是很耗電的,所以在掃描過程需要保證
1.一旦找到目標設備,需要停止掃描
2.掃描不要設置循環(huán),而且需要設置一個時間
回調如下
// 設備掃描回調 private ScanCallback mScanCallback = new ScanCallback() { @Override public void onScanResult(int callbackType, final ScanResult result) { runOnUiThread(new Runnable() { @Override public void run() { // 廣播的信息,可以在result中獲取 MDevice mDev = new MDevice(result.getDevice(), result.getRssi()); if (!mList.contains(mDev)) { mList.add(mDev); } if (mList.size() > 0) { mScanner.stopScan(mScanCallback); Toast.makeText(MainActivity.this, "掃描結束,設備數(shù) " + mList.size() , Toast.LENGTH_SHORT).show(); } } }); } };
開始掃描
private BluetoothAdapter mBluetoothAdapter; private boolean mScanning; private Handler mHandler; // Stops scanning after 10 seconds. private static final long SCAN_PERIOD = 10000; ... private void scanLeDevice(final boolean enable) { if (enable) { // Stops scanning after a pre-defined scan period. mHandler.postDelayed(new Runnable() { @Override public void run() { mScanning = false; mBluetoothAdapter.stopLeScan(mLeScanCallback); } }, SCAN_PERIOD); mScanning = true; mBluetoothAdapter.startLeScan(mLeScanCallback); } else { mScanning = false; mBluetoothAdapter.stopLeScan(mLeScanCallback); } ... }
這里遇到一個坑,就是實際中手機與一些智能硬件連接時,也就是需要連接指定的硬件,設備有一個UUID,所以可以通過如下方法連接
startLeScan(UUID[], BluetoothAdapter.LeScanCallback)
但是實際中使用時,連接時會出錯,仍需要再次驗證。
我當時的做法是采用了另外一種方法,當時這種方法,要求 API 高于 21。
private void scanLeDevice() { //50秒后停止掃描 mHander.postDelayed(stopScanRunnable, 50000); List<ScanFilter> filters = new ArrayList<>(); ScanFilter filter = new ScanFilter.Builder() //"D8:B0:4C:E8:66:DC" 測試MAC 1 //"D8:B0:4C:E2:45:2A" 測試MAC 2 .setDeviceAddress("D8:B0:4C:E2:45:2A") .build(); filters.add(filter); // 掃描 mScanner.startScan(filters, new ScanSettings.Builder().setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY).build(), mScanCallback); }
掃描結束后需要停止掃描
boolean startLeScan(BluetoothAdapter.LeScanCallback callback)
連接
設備連接
通過掃描能夠獲得設備 BluetoothDevice,包含地址和名字
通過設備連接并獲取 BluetoothGatt,后面通過 BluetoothGatt 的實例來進行client的操作,如使用該實例去發(fā)現(xiàn)服務,獲取讀、寫、通知等屬性
public static BluetoothGatt mBluetoothGatt; mBluetoothGatt = device.connectGatt(context, false, mGattCallback);
通過連接回調來監(jiān)聽連接的狀態(tài),包含三種狀態(tài),連接、斷開、正在連接,根據(jù)狀態(tài)可以發(fā)送廣播,
在接收廣播的位置進行做相應的處理
private final static BluetoothGattCallback mGattCallback = new BluetoothGattCallback() { @Override public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) { String intentAction; // GATT Server connected if (newState == BluetoothProfile.STATE_CONNECTED) { System.out.println("---------------------------->已經連接"); intentAction = ACTION_GATT_CONNECTED; mConnectionState = STATE_CONNECTED; broadcastConnectionUpdate(intentAction); } // GATT Server disconnected else if (newState == BluetoothProfile.STATE_DISCONNECTED) { System.out.println("---------------------------->連接斷開"); intentAction = ACTION_GATT_DISCONNECTED; mConnectionState = STATE_DISCONNECTED; broadcastConnectionUpdate(intentAction); } // GATT Server disconnected else if (newState == BluetoothProfile.STATE_DISCONNECTING) { System.out.println("---------------------------->正在連接"); // intentAction = ACTION_GATT_DISCONNECTING; // mConnectionState = STATE_DISCONNECTING; // broadcastConnectionUpdate(intentAction); } } }
當操作完成后,需要關閉連接,必須調用 BluetoothGatt#close 方法釋放連接資源
發(fā)現(xiàn)服務
由于有了 mBluetoothGatt,就可以去發(fā)現(xiàn)服務,再通過服務去獲取可以操作的屬性
mBluetoothGatt.discoverServices();
發(fā)現(xiàn)服務以及獲取其他屬性,如write和read,notify,Descriptor相關的屬性,均是在 BluetoothGattCallback 中有回調,在回調中就可以通過發(fā)送廣播,然后在其他位置做處理,
如接收數(shù)據(jù)就會有回調,然后將數(shù)據(jù)傳遞出去,對數(shù)據(jù)解析等
@Override public void onServicesDiscovered(BluetoothGatt gatt, int status) { // GATT Services discovered //發(fā)現(xiàn)新的服務 if (status == BluetoothGatt.GATT_SUCCESS) { System.out.println("---------------------------->發(fā)現(xiàn)服務"); broadcastConnectionUpdate(ACTION_GATT_SERVICES_DISCOVERED); } } //通過 Descriptor 寫監(jiān)聽 @Override public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) { } // 通過 Descriptor 讀監(jiān)聽 @Override public void onDescriptorRead(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) { } @Override public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { //write操作會調用此方法 if (status == BluetoothGatt.GATT_SUCCESS) { System.out.println("onCharacteristicWrite ------------------->write success"); Intent intent = new Intent(ACTION_GATT_CHARACTERISTIC_WRITE_SUCCESS); mContext.sendBroadcast(intent); } else { Intent intent = new Intent(ACTION_GATT_CHARACTERISTIC_ERROR); intent.putExtra(Constants.EXTRA_CHARACTERISTIC_ERROR_MESSAGE, "" + status); mContext.sendBroadcast(intent); } } @Override public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { // 接收數(shù)據(jù) } @Override public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { //notify 會回調用此方法 broadcastNotifyUpdate(characteristic); } @Override public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) { super.onMtuChanged(gatt, mtu, status); }
數(shù)據(jù)傳輸
1、數(shù)據(jù)讀取
這里有兩個方法:
方法一: 一般數(shù)據(jù)讀取的話,想到的是用 read 屬性,所以需要獲取特定通道的 BluetoothGattCharactristic。
1)BluetoothGatt#getService 得到服務
2)BluetoothGattService#getCharactristic 獲取 BluetoothGattCharactristic,這里獲取的 BluetoothGattCharactristic 是有指定 UUID 的,也就是說不同的 Charactristic的 UUID 是不同的,讀和寫的通道不同,根據(jù)不同的操作,然后通過UUID獲取相應的通道
3)BluetoothGattCharactristic#readCharacteristic 方法可以通知系統(tǒng)去讀取特定的數(shù)據(jù)
4)BluetoothGattCallback#onCharacteristicRead 方法。通過 BluetoothGattCharacteristic#getValue 可以讀取到藍牙設備的數(shù)據(jù)
方法二:采用 notify 屬性,客戶端發(fā)送數(shù)據(jù),服務端監(jiān)聽屬性變化,然后根據(jù) 屬性的 UUID 判斷是否是 notify 的屬性,如果是的話,說確實是由遠程設備發(fā)過來的數(shù)據(jù)。
1)BluetoothGatt#getService 得到服務
2)BluetoothGattService#getCharactristic 獲取 BluetoothGattCharactristic,這里的這個屬性是 notify 屬性
3)獲得屬性后需要進行判斷設備是否支持notify操作,然后再設備打開notify通知
void prepareBroadcastDataNotify( BluetoothGattCharacteristic characteristic) { final int charaProp = characteristic.getProperties(); Toast.makeText(this, " " + charaProp, Toast.LENGTH_SHORT).show(); if ((charaProp | BluetoothGattCharacteristic.PROPERTY_NOTIFY) > 0) { BluetoothLeService.setCharacteristicNotification(characteristic, true); } }
4) 設置屬性時,也要通知遠程設備端也要開啟 notify 屬性
public static void setCharacteristicNotification(BluetoothGattCharacteristic characteristic, boolean enabled) { if (mBluetoothAdapter == null || mBluetoothGatt == null) { return; } //通知遠程端開啟 notify if (characteristic.getDescriptor(UUID.fromString(GattAttributes.CLIENT_CHARACTERISTIC_CONFIG)) != null) { if (enabled == true) { BluetoothGattDescriptor descriptor = characteristic .getDescriptor(UUID.fromString(GattAttributes.CLIENT_CHARACTERISTIC_CONFIG)); descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE); mBluetoothGatt.writeDescriptor(descriptor); } else { BluetoothGattDescriptor descriptor = characteristic .getDescriptor(UUID.fromString(GattAttributes.CLIENT_CHARACTERISTIC_CONFIG)); descriptor.setValue(BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE); mBluetoothGatt.writeDescriptor(descriptor); } } mBluetoothGatt.setCharacteristicNotification(characteristic, enabled); }
數(shù)據(jù)寫入
對于 BLE 方式的數(shù)據(jù)傳輸來說,數(shù)據(jù)的大小是有限制的,一次性最多可以傳輸512個字節(jié),這也是BLE小數(shù)據(jù)量傳輸?shù)奶攸c,另外,對于每次傳輸,也有限制,每個數(shù)據(jù)包大小不超過20個字節(jié),超過20個字節(jié)的話,需要分包處理。寫的步驟和讀取類似。
1)BluetoothGatt#getService 得到服務
2)BluetoothGattService#getCharactristic 獲取 BluetoothGattCharactristic,這里的這個屬性是 write 屬性
3)寫入字節(jié)數(shù)據(jù)
public static void writeCharacteristicGattDb( BluetoothGattCharacteristic characteristic, byte[] byteArray) { if (mBluetoothAdapter == null || mBluetoothGatt == null) { return; } else { byte[] valueByte = byteArray; characteristic.setValue(valueByte); mBluetoothGatt.writeCharacteristic(characteristic); } }
4)對于手機端,寫入數(shù)據(jù)后,遠程端會接受,同時回調中也會能夠接收,也可以在回調中做一下數(shù)據(jù)判斷,看是否是自己發(fā)出的數(shù)據(jù)
@Override public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { //write操作會調用此方法 if (status == BluetoothGatt.GATT_SUCCESS) { System.out.println("onCharacteristicWrite ------------------->write success"); Intent intent = new Intent(ACTION_GATT_CHARACTERISTIC_WRITE_SUCCESS); // 這里通過屬性能夠讀取你發(fā)送的數(shù)據(jù),可以對此數(shù)據(jù)進行判斷 characteristic.getValue(); mContext.sendBroadcast(intent); } else { Intent intent = new Intent(ACTION_GATT_CHARACTERISTIC_ERROR); intent.putExtra(Constants.EXTRA_CHARACTERISTIC_ERROR_MESSAGE, "" + status); mContext.sendBroadcast(intent); } }
其他
一般在通訊過程中,需要有連接的心跳包,來檢測是否仍處于連接狀態(tài),可以通過設置定時器,主機端定時 write 數(shù)據(jù),客戶端定時 notify 數(shù)據(jù)
斷開連接
操作完成,需要斷開藍牙并釋放資源,通過 BluetoothGatt#disconnect 斷開連接,然后回調中會收到斷開的監(jiān)聽,可以根據(jù)狀態(tài)釋放資源。BluetoothGattCallback#onConnectionStateChange回調中通過這個方法的 newState 參數(shù)可以判斷是連接成功還是斷開成功的回調,斷開成功的話,然后調用 BluetoothGatt#close 方法釋放資源
參考
總結
到此這篇關于Android?藍牙BLE開發(fā)完全指南的文章就介紹到這了,更多相關Android?藍牙BLE開發(fā)內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
打飛機游戲終極BOSS Android實戰(zhàn)打飛機游戲完結篇
打飛機游戲終極BOSS,Android實戰(zhàn)打飛機游戲完結篇,具有一定的參考價值,感興趣的小伙伴們可以參考一下2016-07-07Android實現(xiàn)EventBus登錄界面與傳值(粘性事件)
這篇文章主要為大家詳細介紹了Android實現(xiàn)EventBus登錄界面與傳值,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-11-11android中寫一個內部類來選擇文件夾中指定的圖片類型實例說明
選擇文件夾中指定的圖片類型,本類是用來選擇文件夾中是.jpg類型的圖片具體實現(xiàn)如下,感興趣的朋友可以參考下哈2013-06-06如何通過Battery Historian分析Android APP耗電情況
Android 從兩個層面統(tǒng)計電量的消耗,分別為軟件排行榜及硬件排行榜。它們各有自己的耗電榜單,軟件排行榜為機器中每個 App 的耗電榜單,硬件排行榜則為各個硬件的耗電榜單。這兩個排行榜的統(tǒng)計是互為獨立,互不干擾的2021-06-06Android 利用ViewPager實現(xiàn)圖片可以左右循環(huán)滑動效果附代碼下載
本文通過一個小demo給大家展示一段代碼實現(xiàn)viewpage圖片左右循環(huán)滑動效果,對viewgager循環(huán)滑動相關知識感興趣的朋友一起學習吧2015-11-11