Android 手機與BLE設備的交互

前言

最近一直在思考一個問題,如何寫文章?即內容高質量又通俗易懂,讓新手既明白其中蘊含的真理又能輕松跑起第一個程序,同時也能讓高手溫故知新,如獲新歡。經過長時間的思索,最終定位為,內容高質量,描述簡潔,思路清晰,對讀者負責任的文章。初出茅廬,不會高手的底層功力,也不會段子手的套路人心,但,堅持做自己,盡自己所能,為人民服務。

BLE的一些關鍵概念

在Android應用層開發BLE,不懂一些理論和協議也沒關系,照樣可以上手開發。本著知其然知其所以然,下面知識點的理解,能夠有力支撐使用Android API。

藍牙類別

低功耗藍牙是不能兼容經典藍牙的,需要兼容,只能選擇雙模藍牙。

  • 低功耗藍牙:字如其名,第一特點就是低功耗,一個紐扣電池可以支持其運行數月至數年,至于怎么實現低功耗,看下文。小體積,低成本,在某寶上的價格有提供郵票體積大小,價格三四塊前的藍牙模塊,可以想象,廠商批發價格會更低。應用場景廣,可以想想,現在的智能家居,智能音箱,智能手表等等物聯網設備,大多數通過BLE進行配網和數據交互。
  • 經典藍牙:經典藍牙,泛指藍牙4.0以下的都是經典藍牙,藍牙4.0以上的,你還懷念通過藍牙讓音箱播放手機的音樂么?經典藍牙常用在語音、音樂等較高數據量傳輸的應用場景上。
  • 雙模藍牙:即在藍牙模塊中兼容BLE和BT.

Android 4.3及更高版本,Android 藍牙堆棧可提供實現藍牙低功耗 (BLE) 的功能,在 Android 8.0 中,原生藍牙堆棧完全符合藍牙 5 的要求。也就是說在Android 4.3以上,我們可以通過Android 原生API和藍牙設備交互。

GAP(Generic Access Profile)

GAP用來控制藍牙設備的廣播和連接。GAP可以使藍牙設備被其他藍牙設備發現,并決定是否可以被連接。GAP協議將藍牙設備分為中心設備和外圍設備。

  • 中心設備功能比強大,用來連接外圍設備,處理數據等。例如手機。
  • 外圍設備一般指非常小和低功耗的設備,用來提供數據,連接功能相對較強大的中心設備。例如體溫計,小米手環等。

外圍設備通過廣播數據掃描回復兩種方式之一讓中心設備發現,然后進行連接,從而達到進行數據交互的前提條件。為了達到低功耗,外圍設備并不是一直廣播,會設定一個廣播間隔,每個廣播間隔中,它會重新發送自己的廣播數據。廣播間隔越長,越省電,同時也不太容易掃描到。

在Android開發中,常通過藍牙MAC進行連接,連接成功后就可以進行交互嘹。

GATT(Generic Attribute Profile)

簡單理解為普通屬性描述,BLE連接成功后,BLE設備基于該描述進行發送和接收類似“屬性”的較短數據。目前大多數BLE屬性描述是基于GATT。一般一個Profile代表了一個特殊的功能應用,例如心率或者電量應用。

ATT(Attribute Protocol)
GATT是基于ATT上實現的,ATT是運行在BLE設備中,它們之間以盡可能小的屬性在進行交互,而屬性則是以Service和Characteristic的形式在ATT上傳輸。下圖是GATT的結構。

GATT結構

  • Characteristic 一個特性(Characteristic)包含一個值(value)和0至n個描述符(descriptors),而每個描述符又可以代表特性的值。
  • Descriptor 描述符是用來定義代表Characteristic的值的屬性。例如用來描述心率的取值范圍和單位。
  • Service 一個Profile代表著一個應用,而Service代表該應用可以提供多少種服務。例如心率監視器提供心率值檢測服務,Service內包含著多個Characteristic。

Service和Characteristic都通過16位或128位的UUID進行識別,16位的UUID需要向官方購買,全球唯一,而120位可以自己定義。一般UUID由硬件部門或者廠商提供。數據的交互都是客戶端發起請求,服務端響應,客戶端進行讀寫從而達到全雙工。

在BLE連接中,定義者兩個角色,GATT客戶端和Gatt服務端,一般認為,主動發起數據請求的是Client,而響應數據結果的是Server。例如手機和手環。在數據交互的過程中,永遠是Client單方面發起請求,然后讀寫Server相關屬性達到全雙工效果。

理論知識就講到這里了哇,下面進行Android應用層的開發哦。

實戰

實戰部分的內容,大多數和藍牙實現聊天功能是一致的。但為了沒有看過這邊文章的同學,我就Ctrl+cCtrl-v一下,順便修改一下代碼。

聲明權限

在AndroidManifest.xml配置下面代碼,讓APP具有藍牙訪問權限和發現周邊藍牙權限。

//使用藍牙需要該權限
<uses-permission android:name="android.permission.BLUETOOTH"/>
//使用掃描和設置需要權限
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
//Android 6.0以上聲明一下兩個權限之一即可。聲明位置權限,不然掃描或者發現藍牙功能用不了哦
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>

