在本系列之前的文章中,我們瞭解了 Bluetooth LE(低功耗藍芽,後文簡稱為BLE)的一些背景並且構建了一個簡單的Activity/Service模式的藍芽低功耗框架。在今天的文章裡,我們將更深入的探討BLE的技術細節,並且實現BLE下的“裝置發現”功能。
發現裝置簡單的說,是在藍芽可見範圍內搜尋可用裝置的過程。為了避免一開始就因為缺乏許可權而無法實現搜尋,首先我們需要在AndroidManifest中新增必要的許可權。我們所需要新增的許可權有android.permission.BLUETOOTH
以及android.permission.BLUETOOTH_ADMIN
。其中第一個許可權是 Android使用藍芽所必要的許可權,而第二個則是一些藍芽附加功能的許可權,比如本次討論的發現裝置功能。
在我們開始一頭鑽入程式碼之前,有必要解釋一下本文中的BleService
是以狀態機的形式運作的。BleService
可以在不同的狀態下執行不同的任務,所以我們從第一個狀態——SCANNING
開始切入。BleService
會在收到一條名為MSG_START_SCAN
的訊息後進入這個狀態。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
; html-script: false ] private static class IncomingHandler extends Handler { @Override public void handleMessage(Message msg) { BleService service = mService.get(); if (service != null) { switch (msg.what) { . . . case MSG_START_SCAN: service.startScan(); Log.d(TAG, "Start Scan"); break; default: super.handleMessage(msg); } } } } |
然後是開始搜尋的startScan()
函式程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
; html-script: false ] public class BleService extends Service implements BluetoothAdapter.LeScanCallback { private final Map mDevices = new HashMap(); public enum State { UNKNOWN, IDLE, SCANNING, BLUETOOTH_OFF, CONNECTING, CONNECTED, DISCONNECTING } private BluetoothAdapter mBluetooth = null; private State mState = State.UNKNOWN; . . . private void startScan() { mDevices.clear(); setState(State.SCANNING); if (mBluetooth == null) { BluetoothManager bluetoothMgr = (BluetoothManager) getSystemService(BLUETOOTH_SERVICE); mBluetooth = bluetoothMgr.getAdapter(); } if (mBluetooth == null || !mBluetooth.isEnabled()) { setState(State.BLUETOOTH_OFF); } else { mHandler.postDelayed(new Runnable() { @Override public void run() { if (mState == State.SCANNING) { mBluetooth.stopLeScan( BleService.this); setState(State.IDLE); } } }, SCAN_PERIOD); mBluetooth.startLeScan(this); } } } |
從程式碼中我們不難發現:首先,我們確認了藍芽服務是否已經開啟,如果檢測到使用者關閉了藍芽服務,那麼就需要去提示使用者開啟。這一過程的實現非常簡單,只要先獲取Android藍芽系統服務BluetoothService
的例項物件,然後從這個物件中我們又可以獲取到代表藍芽射頻的BluetoothAdapter
例項。注意這裡需要做一下非空判斷,接著可以通過這個Adapter
例項的isEnabled()
函式來判斷藍芽是否開啟並且處於可用狀態了。如果服務是不可用狀態,那麼我們需要定義一個恰當的狀態,並且通知給所有監聽了服務的客戶端,以便於進一步的處理(本文中就是我們的 Activity)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
; html-script: false ] public class BleActivity extends Activity { private final int ENABLE_BT = 1; . . . private void enableBluetooth() { Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE); startActivityForResult(enableBtIntent, ENABLE_BT); } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { if(requestCode == ENABLE_BT) { if(resultCode == RESULT_OK) { //Bluetooth connected, we may continue startScan(); } else { //The user has elected not to turn on //Bluetooth. There's nothing we can do //without it, so let's finish(). finish(); } } else { super.onActivityResult(requestCode, resultCode, data); } } private void startScan() { mRefreshItem.setEnabled(false); mDeviceList.setDevices(this, null); mDeviceList.setScanning(true); Message msg = Message.obtain(null, BleService.MSG_START_SCAN); if (msg != null) { try { mService.send(msg); } catch (RemoteException e) { Log.w(TAG, "Lost connection to service", e); unbindService(mConnection); } } } } |
服務端收到訊息後,為了提醒使用者開啟藍芽,Android系統專門為了這種情況提供了API。我們選擇呼叫這一系統介面,從而保證在不同機型上有較好的原生使用者體驗。當然,我們也可以通過程式碼直接去開啟藍芽,但是更推薦的做法是主動去提示使用者開啟。這樣做帶來的另一個好處是非常簡單的程式碼實現,我們只需要喚起一個特定的Activity
去提示使用者操作,而這個Intent Action
已經在BluetoothAdapter
中定義好了(參考上面程式碼的7-9行)。當使用者操作完成後,我們就會在之前Activity
的onActivityResult()
方法中,收到處理完成後的訊息。
不難發現,至此我們還沒有做任何BLE特有的步驟,不過這一些步驟均是使用藍芽服務所必要的。
之後就是掃描支援BLE的裝置了。由於Android為此在BluetoothAdapter
中封裝了一個名為startLeScan()
的方法,而該方法就是用來掃描裝置的!所以我們只需要簡單的呼叫該方法就可以開始掃描裝置。另外,這一方法需要傳入一個BluetoothAdapter.LeScanCallback
例項來接受掃描中各種狀態的回撥。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
; html-script: false ] public class BleService extends Service implements BluetoothAdapter.LeScanCallback . . . private void startScan() { mDevices.clear(); setState(State.SCANNING); if (mBluetooth == null) { BluetoothManager bluetoothMgr = (BluetoothManager) getSystemService(BLUETOOTH_SERVICE); mBluetooth = bluetoothMgr.getAdapter(); } if (mBluetooth == null || !mBluetooth.isEnabled()) { setState(State.BLUETOOTH_OFF); } else { mHandler.postDelayed(new Runnable() { @Override public void run() { if (mState == State.SCANNING) { mBluetooth.stopLeScan( BleService.this); setState(State.IDLE); } } }, SCAN_PERIOD); mBluetooth.startLeScan(this); } } @Override public void onLeScan(final BluetoothDevice device, int rssi, byte[] scanRecord) { if (device != null && !mDevices.containsValue(device) && device.getName() != null && device.getName().equals("SensorTag")) { mDevices.put(device.getAddress(), device); Message msg = Message.obtain(null, MSG_DEVICE_FOUND); if (msg != null) { Bundle bundle = new Bundle(); String[] addresses = mDevices.keySet() .toArray(new String[mDevices.size()]); bundle.putStringArray(KEY_MAC_ADDRESSES, addresses); msg.setData(bundle); sendMessage(msg); } Log.d(TAG, "Added " + device.getName() + ": " + device.getAddress()); } } } |
切記,startLeScan()
(注:原文說是onStartLeScan()
,不過Android中並沒有這個函式,從上下文意思看應該指startleScan()
)僅僅只是發起了搜尋過程,所以我們必須要記得去停止搜尋。在生產使用中,基於不同的需求,一般儘量在找到裝置後儘快停止搜尋,不過在本文的例子中,我們會通過postDelayed()
去定時呼叫stopLeScan()
來停止服務。
在搜尋過程中,每次藍芽Adapter
收到任何來自BLE裝置的廣播資訊都會呼叫BluetoothAdapter.LeScanCallback
中的onLeScan()
回撥。由於在廣播模式下的BLE裝置會每秒傳送10條廣播資訊,因此在搜尋過程中我們要仔細過濾這些冗餘資訊,保證程式只處理新裝置發來的訊息。為了達到這個目的,這裡通過已發現裝置的MAC地址(使用MAC地址在後續會帶來一些方便)在onLeScan()
中去重,然後把資訊儲存到一個Map中去。
除了過濾冗餘,我們也需要過濾掉那些我們並不關心的裝置。通常我們會通過裝置的一些特徵資訊來篩選(後續文章中會詳細講述),不過 SensorTag documentation 建議對於基於 SensorTag 開發的裝置我們只需要簡單的去匹配裝置名為“SensorTag”的裝置就可以,本文就選擇了這個方式。
每當我們發現一個符合條件的新裝置,我們便把這個裝置新增到Map中去,同時也把所有已發現裝置的MAC地址打包成一個String
陣列通過MSG_DEVICE_FOUND
訊息傳送給Activity
。
值得注意的是雖然我們服務中的操作都是在UI執行緒中執行的,但我們並不需要去擔心這會導致UI執行緒的阻塞。啟動BLE搜尋的呼叫是非同步執行的,並且會啟用一個後臺Task來回撥onLeScan()
。因此,只要我們保證沒有在onLeScan()
中進行計算密集型的任務,那麼是不需要擔心這個後臺Task會帶來任何阻塞的問題。
本文中的Activity
也是以狀態機的形式來執行,該Activity
會根據BleService
的狀態去重新整理改變UI。上方的重新整理選單就是根據BleService
是否處於SCANNING
狀態來切換可用/不可用狀態,同時該狀態也會使Activity
切換到展示已發現裝置列表的fragment
。 無論何時Activity
一收到MSG_DEVICE_FOUND
訊息就會去重新整理已發現裝置列表的介面。由於這和BLE並無太大關係,這部分UI重新整理的程式碼文中就不展開說明了,有興趣的同學可以點選 這裡 來訪問詳細程式碼。
現在我們已經可以通過這個demo來發現周圍處於廣播模式的BLE裝置,並且在列表中看到這些裝置。
至此,我們完成了基本的“發現裝置”功能,之後我們要做的是去連線上其中一個我們發現的裝置,這部分會在下一篇文章中講述。
本文的原始碼可以在這裡下載。