安卓藍芽BLE裝置開發
前段時間做了一個有關於安卓藍芽BLE裝置的開發專案,主要的功能包括了搜尋藍芽ble裝置和ble裝置的資料讀寫等等,本篇部落格用於記錄安卓藍芽ble裝置的通訊的細節。
其實關於BLE裝置的通訊在API中已經講地比較清楚了,這裡只是做一個總結,如果要進行BLE裝置的開發,首先可以閱讀API.
BLE有關API
BLE裝置的定義和特點
BLE-維基百科
對於BLE裝置的定義,我們只需要看維基百科的說明就好了,簡單的說明一下BLE的原理。
首先對於BLE硬體來說,它能夠決定什麼時候去發出請求和外接的裝置進行連線,安卓官方稱之為advertisement,市面上見到的BLE裝置,比如藍芽音響,一開啟就能夠被手機檢測到,原因是其硬體上設定了會一直髮出advertisement,直到連線上裝置為止。但是實際上BLE裝置本身是可以控制自己要不要發出advertisement的,如果BLE裝置沒有發出這個advertisement,那麼我們是搜尋不到它的。
對於BLE裝置還有一個坑,那就是BLE裝置只能連線一個裝置,也就是說,一旦當BLE裝置連線了一臺手機後,其他的手機上就無法搜尋到這臺BLE裝置了。我猜測應該是BLE裝置連線之後就不會發出advertisement了,導致其他裝置搜尋不到。
安卓上進行BLE開發的步驟
1、AndroidManifest中宣告許可權
對於BLE開發我們需要宣告兩個許可權
<uses-permission android:name="android.permission.BLUETOOTH"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
其中第一個許可權是隻要我們進行與藍芽相關的操作都需要宣告的,比如你的APP只是希望能夠被別人檢測到,然後通過藍芽交換資料,就需要宣告這一個。
第二個許可權是如果你的裝置需要主動搜尋藍芽裝置,或者是對藍芽的某些設定進行更改就需要這個許可權,一般來說我們在進行BLE開發的時候會同時宣告這兩個許可權。
因為不是所有的手機上都有藍芽BLE功能,需要API18(Android4.3)以上才能進行BLE的開發,所以我們可以宣告features過濾掉不滿足要求的裝置
<uses-feature android:name="android.hardware.bluetooth_le" android:required="true"/>
2、獲取BluetoothAdapter
BluetoothAdapter是用來獲取可以連線的BLE裝置列表的一個類,想要和BLE裝置通訊當然需要先能夠搜尋到可用的BLE裝置。安卓獲取BluetoothAdapter的例項的方式很簡單。首先通過呼叫系統服務獲得BluetoothManager的例項,然後呼叫BluetoothManager的getAdapter()方法就可以了,程式碼如下:
final BluetoothManager bluetoothManager =
(BluetoothManager) getSystemService(Context.BLUETOOTH _SERVICE);
mBluetoothAdapter = bluetoothManager.getAdapter();
當然,這裡的mBluetoothAdapter我們最好儲存為類的資料成員,因為之後我們還會用到它去獲取當前可連線的BLE裝置的列表。
3、開啟藍芽
得到了BluetoothAdapter的例項之後我們就可以用其中的方法去搜索裝置了,但是在此之前,我們首先要開啟手機上的藍芽功能,這個可以通過使用者自己找到藍芽去開啟,但是這樣很明顯會影響使用者體驗。沒有人想在使用APP的時候還需要去找到系統的藍芽並開啟。安卓API為我們提供了在APP中開啟系統藍芽的方法:即呼叫BluetoothAdapter中的enable()方法。
為了嚴謹,我們需要判斷上一步獲得的mBluetoothAdapter是否為空(只有當裝置不支援BLE的時候這個例項才會為null)
public void enable() {
if(mBluetoothAdapter == null || ! mBluetoothAdapter.isEnabled()) {
mBluetoothAdapter.enable();
}
}
這裡我們呼叫了mBluetoothAdapter.isEnabled(),避免重複開啟藍芽。
4、搜尋BLE裝置
開啟藍芽之後,我們終於可以搜尋BLE裝置了。這個部分需要注意的問題主要是搜尋時間的問題,因為搜尋BLE裝置是一個很費電的過程,所以搜尋的時間不應該設定太長。在開始搜尋的同時,我們應該設定一個Handler,然後呼叫Handler的postDelayed(Runnable r)方法去設定在搜尋一定時間後停止搜尋。
將搜尋BLE裝置的過程獨立寫成一個方法,這樣如果需要多次搜尋的話只需要再次呼叫此方法即可。另外應該設立一個標誌位mScanning,在調用搜索方法的時候應該判斷標誌位,如果現在正在搜尋就不應該再次進行搜尋。
public void scanLeDevice(boolean enable) {
if(enable) {
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
mScanning = false;
mBluetoothAdapter.stopLeScan(mLeScanCallback);
mBLEListActivity.setTextInfo();
}
}, SCAN_PERIOD);
mScanning = true;
mBluetoothAdapter.startLeScan(mLeScanCallback);
} else {
mScanning = false;
mBluetoothAdapter.stopLeScan(mLeScanCallback);
}
}
這段方法的核心主要是兩個方法:mBluetoothAdapter.startLeScan(mLeScanCallback)和
mBluetoothAdapter.stopLeScan(mLeScanCallback)。分別對應開始BLE裝置的搜尋和停止BLE裝置的搜尋。需要注意的是這兩個方法只針對於BLE裝置,對於普通的藍芽裝置,呼叫這個方法是搜尋不到的。
但是你可能要問了,我們怎麼才能得到搜尋的結果呢?請注意,在這兩個方法中都有一個共同的引數mLeScanCallback,這個mLeScanCallback就是我們獲取搜尋結果的載體。
讓我們一起來看看mLeScanCallback的定義
private BluetoothAdapter.LeScanCallback mLeScanCallback =
new BluetoothAdapter.LeScanCallback() {
@Override
public void onLeScan(final BluetoothDevice device, int rssi, byte[] scanRecord) {
if(!mBLEListActivity.mBLEArrayList.contains(device)) {
mBLEListActivity.addBluetoothItem(device);
mBLEListActivity.runOnUiThread(new Runnable() {
@Override
public void run() {
mBLEListActivity.mBLEAdapter.notifyDataSetChanged();
mBLEListActivity.mListView.setAdapter(
mBLEListActivity.mBLEAdapter);
}
});
}
}
};
可以看到mLeScanCallback實際上是一個BluetoothAdapter.LeScanCallback的例項。LeScanCallback中包含了一個回撥方法onLeScan(),我們通過重寫這個方法去進行一系列操作。
到這裡我們基本上就可以理解了,實際上BLE的搜尋的實現是通過回撥方法完成的,當檢測到BLE裝置的時候,就呼叫一次onLeScan()方法,然後我們應該在裡面去儲存搜尋到的device,比如放在一個List中。一般的處理方法是將搜尋到的device放在ListView中進行顯示,然後響應使用者的點選進行連線。這不屬於BLE開發的範疇,所以暫不贅述。
5、與指定BLE裝置進行通訊
通過第4步我們能夠搜尋到我們想要的device,現在應該開始通訊了。通訊的第一步就是和BLE裝置建立連線。安卓用BluetoothGatt類來管理這種連線,首先我們需要獲得一個BluetoothGatt例項,只需要呼叫BluetoothDevice的connectGatt()方法即可。
if(mBLEDevice != null) {
Log.d(TAG, "建立連線");
mBluetoothGatt = mBLEDevice.connectGatt(mBLEService, true, mGattCallback);
} else {
Log.e(TAG, "沒找到特定的BLE裝置,連線無法建立");
}
這裡有三個引數,重點說一下第三個引數mGattCallback,這時連線過程中最為重要的一個引數,其定義如下(此部分為安卓官方的程式碼)
// A service that interacts with the BLE device via the Android BLE API.
public class BluetoothLeService extends Service {
private final static String TAG = BluetoothLeService.class.getSimpleName();
private BluetoothManager mBluetoothManager;
private BluetoothAdapter mBluetoothAdapter;
private String mBluetoothDeviceAddress;
private BluetoothGatt mBluetoothGatt;
private int mConnectionState = STATE_DISCONNECTED;
private static final int STATE_DISCONNECTED = 0;
private static final int STATE_CONNECTING = 1;
private static final int STATE_CONNECTED = 2;
public final static String ACTION_GATT_CONNECTED =
"com.example.bluetooth.le.ACTION_GATT_CONNECTED";
public final static String ACTION_GATT_DISCONNECTED =
"com.example.bluetooth.le.ACTION_GATT_DISCONNECTED";
public final static String ACTION_GATT_SERVICES_DISCOVERED =
"com.example.bluetooth.le.ACTION_GATT_SERVICES_DISCOVERED";
public final static String ACTION_DATA_AVAILABLE =
"com.example.bluetooth.le.ACTION_DATA_AVAILABLE";
public final static String EXTRA_DATA =
"com.example.bluetooth.le.EXTRA_DATA";
public final static UUID UUID_HEART_RATE_MEASUREMENT =
UUID.fromString(SampleGattAttributes.HEART_RATE_MEASUREMENT);
// Various callback methods defined by the BLE API.
private final BluetoothGattCallback mGattCallback =
new BluetoothGattCallback() {
@Override
public void onConnectionStateChange(BluetoothGatt gatt, int status,
int newState) {
String intentAction;
if (newState == BluetoothProfile.STATE_CONNECTED) {
intentAction = ACTION_GATT_CONNECTED;
mConnectionState = STATE_CONNECTED;
broadcastUpdate(intentAction);
Log.i(TAG, "Connected to GATT server.");
Log.i(TAG, "Attempting to start service discovery:" +
mBluetoothGatt.discoverServices());
} else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
intentAction = ACTION_GATT_DISCONNECTED;
mConnectionState = STATE_DISCONNECTED;
Log.i(TAG, "Disconnected from GATT server.");
broadcastUpdate(intentAction);
}
}
@Override
// New services discovered
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
if (status == BluetoothGatt.GATT_SUCCESS) {
broadcastUpdate(ACTION_GATT_SERVICES_DISCOVERED);
} else {
Log.w(TAG, "onServicesDiscovered received: " + status);
}
}
@Override
// Result of a characteristic read operation
public void onCharacteristicRead(BluetoothGatt gatt,
BluetoothGattCharacteristic characteristic,
int status) {
if (status == BluetoothGatt.GATT_SUCCESS) {
broadcastUpdate(ACTION_DATA_AVAILABLE, characteristic);
}
}
...
};
...
}
程式碼比較繁雜,不需要仔細看。我們只需要知道mGattCallback是一個BluetoothGattCallback的例項,裡面有很多的回撥方法就夠了。下面我會說到一些比較重要的回撥方法,這些回撥方法主要是用於檢測通訊過程中狀態的改變的。
連線的過程中我們傳入了mGattCallback,然後mGattCallback中的回撥方法是需要我們自己去重寫的,在對應不同狀態的時候去進行不同的動作。比較重要的方法有以下幾個:
onConnectionStateChange()
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
super.onConnectionStateChange(gatt, status, newState);
if(newState == BluetoothProfile.STATE_CONNECTED) {
mBluetoothGatt = gatt;
if(mBluetoothGatt != null) {
mBluetoothGatt.discoverServices();
} else {
Log.d(TAG, "mBluetoothGatt為空");
}
} else if(newState == BluetoothProfile.STATE_DISCONNECTED) {
mActivity.runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(mActivity, "連線斷開,請檢查裝置", Toast.LENGTH_LONG).show();
}
});
}
}
這個方法主要檢測的是裝置的連線狀態的改變,每當裝置連線狀態發生改變的時候,即從連線到斷開或者從斷開到連線的時候都會呼叫此方法。我們應該判斷newState的值,然後在連線成功的時候去呼叫mBluetoothGatt.discoverServices()(之後我會說明這個方法的作用),在連線斷開的時候進行清理,並且告知使用者連線斷開了。
onServiceDiscovered()
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
super.onServicesDiscovered(gatt, status);
if(status == BluetoothGatt.GATT_SUCCESS) {
mActivity.runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(mActivity, "已連線", Toast.LENGTH_LONG).show();
}
});
try {
getBtgService();
getBtgCharacteristic();
mBLEAudioTrack.play();
if(mReadCharacteristic != null) {
mBluetoothGatt.setCharacteristicNotification(mReadCharacteristic, true);
}
}catch(Exception e) {
e.printStackTrace();
}
} else {
Log.e("TAG", "連線失敗");
}
}
剛才我們提到了在連線成功時候需要呼叫mBluetoothGatt.discoverServices(),為什麼呢?這就涉及到藍芽BLE通訊上的一個設計,藍芽的資料傳輸並不是一位一位傳過來的,而是通過Characteristic進行封裝後傳過來的,一個Characteristic中包裝了一組資料。在Characteristic上層,還有Service,封裝了多個Charateristic。在實際的通訊中,我們需要首先獲取BluetoothGattService,然後通過BluetoothGattService的例項去獲取BluetoothGattCharacteristic。
而在獲取BluetoothGattService之前,我們需要先獲取服務,這個獲取服務相當於是告訴藍芽裝置我要開始進行資料交換了,請你準備好,mBluetoothGatt.discoverServices()就是完成這個功能的。當找到了服務之後,安卓就會自動呼叫上面的onServicesDiscovered()方法,然後我們就可以在裡面去獲取BluetoothGattService和BluetoothGattCharacteristic。
怎麼獲取Service和Characteristic呢?
這裡用到了UUID,關於UUID的基本知識請參照維基百科-UUID
在Java中UUID通過java.util.UUID.fromString(String str)來產生的,在這一部分需要和BLE硬體相適應,確定BLE裝置上傳送資料和接收資料的Service和Characteristic的UUID號。以下是我所使用的BLE裝置的Service和Characteristic的獲取方式
首先用UUID號得到UUID物件
private static final String SERVICE_NAME = "0000FFF0-0000-1000-8000-00805F9B34FB";
private static final UUID SERVICE_UUID = UUID.fromString(SERVICE_NAME);
private static final String CHARC_NAME = "0000FFF6-0000-1000-8000-00805F9B34FB";
private static final UUID CHARC_UUID = UUID.fromString(CHARC_NAME);
獲取Service例項
public void getBtgService() {
mBluetoothGattService = mBluetoothGatt.getService(SERVICE_UUID);
Log.i(TAG, "get service"+mBluetoothGattService.toString());
}
獲取Characteristic例項
public void getBtgCharacteristic() {
mReadCharacteristic = mBluetoothGattService.getCharacteristic(CHARC_UUID);
mWriteCharacteristic = mBluetoothGattService.getCharacteristic(CHARC_UUID);
Log.i(TAG, "get characteristic"+mReadCharacteristic.toString());
List<BluetoothGattDescriptor> descriptors = mReadCharacteristic.getDescriptors();
for (BluetoothGattDescriptor bgp : descriptors) {
Log.i(TAG, "setCharacteristicNotification: " + bgp);
bgp.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
mBluetoothGatt.writeDescriptor(bgp);
}
}
在獲取mReadCharacteristic的時候需要注意,我們通過mReadCharacteristic.getDescriptors()獲取了它的描述符,然後對下面的所有描述符都設定了屬性,這樣做的原因是因為之後我們需要回調方法onCharacteristicChanged(),如果不這樣設定,那麼當讀資料成功後不會進入onCharacteristicChanged()這個回撥方法中,這樣我們就無法獲得之後的資料了。所以為了讓讀操作能夠一直持續,我們需要設定其Descriptor。
注意,在獲取了Service和Characteristic之後實際上我們就可以開始進行資料通訊了,我在這裡呼叫了方法
if(mReadCharacteristic != null) {
mBluetoothGatt.setCharacteristicNotification(mReadCharacteristic, true);
}
通過這個方法,現在就能監聽安卓裝置是否收到BLE裝置發來的資料,如果收到了發來的資料,會呼叫方法onCharacteristicRead(),下面介紹這個方法。
onCharacteristicRead()
public void onCharacteristicRead(BluetoothGatt gatt,
BluetoothGattCharacteristic characteristic,
int status) {
if (status == BluetoothGatt.GATT_SUCCESS) {
readCharacteristicValue(mReadCharacteristic);
mBluetoothGatt.setCharacteristicNotification(mReadCharacteristic, true);
}
}
onCharacteristicRead()這個方法會在接收資料的時候被呼叫,在這裡面我們可以去得到BLE裝置發過來的資料,通過characteristic.getValue()即可。這裡對資料的操作被封裝在readCharacteristicValue(mReadCharacteristic)中了。
注意,在這個方法中,一定要再次呼叫mBluetoothGatt.setCharacteristicNotification(mReadCharacteristic, true);這樣才能保證下一次接收到資料時又能進入此回撥方法。這樣就能保證連續地接收BLE裝置的資料了。
onCharacteristicWrite()
public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic,
int status) {
switch(status) {
case BluetoothGatt.GATT_SUCCESS:
Log.d(TAG, "write data success");
break;
case BluetoothGatt.GATT_FAILURE:
Log.d(TAG, "write data failed");
case BluetoothGatt.GATT_WRITE_NOT_PERMITTED:
Log.d(TAG, "write not permitted");
}
super.onCharacteristicWrite(gatt, characteristic, status);
}
如果需要向BLE裝置寫資料其實是與讀資料相似的處理,就不贅述了。
6、結束通訊時,釋放資源
這一部分比較簡單,只需要呼叫如下的close()方法即可
public void close() {
if (mBluetoothGatt == null) {
return;
}
mBluetoothGatt.close();
mBluetoothGatt = null;
}
結語
這篇部落格作為我部落格生涯的一個開始,以後我會在學習到新的知識的時候儘量多地去總結,會保持部落格的更新。