安卓低功耗藍牙開發

近幾日做了些安卓低功耗藍牙的項目,主要是用了北歐半導體公司的板子。不過對於安卓上位機來說,是哪家公司的板子,差別並不是很大。
剛開始對藍牙不是很瞭解,找了NordicSemiconductor的Android-nRF-Toolbox和谷歌自己的sample 的代碼研究了一番。
Nordic的代碼較爲龐大,內容也很豐富,包括了dfu升級服務,體溫服務,串口服務等等。而谷歌的相對要簡單些,僅僅讓使用者能夠看出藍牙發上來的數據,對於心率的服務還做了解析。兩者的共同點是將大部分的藍牙操作置於Service之中,而acticivty只需要綁定服務,並且註冊相應的廣播來接收各種信息即可。
因此,我對藍牙的操作進行了一些小小的總結。只需要基於以下幾個基本功能,就可以完成一個簡單的藍牙操作過程。

一、藍牙的初始化

1.判斷安卓設備是否支持ble

如果安卓設備不支持ble的話,基本就沒得玩了。不過現在的安卓手機普遍都支持,按照官方文檔的說法,如圖。
develop.android的截圖
只要是安卓4.3以上,即api18以上的設備基本都支持ble。但是不排除有些安卓設備是從4.3以下升級上來的,這些設備可能就不能使用低功耗藍牙了,但是這種情況還是比較少的。儘管如此,我們還是可以用

public boolean isBLESupported() {
        boolean flag = true;
             if(!mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
            flag = false;
        }
        return flag;
    }

來判斷以下。

2.獲取本地藍牙適配器

mBluetoothManager = (BluetoothManager)mContext.getSystemService(Context.BLUETOOTH_SERVICE);
            mBluetoothAdapter = mBluetoothManager.getAdapter();

只有在獲取本地藍牙適配器之後才能進行相應的藍牙操作。

3.判斷藍牙是否已經打開

儘管這不是必須的,但是有時候可能會使用到。

private boolean isBLEEnabled() {
        return mBluetoothAdapter != null && mBluetoothAdapter.isEnabled();
    }

4.開啓藍牙

開啓藍牙有兩種方式,一種是需要通過用戶同意纔打開,另一種是不經過用戶,直接打開。這兩種方式各有利弊。要根據情況來稍作判斷。

public void enableBle(boolean bShowDialog) {
        if (isBLESupported()){
            if (bShowDialog){
                //這種是需要彈出對話框讓用戶選擇是否打開的
                final Intent enableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
                mContext.startActivity(enableIntent);
            }else{
                //這種是不經過用戶,直接打開
                mBluetoothAdapter.enable();
            }
        }else {
            Log.v(TAG,"ble is not support");
        }
    }

5.關閉藍牙

mBluetoothAdapter.disable();

關閉的代碼較爲簡單。

二、掃描設備

掃描設備是相當耗能的,通常情況下,在掃描設備的時候不會進行其他有關藍牙的操作。按照官方文檔,我們掃描的時候還應當設定一個掃描時間,免得手機一直再掃描,消耗電量。
這裏寫圖片描述
掃描設備又有幾種方式,不同安卓系統之間的函數又有不同,5.0以上(含)的系統和5.0以下的系統,用的是不同的掃描函數,但是本質上差的不是很多,這裏只提供5.0以下的掃描函數,因爲,即使是5.0以上的系統也還可以用5.0以下的函數進行掃描。

1.準備一個掃描回調

爲什麼要弄一個這種東西呢?這裏涉及到了java裏的一種回調機制,簡單的說
比方說:有一天,我去麪包店買麪包,可是店員告訴我麪包賣完了,我留了個電話給他,讓他有面包的時候打電話通知我,我再過來買。這樣做的好處是,我可以先去幹別的事情而不用爲了這點麪包苦苦等待。
運用到藍牙中就是,我開啓了掃描,在掃描的這段時間裏我可以先處理一些和藍牙有關或無關的事情。不會讓掃描這件事一直阻塞着我的進程。

private BluetoothAdapter.LeScanCallback mLeScanCallback =
            new BluetoothAdapter.LeScanCallback() {

                @Override
                public void onLeScan(final BluetoothDevice device, int rssi, byte[] scanRecord) {
                    //在這裏可以處理掃描到設備的時候想要做的事情
                    //你可以發送一個掃描到設備的廣播,或者發個消息,等等
                }
            };

2.掃描設備

掃描呢,就得注意一個東西,就是掃描時間,我們可以弄一個延時函數Handler類的postDelayed並用上Runnable就可以達到延時效果,開啓掃描後延時一定的毫秒數停止掃描。
另外呢,掃描的時候可以指定相應的服務的uuid進行掃描,也可以不管三七二十一,只要是低功耗藍牙設備都給掃了。只需要調用不同的startLeScan就可以。

