Android:手機如何控制BLE裝置?

哦豁技術發表於2019-06-19

前言

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

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有一個感性的認知。

不知道看完本文,是否對您開文有益?

Star or 點贊本文 是一種鼓勵與支援哦,努力堅持寫出好文!

Demo的程式碼地址Github

相關文章