為了適配Android 6.0,在主Activity中添加動態申請定位權限代碼,不添加掃描不到藍牙代碼哦。

    /**
     * Android 6.0 動態申請授權定位信息權限,否則掃描藍牙列表為空
     */
    private void requestPermissions() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            if (ContextCompat.checkSelfPermission(this,
                    Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) {

                if (ActivityCompat.shouldShowRequestPermissionRationale(this,
                        Manifest.permission.ACCESS_COARSE_LOCATION)) {
                    Toast.makeText(this, "使用藍牙需要授權定位信息", Toast.LENGTH_LONG).show();
                }
                //請求權限
                ActivityCompat.requestPermissions(this,
                        new String[]{Manifest.permission.ACCESS_COARSE_LOCATION},
                        REQUEST_ACCESS_COARSE_LOCATION_PERMISSION);
            }
        }
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        if (requestCode == REQUEST_ACCESS_COARSE_LOCATION_PERMISSION) {
            if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                //用戶授權
            } else {
                finish();
            }

        }

        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
    }

檢測設備是否支持BLE功能

避免部分同學在不支持藍牙的手機或者設備安裝了Demo,或者安裝在模擬器了。

    /**
     * 是否支持BLE
     */
    private boolean isSupportBLE() {
        mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();

        BluetoothManager manager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);

        mBluetoothAdapter = manager.getAdapter();
            //設備是否支持藍牙
        if (mBluetoothAdapter == null
                    //系統是否支持BLE
                && !getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
            Log.e(TAG, "not support bluetooth");
            return true;
        } else {
            Log.e(TAG, " support bluetooth");
            return false;
        }

    }

    /**
     * 彈出不支持低功耗藍牙對話框
     */
    private void showNotSupportBluetoothDialog() {
        AlertDialog dialog = new AlertDialog.Builder(this).setTitle("當前設備不支持BLE").create();
        dialog.show();
        dialog.setOnDismissListener(new DialogInterface.OnDismissListener() {
            @Override
            public void onDismiss(DialogInterface dialog) {
                finish();
            }
        });

    }

開啟藍牙

有了支持BLE的手機,那么要檢測手機藍牙是否打開。如果沒有打開則打開藍牙和監聽藍牙的狀態變化的廣播。藍牙打開后,掃描周邊藍牙設備。

    //開啟藍牙
    private void enableBLE() {
        if (mBluetoothAdapter.isEnabled()) {
            startScan();
        } else {
            mBluetoothAdapter.enable();
        }
    }
    //注冊監聽藍牙狀態變化廣播
    private void registerBluetoothReceiver() {
        IntentFilter filter = new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED);
        registerReceiver(bluetoothReceiver, filter);
    }

    BroadcastReceiver bluetoothReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            String action = intent.getAction();

            if (action.equals(BluetoothAdapter.ACTION_STATE_CHANGED)) {
                int state = mBluetoothAdapter.getState();
                if (state == BluetoothAdapter.STATE_ON) {
                    startScan();
                }
            }
        }
    };

掃描

Android 5.0以上的掃描API和Android 5.0以下的API已經不一樣了。藍牙掃描是非常耗電的,Android 默認在手機息屏停止掃描,在手機亮屏后開始掃描。為了更好的降低耗電,正式APP應該主動關閉掃描,不應該循環掃描。BLE掃描速度非常快,我們根據掃描到的藍牙設備MAC保存Set集合中,過濾掉重復的設備。

   private void startScan() {

        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
            //android 5.0之前的掃描方式
            mBluetoothAdapter.startLeScan(new BluetoothAdapter.LeScanCallback() {
                @Override
                public void onLeScan(BluetoothDevice device, int rssi, byte[] scanRecord) {

                }
            });
        } else {
            //android 5.0之后的掃描方式
             scanner = mBluetoothAdapter.getBluetoothLeScanner();

             scanCallback=new ScanCallback() {
                 @Override
                 public void onScanResult(int callbackType, ScanResult result) {

                     //停止掃描
                     if (firstScan){
                         handler.postDelayed(new Runnable() {
                             @Override
                             public void run() {
                                 scanner.stopScan(scanCallback);

                             }
                         },SCAN_TIME);

                         firstScan=false;
                     }

                     String mac=result.getDevice().getAddress();

                     Log.i(TAG,"mac:"+mac);
                     //過濾重復的mac
                     if (!macSet.contains(mac)){
                         macSet.add(result.getDevice().getAddress());
                         deviceList.add(result.getDevice());
                         deviceAdapter.notifyDataSetChanged();
                     }
                 }

                 @Override
                 public void onBatchScanResults(List<ScanResult> results) {
                     super.onBatchScanResults(results);
                     //需要藍牙芯片支持,支持批量掃描結果。此方法和onScanResult是互斥的,只會回調其中之一
                 }

                 @Override
                 public void onScanFailed(int errorCode) {
                     super.onScanFailed(errorCode);
                     Log.e(TAG,"掃描失敗:"+errorCode);
                 }
             };

            scanner.startScan(scanCallback);
        }

    }