public void scanLeDevice(UUID[] serviceUuids, long scanPeriod){
        // Stops scanning after a pre-defined scan period.
        if (isBLEEnabled()){
            mHandler.postDelayed(new Runnable() {
                @Override
                public void run() {
                    mScanning = false;
                    mBluetoothAdapter.stopLeScan(mLeScanCallback);
                }
            }, scanPeriod);//scanPeriod是毫秒,也就是1秒 = 1000

            //mBluetoothAdapter.stopLeScan(mLeScanCallback);若調用這條函數,即可掃描全部ble設備
            mBluetoothAdapter.startLeScan(serviceUuids, mLeScanCallback);

            //這裏我發送了個開始掃描的廣播,可發可不發
            broadcastUpdate(BleBroadcastAction.ACTION_DEVICE_DISCOVERING);
        }
    }

3.停止掃描

停止掃描也比較簡單

mBluetoothAdapter.stopLeScan(mLeScanCallback);

三、連接設備

  1. 準備一個BluetoothGattCallback——一個基於gatt服務的回調
    有是一個回調,java中因爲沒有函數指針,所以回調通常會藉助接口或者抽象類來完成。這個回調包含了幾個功能:
    1. 接收手機與ble設備連接狀態改變的信號,對應函數onConnectionStateChange(BluetoothGatt gatt, int status,
    int newState)
    2. 發現所連接的設備具有的服務。對應函數onServicesDiscovered(BluetoothGatt gatt, int status)
    3. 讀取所連接服務對應的數據,對應函數onCharacteristicRead(BluetoothGatt gatt,
    BluetoothGattCharacteristic characteristic,
    int status)
    4. 得到數據改變的通知,並獲取改變的結果,對應函數onCharacteristicChanged(BluetoothGatt gatt,
    BluetoothGattCharacteristic characteristic)
    注意:這裏的涉及到一個藍牙連接後的順序問題,必須要先連接設備,才能發現設備相應的服務。發現服務後,必須要先連接服務,才能獲取服務對應的數據,和發送給服務對應的數據。
private final BluetoothGattCallback mGattCallback = new BluetoothGattCallback() {
        @Override
        public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
            String intentAction;
            if (newState == BluetoothProfile.STATE_CONNECTED) {
                intentAction = BleBroadcastAction.ACTION_GATT_CONNECTED;
                mConnectionState = STATE_CONNECTED;
                broadcastUpdate(intentAction);
                Log.i(TAG, "Connected to GATT server.");
                // Attempts to discover services after successful connection.
                Log.i(TAG, "Attempting to start service discovery:" +
                        mBluetoothGatt.discoverServices());

            } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
                intentAction = BleBroadcastAction.ACTION_GATT_DISCONNECTED;
                mConnectionState = STATE_DISCONNECTED;
                Log.i(TAG, "Disconnected from GATT server.");
                broadcastUpdate(intentAction);
                close();
            }
        }

        @Override
        public void onServicesDiscovered(BluetoothGatt gatt, int status) {
            if (status == BluetoothGatt.GATT_SUCCESS) {
                broadcastUpdate(BleBroadcastAction.ACTION_GATT_SERVICES_DISCOVERED);
                connectCharacteristic(mCharacteristicUuidArr);
            } else {
                Log.w(TAG, "onServicesDiscovered received: " + status);
            }
        }

        @Override
        public void onCharacteristicRead(BluetoothGatt gatt,
                                         BluetoothGattCharacteristic characteristic,
                                         int status) {
            if (status == BluetoothGatt.GATT_SUCCESS) {
                broadcastUpdate(characteristic.getUuid().toString(), characteristic);
            }
        }

        @Override
        public void onCharacteristicChanged(BluetoothGatt gatt,
                                            BluetoothGattCharacteristic characteristic) {
            broadcastUpdate(characteristic.getUuid().toString(), characteristic);
        }
    };

這上面是谷歌的代碼,我只做了略微修改。總的來說,只要調用了哪個回調函數都可以發個廣播或者消息,讓其他activity或者service去處理。

2.連接
連接ble設備,第一次連接是比較簡單的,調用

final BluetoothDevice device = mBluetoothAdapter.getRemoteDevice(address);//address是mac字符串
mBluetoothGatt = device.connectGatt(mContext, false, mGattCallback);

即可開始連接。
但是這裏又涉及到一個比較隱晦坑爹的點,就再次連接同一個設備的情況。我一開始在做這個部分的時候沒有注意到連接同一個設備的情況。結果導致我待會斷開連接之後,設備還是一直保持通信。原因是安卓手機無法識別他們是同一個設備,建立了新的連接,把設備當做新設備連接來處理。我後來斷開的設備只是斷掉了安卓認爲的新設備。
這裏呢,可以先判斷是否連接過,如果連接過,可以調用connect();方法重新連接。

if (mBluetoothDeviceAddress != null && address.equals(mBluetoothDeviceAddress)
                && mBluetoothGatt != null) {
            Log.d(TAG, "Trying to use an existing mBluetoothGatt for connection.");
            if (mBluetoothGatt.connect()) {
                mConnectionState = STATE_CONNECTING;
                return true;
            } else {
                return false;
            }
        }

但是,這麼做呢,經過試驗是有些缺陷的,連接時間會大大拉長。可以採取nordic的做法。

