使用RxJava幫助低功耗藍芽(BLE)進行通訊

Salama發表於2016-11-27

  Android中的藍芽開發有兩種,一種是傳統藍芽,另一種是低功耗藍芽,這兩者完全不一樣,開發前你得弄清你需要開發的是哪一種,用傳統藍芽的方式進行低功耗藍芽的開發你可能都沒法使你的裝置連上藍芽,不要問我為什麼知道,說多了都是淚 (TT)
  低功耗藍芽(Bluetooth Low Energy)簡稱BLE,常見於各種運動手環、電子血壓計等健康管理裝置,Android4.3(API級別18)中引入了面向低功耗藍芽的API支援。也就是說開發的前提是手機裝置支援BLE並且系統是Android4.3以上,與手機通訊的藍芽裝置是低功耗藍芽。
  如果你看了官方文件上的示例,你會發現使用了Handler、和廣播進行非同步通訊,之前我在公司專案中藍芽功能也是這麼寫的,現在有了RxJava,我們可以寫的更優(zhaung)雅(bi)些,所以就有了這篇文章。

本文的程式碼地址:RxBleDemo
關於RxJava,如果你還不瞭解,可以看給 Android 開發者的 RxJava 詳解
關於低功耗藍芽的開發你可以看官方文件或者直接閱讀本文

整體思路

  假定你已經有了一部支援BLE的手機和一個可以通訊的低功耗藍芽模組,那麼就可以按下面的步驟開搞了:

  • 藍芽許可權
  • 設定並開啟手機藍芽
  • 查詢藍芽裝置
  • 連線藍芽服務
  • 進行藍芽通訊

詳細步驟

設定藍芽許可權

  在應用中的manifest檔案中宣告藍芽許可權

<uses-permission android:name="android.permission.BLUETOOTH"/>  
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>複製程式碼

  另外我們可以通過PackageManager.hasSystemFeature()方法來判斷當前的手機裝置是否支援BLE。

設定並開啟手機藍芽裝置

  首先通過BluetoothManager獲取手機中唯一的的藍芽介面卡(藍芽傳送接收器)BluetoothAdapter物件。再通過BluetoothAdapter開啟手機藍芽裝置,程式碼如下:

public void initBle(Context context) {
        this.mContext = context.getApplicationContext();
        BluetoothManager bluetoothManager =
                (BluetoothManager) this.mContext.getSystemService(Context.BLUETOOTH_SERVICE);
        mBleAdapter = bluetoothManager.getAdapter();
        if (mBleAdapter != null) {
            mBleAdapter.enable();//不彈對話方塊直接開啟藍芽
        }
    }複製程式碼

  值得一提的是我將藍芽功能簡單的封裝成了一個工具類,用到了靜態內部類的單例模式,為了防止記憶體洩露,在初始化藍芽的時候傳入了Application的Context物件的引用

查詢藍芽裝置

  定義了一個scanBleDevices(boolean enable)方法用於開啟和關閉掃描。這裡使用了BluetoothAdapter.startLeScan(LeScanCallback)方法開啟掃描,需要傳入一個掃描回撥:

private BluetoothAdapter.LeScanCallback mBleScanCallback = new BluetoothAdapter.LeScanCallback() {
    @Override
    public void onLeScan(BluetoothDevice bleDevice, int rssi, byte[] scanRecord) {
        if (mIsScanning) {
            Log.d(TAG, "onLeScan:找到裝置" + bleDevice.getName());
            if (mTargetDeviceName.equals(bleDevice.getName())) {
                connectDevice(bleDevice);//連線藍芽裝置
            }
        } else {
            Log.d(TAG, "onLeScan: 停止掃描");
        }
    }
};複製程式碼

  這個回撥也用於關閉藍芽掃描的方法BluetoothAdapter.stopLeScan(LeScanCallback),所以定義了一個布林型變數mIsScanning判斷藍芽掃描的開啟和關閉。
  說了這麼多,我們的RxJava好像還沒登場。在掃描過程中,我們需要限定藍芽的掃描的超時時間,不能讓手機這麼一直掃描,所以我們可以通過RxJava中的timer延時一段時間後執行停止掃描:

Observable.timer(SCAN_PERIOD, TimeUnit.MILLISECONDS).subscribe(new Action1<Long>() {
    @Override
    public void call(Long aLong) {
        mIsScanning = false;
        mBleAdapter.stopLeScan(mBleScanCallback);
    }
});複製程式碼

連線藍芽服務並接收資料

  當查詢到名為mTargetDeviceName的目標藍芽裝置,就可以通過下面的方法去連線,準確的說是連線裝置上的GATT服務:

private void connectDevice(BluetoothDevice bleDevice) {
    scanBleDevices(false);
    mBleGatt = bleDevice.connectGatt(mContext, true, new BleGattCallback());
    mBleGatt.connect();
    Log.d(TAG, "開始連線裝置:" + mBleGatt.getDevice().getName());
}複製程式碼

  連線藍芽裝置前需要關閉藍芽掃描。bleDevice.connectGatt(mContext, true, new BleGattCallback())方法返回了一個藍芽GATT物件,這個方法中的true代表自動連線(藍芽模組斷電重啟後,可以重新連線它),呼叫GATT的connect()方法進行連線,連線過程中會執行傳入的回撥BleGattCallback,這個回撥繼承了BluetoothGattCallback並重寫了以下三個方法:

一、onConnectionStateChange

@Override
public void onConnectionStateChange(BluetoothGatt bleGatt, int status, int newState) {
    super.onConnectionStateChange(bleGatt, status, newState);
    Log.d(TAG, "onConnectionStateChange: 連線狀態: " + newState);
    if (newState == BluetoothGatt.STATE_CONNECTED) {//連線成功
        Log.d(TAG, "onConnectionStateChange: 裝置連線");
        bleGatt.discoverServices();//搜尋服務
    } else if (newState == BluetoothGatt.STATE_DISCONNECTED) {//斷開連線
        Log.d(TAG, "onConnectionStateChange: 裝置斷開");
    }
}複製程式碼

這個方法監聽連線狀態的改變,連線狀態有四個值:

描述
STATE_CONNECTED 已連線
STATE_CONNECTING 正在連線
STATE_DISCONNECTED 斷開連線
STATE_DISCONNECTING 正在斷開連線

  當裝置已連線時,需要通過discoverServices()查詢GATT服務,查詢服務過程中會執行重寫的第二個方法onServicesDiscovered

二、onServicesDiscovered

  可以在此方法中獲取GATT的服務列表,這個服務列表中的每一個服務對應著一個BluetoothGattCharacteristic(用於通訊)列表,需要對這個列表通過UUID過濾出我們想要的BluetoothGattCharacteristic,然後就可以拿這個BluetoothGattCharacteristic進行通訊了。

關於 UUID
通用唯一識別符號 (UUID) 是用於唯一標識資訊的字串 ID 的 128 位標準化格式。 UUID 的特點是其足夠龐大,因此你可以選擇任意隨機值而不會發生衝突。 在此示例中,它被用於唯一標識應用的藍芽服務。 要獲取 UUID 以用於你的應用,你可以使用網路上的眾多隨機 UUID 生成器之一,然後使用 fromString(String) 初始化一個 UUID。不必過多糾結於UUID。

  上面這個過程如果用傳統的方式編寫的話,那就是列表遍歷巢狀列表遍歷再巢狀if判斷,下次再看的話就是一堆迷之縮排,還好可以用RxJava寫出鏈式的結構:

@Override
public void onServicesDiscovered(final BluetoothGatt bleGatt, int status) {
    Log.d(TAG, "onServicesDiscovered: 查詢服務: " + bleGatt.getServices().size());
    List<BluetoothGattService> serviceList = bleGatt.getServices();
    Observable.from(serviceList)
            .flatMap(new Func1<BluetoothGattService, Observable<BluetoothGattCharacteristic>>() {
                @Override
                public Observable<BluetoothGattCharacteristic> call(BluetoothGattService bleGattService) {
                    return Observable.from(bleGattService.getCharacteristics());
                }
            })
            .filter(new Func1<BluetoothGattCharacteristic, Boolean>() {
                @Override
                public Boolean call(BluetoothGattCharacteristic bleGattChar) {
                    return bleGattChar.getUuid().toString().equals(UUID);
                }
            })
            .subscribe(new Action1<BluetoothGattCharacteristic>() {
                @Override
                public void call(BluetoothGattCharacteristic bleGattChar) {
                    bleGatt.setCharacteristicNotification(bleGattChar, true);//設定開啟接收藍芽資料
                    mBleGattChar = bleGattChar;
                }
            });
}複製程式碼

三、onCharacteristicChanged

  此方法用於接收藍芽模組傳送過來的資料,它是非同步的,可以用RxJava方便的切換到Android主執行緒:

@Override
public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
    Log.d(TAG, "onCharacteristicChanged");
    String receiveData = new String(characteristic.getValue());
    Log.d(TAG, "收到藍芽發來資料:" + receiveData);
    Observable.just(receiveData)
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe(new Action1<String>() {
                @Override
                public void call(String receiveData) {
                    //處理receiveData
                }
        });
}複製程式碼

  前面提到了整個藍芽功能是一個工具類,那麼我們怎麼在我們想要的地方(Activity)接收到這個receiveData呢?也許你會說可以在這裡寫一個介面回撥啊,是的沒問題。在瞭解到可以通過RxJava實現EventBus事件匯流排後,我想可以寫一個簡單的RxBus在這裡發射資料,在需要的地方訂閱並接受資料。

關於RxBus,你可以先看implementing-an-event-bus-with-rxjava-rxbus,然後可以看state-propagation-in-android-with-rxjava-subjects(需要科學上網)。此外,國內也有很多相關文章。

先定義一個Subject,它既是觀察者又是被觀察者

private Subject<String, String> mBus = new SerializedSubject<>(PublishSubject.<String>create());複製程式碼

然後在處理receiveData的地方發射資料

mBus.onNext(receiveData);複製程式碼

再定義一個方法用於接收資料

public Observable<String> receiveData() {
    return mBus;
}複製程式碼

最後在需要接收資料的地方訂閱

mRxBle.receiveData().subscribe(new Action1<String>() {
    @Override
    public void call(String receiveData) {
        sendTv.setText(mStringBuffer.append(receiveData).append("\n"));
    }
});複製程式碼

向藍芽裝置傳送資料

  通訊不僅僅是接收資料,還需要傳送資料,這個實現起來很簡單,只要使用我們之前拿到的BluetoothGattCharacteristic物件以及BluetoothGatt物件進行相關方法的呼叫就行,在專案中由於需要對資料進行延時傳送,所以也用到了timer

Observable.timer(time, TimeUnit.MILLISECONDS)
        .subscribe(new Action1<Long>() {
            @Override
            public void call(Long l) {
                if (mBleGatt != null && mBleGattChar != null) {
                    mBleGattChar.setValue(data);//設定資料
                    boolean isSend = mBleGatt.writeCharacteristic(mBleGattChar);//寫入(傳送)資料
                    Log.d(TAG, "傳送:" + (isSend ? "成功" : "失敗"));
                }
            }
        });複製程式碼

  RxJava的強大之處在於他有各種各樣的操作符,可以對釋出的資料來源進行各種各樣的處理,實際專案中有很多應用的場景。

最後

  我們寫一個簡單的demo進行測試一下,效果如下:

  • Android端傳送Hello,Ble給藍芽模組,接受藍芽模組發過來的Hello,Android!


    使用RxJava幫助低功耗藍芽(BLE)進行通訊
  • 使用除錯助手除錯藍芽模組,接受Android端傳送過來的Hello,Ble,向Android端傳送Hello,Android!


    使用RxJava幫助低功耗藍芽(BLE)進行通訊

本文的所有程式碼地址:RxBleDemo
本文同步於我的個人部落格

相關文章