這里主要實現的Android 5.0后的掃描,通過將掃描到的設備添加到list,并顯示到界面上。由于可能掃描到重復的藍牙設備,通過Set過濾掉重復的設備。

抽象類ScanCallback作為BLE掃描的回調,重寫其中三個抽象方法。

  • onScanResult 一般情況,我們重寫該方法,每掃描到設備則回調一次。
  • onBatchScanResults 接口文檔注釋是回調之前已經掃描的的藍牙列表,但實際在測試沒有結果,網上搜了一下,結果在代碼中備注了。
  • onScanFailed 掃描失敗

ScanResult掃描結果內包含掃描到的周邊BLE設備BluetoothDevice。通過BluetoothDevice,我們可以獲取周邊BLE的相關信息,例如MAC,連接狀態等。

連接BLE

在上一步獲得我們的BLE列表后,選擇我們要連接的BLE設備,進行連接。處理listview 的點擊效果,進行連接BLE設備。

    lv.setOnItemClickListener(new AdapterView.OnItemClickListener() {
        @Override
        public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
            BluetoothDevice device = deviceList.get(position);
            bluetoothGatt = device.connectGatt(MainActivity.this, true, gattCallback);
        }
    });

通過BluetoothDevice的connectGatt()方法連接周邊BLE設備。現在明白為何要先了解GATT了吧。connectGatt()方法有三個參數,第二個參數表示當設備可用時,是否自動連接,第三個參數是BluetoothGattCallback類型,通過該回調,我們可以知道BLE的連接狀態和對Service、Charateristic進行操作,從而進行數據交互。connectGatt()方法會返回類型BluetoothGatt的實例,通過該實例,我們可以發送請求服務端

BluetoothGattCallback

抽象類BluetoothGattCallback有很多方法需要我們重寫,我們這里說幾個比較重要的,其他可以看Demo。我們通過定義 GattCallback繼承BluetoothGattCallback,并在類中重寫其方法。這里假設我們通過手機去連接小米手環,那么手機就是Gatt客戶端,小米手環就是Gatt服務端。

  • onConnectionStateChange(BluetoothGatt gatt, int status, int newState)
    該方法手機連接或者斷開連接到小米手環會回調該方法。參數一代表當前Gatt客戶端,也就是我們的手機。參數二表示連接或者斷開連接的操作是否成功,只有參數二status值為GATT_SUCCESS,參數三才有效。參數三會返回STATE_CONNECTEDSTATE_DISCONNECTED表示當前客戶端和服務端的連接狀態。連接成功后,我們通過bluetoothGatt對象的 discoverServices()
  • onServicesDiscovered(BluetoothGatt gatt, int status)當發現Service就會回調該方法,參數二值為GATT_SUCCESS表示服務端的所有服務已經被搜索完畢,此時可以調用bluetoothGatt.getServices()獲得Service列表,進而獲得所有Characteristic。

也可以通過指定的UUID獲得Service和Characteristic。

private void updateValue() {
    BluetoothGattService service = bluetoothGatt.getService(UUID.fromString(serviceUuid));
    if (service == null) return;
    BluetoothGattCharacteristic characteristic = service.getCharacteristic(UUID.fromString(charUuid));
    enableNotification(characteristic, charUuid);
    characteristic.setValue("on");
}

設置GATT通知

這樣當我們修改characteristic成功后,會回調告知我們。

private void enableNotification(BluetoothGattCharacteristic characteristic,String uuid){
    bluetoothGatt.setCharacteristicNotification(characteristic,true);
    BluetoothGattDescriptor descriptor = characteristic.getDescriptor(
            UUID.fromString(uuid));
    descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
    bluetoothGatt.writeDescriptor(descriptor);
}

上面代碼設置成功后,會回調BluetoothGattCallback的onCharacteristicChanged()方法。

如果Characteristic的值被修改,會回調BluetoothGattCallback的onCharacteristicChanged()方法,在這里我們可以進一步提高用戶體驗。需要注意一下,類BluetoothGattCallback有很多方法需要我們實現,因為Gatt的響應結果都是回調該對象的方法。

小結一下

Gatt客戶端通過BluetoothDevice的connectGatt()方法與服務端連接成功后,利用返回的BluetoothGatt對象,請求Gatt服務端相關數據。Gatt服務端根據請求,將自身的狀態通過回調客戶端傳入的BluetoothGattCallback對象的相關方法,從而告知客戶端。

關閉BLE

當我們使用完BLE之后,應該及時關閉,以釋放相關資源和降低功耗。

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

總結

在應用層操作BLE難度不大,因為Android屏蔽了很多藍牙棧協議的細節。但應用層開發會苦于沒有硬件設備支持。通過本文,我們知道BLE的AP和GATT等等一些概念,了解Android BLE開發的整體流程,對BLE有一個感性的認知。

堅持初心,寫優質好文章
開文有益,點贊支持好文

Demo的代碼地址Github

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容