if (mConnected)
            return;

        if (mBluetoothGatt != null) {
            Logger.d(mLogSession, "gatt.close()");
            mBluetoothGatt.close();
            mBluetoothGatt = null;
        }

        final boolean autoConnect = shouldAutoConnect();
        mUserDisconnected = !autoConnect; // We will receive Linkloss events only when the device is connected with autoConnect=true
        Logger.v(mLogSession, "Connecting...");
        Logger.d(mLogSession, "gatt = device.connectGatt(autoConnect = " + autoConnect + ")");
        mBluetoothGatt = device.connectGatt(mContext, autoConnect, getGattCallback());

這樣做,可以加快連接速度。

3.獲取服務

連接完成後,需調用BluetoothGatt的discoverServices()函數去查詢當前連接的設備所具有的服務,當安卓設備發現所連接的設備具有服務時,會調用我們剛纔重寫的回調函數onServicesDiscovered(BluetoothGatt gatt, int status)。我們便可調用mBluetoothGatt.getServices()去獲取設備擁有的服務。並進行操作,獲取他們的characteristic。

4.斷開連接

有連接,則有斷開連接。

public void disconnect() {
        if (mBluetoothAdapter == null || mBluetoothGatt == null) {
            Log.w(TAG, "BluetoothAdapter not initialized");
            return;
        }
        mConnectionState = STATE_DISCONNECTING;
        broadcastUpdate(BleBroadcastAction.ACTION_GATT_DISCONNECTING);
        mBluetoothGatt.disconnect();
    }

程序結束後呢,還得釋放資源,怎麼說呢像c++的delete或者c語言free那樣吧。調用BluetoothGatt的close函數,關閉一波
這裏寫圖片描述

public void close() {
    if (mBluetoothGatt == null) {
        return;
    }
    mBluetoothGatt.close();
    mBluetoothGatt = null;
}

四、數據收發

1.接收數據

收數據呢,安卓也是採取了回調的方式,一收到數據,就立馬通知用戶進行處理,這樣做的好處呢,我們可以及時處理收到的數據,不會像stm32 串口處理數據一樣,即有可能因爲定時器來不及處理,數據就被覆蓋。筆者寫單片機的時候還是被坑了一波爹的。
當然,設備那麼多個服務,我們也不是要全部接受數據,安卓也沒有那麼勤快,什麼數據都自動幫我們接收,想要接受什麼數據,我們得先通知他一聲。

private BluetoothGatt mBluetoothGatt;
BluetoothGattCharacteristic characteristic;
boolean enabled;
...
mBluetoothGatt.setCharacteristicNotification(characteristic, enabled);
...

調用setCharacteristicNotification(characteristic,enabled)通知安卓我要接收那個服務的數據,要接收就true,不接收就false。
這裏又涉及到了一個東西,就是characteristic。
characteristic可能更貼近服務的概念吧
這裏寫圖片描述
Service是characteristic的集合,也就是說Service更像是一個組名,不直接提供數據交換,而Descriptor是用來描述characteristic的,比方說描述某個characteristic是否可讀,是否可寫。(這裏有待考證,我還沒有親自試驗過)。所以用戶使用最多應該還是characteristic。基本上光使用characteristic就可以換成數據交互。
調用readCharacteristic可以讀取數據,並進入onCharacteristicRead的回調中去,解析讀到的數據。

2.發送數據

發數據呢,這是一個比較奇怪的方式,不像串口一樣,懟着哪個通道就把數據給丟出去,方便快捷。按說是需要獲取到你要寫數據的characteristic,然後調用setValue函數,把你想要發的數據寫進characteristic,然後再發出去。

private boolean writeCharacteristic(final BluetoothGattCharacteristic characteristic) {
        final BluetoothGatt gatt = mBluetoothGatt;
        if (gatt == null || characteristic == null)
            return false;

        // Check characteristic property
        final int properties = characteristic.getProperties();
        if ((properties & (BluetoothGattCharacteristic.PROPERTY_WRITE | BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE)) == 0)
            return false;

        return gatt.writeCharacteristic(characteristic);
    }

    public boolean writeBle(byte[] data, UUID uuid) {
        BluetoothGattCharacteristic targetCharacteristic = null;
        for (BluetoothGattService bluetoothGattService : getSupportedGattServices()) {
            for (BluetoothGattCharacteristic bluetoothGattCharacteristic : bluetoothGattService.getCharacteristics()) {
                if (bluetoothGattCharacteristic.getUuid().toString().equals(uuid.toString())) {
                    targetCharacteristic = bluetoothGattCharacteristic;
                }
            }
        }
        if (targetCharacteristic == null){
            return false;
        }else {
            targetCharacteristic.setValue(data);
            return writeCharacteristic(targetCharacteristic);
        }
//        return writeCharacteristic()
    }

大體就先說這麼多東西吧,筆者也只是瞭解點皮毛,歡迎討論。
參考鏈接:https://developer.android.com/guide/topics/connectivity/bluetooth-le.html
代碼資源:
https://github.com/will4906/AndroidBleDemo

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章