Android:手機如何控制BLE設備?

前言

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

BLE的一些關鍵概念

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

藍牙類別

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

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

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

GAP(Generic Access Profile)

GAP用來控制藍牙設備的廣播和鏈接。GAP可使藍牙設備被其餘藍牙設備發現,並決定是否能夠被鏈接。GAP協議將藍牙設備分爲中心設備和外圍設備。ide

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

外圍設備經過廣播數據掃描回覆兩種方式之一讓中心設備發現,而後進行鏈接,從而達到進行數據交互的前提條件。爲了達到低功耗,外圍設備並非一直廣播,會設定一個廣播間隔,每一個廣播間隔中,它會從新發送本身的廣播數據。廣播間隔越長,越省電,同時也不太容易掃描到。post

在Android開發中,常經過藍牙MAC進行鏈接,鏈接成功後就能夠進行交互嘹。測試

GATT(Generic Attribute Profile)

簡單理解爲普通屬性描述,BLE鏈接成功後,BLE設備基於該描述進行發送和接收相似「屬性」的較短數據。目前大多數BLE屬性描述是基於GATT。通常一個Profile表明了一個特殊的功能應用,例如心率或者電量應用。ui

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

GATT結構

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

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

在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

相關文章
相關標籤/搜索