1. 程式人生 > >安卓藍芽BLE裝置開發

安卓藍芽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;
}

結語

 這篇部落格作為我部落格生涯的一個開始,以後我會在學習到新的知識的時候儘量多地去總結,會保持部落格的更新。