1. 程式人生 > >Android 藍芽開發 —— BLE

Android 藍芽開發 —— BLE

藍芽——BLE

介紹

1.BLE 是 Bluetooth Low Energy 的縮寫,意思為低功耗藍芽。由藍芽技術聯盟(Bluetooth SIG)設計的無線通訊技術,主要用於醫療,健身,安全和家庭娛樂行業。 與傳統藍芽相比,藍芽低功耗旨在大幅降低功耗和成本,同時也能夠達到相同的通訊效果。
支援多個平臺,包括 IOS,Android,Windows Phone 和 BlackBerry 以及 macOS,Linux,Windows 8 和 Windows 10 在內的移動作業系統本身支援藍芽低功耗。 藍芽 SIG 預測,到 2018 年,超過 90% 的藍芽智慧手機將支援藍芽低功耗。

在安卓平臺,
在 Android 4.3 (API level 18) 以後引進來的,通過這些 API 可以掃描藍芽裝置、連線裝置,查詢 services、讀寫裝置的 characteristics(屬性特徵),然後通過屬性進行資料傳輸。

特點:

低功耗,使用 BLE 與周圍裝置進行通訊時,其峰值功耗為傳統藍芽的一半
傳輸距離提升到 100 米
低延時,最短可在3 ms內完成連線並開始進行資料傳輸

缺點:

傳輸資料量較小,最大 512 個位元組,超過 20 個位元組需要分包處理

應用領域:

主要用於智慧硬體,像健康護理、運動和健身、裝置電源管理等

連線模式

對於BLE單裝置來講常見的藍芽模組的工作模有四種:

  • 主裝置模式
  • 從裝置模式
  • 廣播模式
  • Mesh組網模式

主裝置模式

可以與一個從裝置進行連線。在此模式下可以對周圍裝置進行搜尋並選擇需要連線的從裝置進行連線。同時可以設定預設連線從裝置的MAC地址,這樣模組上電之後就可以查詢此模組並進行連線。

從裝置模式

BLE支援從裝置模式,在此模式下完全符合BLE4.1協議,使用者可以根據協議自己開發APP。此模式下包含一個串列埠收發的Service,使用者可以通過UUID找到它,裡面有兩個通道,分別是讀和寫。使用者可以操作這兩個通道進行資料的傳輸。

廣播模式

在這種模式下模組可以一對多進行廣播。使用者可以通過AT指令設定模組廣播的資料,模組可以在低功耗的模式下持續的進行廣播,應用於極低功耗,小資料量,單向傳輸的應用場合,比如無線抄表,室內定位等功能。

Mesh組網模式

在這種模式下模組可以實現簡單的自組網路,每個模組只需要設定相同的通訊密碼就可以加入到同一網路當中,每一個模組都可以發起資料,每個模組可以收到資料並且進行回覆。並且不需要閘道器,即使某一個裝置出現故障也會跳過並選擇最近的裝置進行傳輸。

GATT協議

GATT generic Attributes的縮寫,中文是通用屬性,是低功耗藍芽裝置之間進行通訊的協議。
GATT定義了一種多層的資料結構,已連線的低功耗藍芽裝置用它來進行通訊,GATT層是傳輸真正資料所在的層。一個GATT伺服器通過一個稱為屬性表的表格組織資料,這些資料就是用於真正傳送的資料。

GATT定義的多層資料結構簡要概括起來就是服務(service)可以包含多個特徵(characteristic),每個特徵包含屬性(properties)和值(value),還可以包含多個描述(descriptor)。它形象的結構如下圖:

pic1

profile(資料配置檔案)

一個profile檔案可以包含一個或者多個服務,一個profile檔案包含需要的服務的資訊或者為對等裝置如何互動的配置檔案的選項資訊。裝置的GAP和GATT的角色都可能在資料的交換過程中改變,因此,這個檔案應該包含廣播的種類、所使用的連線間隔、所需的安全等級等資訊。

需要注意的是: 一個profile中的屬性表不能包含另一個屬性表。

屬性

一個屬性包含控制代碼、UUID(型別)、值,控制代碼是屬性在GATT表中的索引,在一個裝置中每一個屬性的控制代碼都是唯一的。UUID包含屬性表中資料型別的資訊,它是理解屬性表中的值的每一個位元組的意義的關鍵資訊。在一個GATT表中可能有許多屬性,這些屬效能可能有相同的UUID。
個人理解,屬性指的是 Service、Characteristic 這樣的物件

Service

一個低功耗藍芽裝置可以定義許多 Service, Service 可以理解為一個功能的集合。裝置中每一個不同的 Service 都有一個 128 bit 的 UUID 作為這個 Service 的獨立標誌。藍芽核心規範制定了兩種不同的UUID,一種是基本的UUID,一種是代替基本UUID的16位UUID。所有的藍芽技術聯盟定義UUID共用了一個基本的UUID:
0x0000xxxx-0000-1000-8000-00805F9B34FB
為了進一步簡化基本UUID,每一個藍芽技術聯盟定義的屬性有一個唯一的16位UUID,以代替上面的基本UUID的‘x’部分。例如,心率測量特性使用0X2A37作為它的16位UUID,因此它完整的128位UUID為:
0x00002A37-0000-1000-8000-00805F9B34FB

Characteristic

在 Service 下面,又包括了許多的獨立資料項,我們把這些獨立的資料項稱作 Characteristic。同樣的,每一個 Characteristic 也有一個唯一的 UUID 作為識別符號。在 Android 開發中,建立藍芽連線後,我們說的通過藍芽傳送資料給外圍裝置就是往這些 Characteristic 中的 Value 欄位寫入資料;外圍裝置傳送資料給手機就是監聽這些 Charateristic 中的 Value 欄位有沒有變化,如果發生了變化,手機的 BLE API 就會收到一個監聽的回撥。

DesCriptor

任何在特性中的屬性不是定義為屬性值就是為描述符。描述符是一個額外的屬性以提供更多特性的資訊,它提供一個人類可識別的特性描述的例項。然而,有一個特別的描述符值得特別地提起:客戶端特性配置描述符(Client Characteristic Configuration Descriptor,CCCD),這個描述符是給任何支援通知或指示功能的特性額外增加的。在CCCD中寫入“1”使能通知功能,寫入“2”使能指示功能,寫入“0”同時禁止通知和指示功能。

使用過程

常採用的模式是主機模式,然後掃描客戶端硬體,然後連線,獲取相關服務和特性,然後進行資料傳輸。
steps

1. 掃描

許可權獲取


<uses-permission android:name="android.permission.BLUETOOTH"/> 使用藍芽所需要的許可權
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/> 使用掃描和設定藍芽的許可權(申明這一個許可權必須申明上面一個許可權)

在Android5.0之前,是預設申請GPS硬體功能的。而在Android 5.0 之後,需要在manifest 中申明GPS硬體模組功能的使用。

    <!-- Needed only if your app targets Android 5.0 (API level 21) or higher. -->
    <uses-feature android:name="android.hardware.location.gps" />

在 Android 6.0 及以上,還需要開啟位置許可權。如果應用沒有位置許可權,藍芽掃描功能不能使用(其它藍芽操作例如連線藍芽裝置和寫入資料不受影響)。

    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>

除了上面的設定之外,如果想設定裝置只支援 BLE,可以加上下面這句話

    <uses-feature android:name="android.hardware.bluetooth_le" android:required="true"/>

同樣,如果不想新增 BLE 的支援,那麼可以設定 required="false"

然後可以在執行時判斷裝置是否支援 BLE,

    // Use this check to determine whether BLE is supported on the device. Then
    // you can selectively disable BLE-related features.
    if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
        Toast.makeText(this, R.string.ble_not_supported, Toast.LENGTH_SHORT).show();
        finish();
    }

初始化

判斷 BLE 在裝置上是否支援,如果不支援的話,那麼可以不用繼續後面的操作了;如果支援,但是有可能藍芽被禁掉了,因為開著藍芽比較好點,使用者一般都會關閉藍芽,這時候可以傳送請求,來開啟藍芽,可以通過兩個步驟來完成。

  1. 獲取 BluetoothAdapter

BluetoothAdapter 對於一個裝置來說唯一的,整個系統或者應用,對藍芽進行操作時都是需要這個的介面卡。它的獲取需要通過系統服務來獲取。

    private BluetoothAdapter mBluetoothAdapter;
    ...
    // Initializes Bluetooth adapter.
    final BluetoothManager bluetoothManager =
            (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
    mBluetoothAdapter = bluetoothManager.getAdapter();

2.開啟藍芽

一般對於使用者來說,在手機上藍芽是關閉,當開啟你的應用時就需要開啟藍芽,有兩種方式,一種是跳轉到設定介面,由使用者自己開啟藍芽;
另外一種時,直接在應用開啟藍芽,不需要使用者開啟,而是直接幫使用者開啟手機上的藍芽。

跳轉到設定介面

// Ensures Bluetooth is available on the device and it is enabled. If not,
// displays a dialog requesting user permission to enable Bluetooth.
if (mBluetoothAdapter == null || !mBluetoothAdapter.isEnabled()) {
    Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
    startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
}

直接開啟藍芽

    // 開啟藍芽
    if (!mBluetoothAdapter.isEnabled()) {
        mBluetoothAdapter.enable();
    }

掃描

掃描藍芽裝置可以通過startLeScan(),其中有一個引數是 ScanCallback,通過它返回掃描結果,因為掃描過程是很耗電的,所以在掃描過程需要保證

1.一旦找到目標裝置,需要停止掃描
2.掃描不要設定迴圈,而且需要設定一個時間

回撥如下


// 裝置掃描回撥
    private ScanCallback mScanCallback = new ScanCallback() {
        @Override
        public void onScanResult(int callbackType, final ScanResult result) {

            runOnUiThread(new Runnable() {
                @Override
                public void run() {

                    // 廣播的資訊,可以在result中獲取
                    MDevice mDev = new MDevice(result.getDevice(), result.getRssi());
                    if (!mList.contains(mDev)) {
                        mList.add(mDev);
                    }
                    if (mList.size() > 0) {
                        mScanner.stopScan(mScanCallback);
                        Toast.makeText(MainActivity.this, "掃描結束,裝置數 " + mList.size()
                                , Toast.LENGTH_SHORT).show();
                    }
                }
            });
        }
    };

開始掃描

private BluetoothAdapter mBluetoothAdapter;
    private boolean mScanning;
    private Handler mHandler;

    // Stops scanning after 10 seconds.
    private static final long SCAN_PERIOD = 10000;
    ...
    private void scanLeDevice(final boolean enable) {
        if (enable) {
            // Stops scanning after a pre-defined scan period.
            mHandler.postDelayed(new Runnable() {
                @Override
                public void run() {
                    mScanning = false;
                    mBluetoothAdapter.stopLeScan(mLeScanCallback);
                }
            }, SCAN_PERIOD);

            mScanning = true;
            mBluetoothAdapter.startLeScan(mLeScanCallback);
        } else {
            mScanning = false;
            mBluetoothAdapter.stopLeScan(mLeScanCallback);
        }
        ...
    }

這裡遇到一個坑,就是實際中手機與一些智慧硬體連線時,也就是需要連線指定的硬體,裝置有一個UUID,所以可以通過如下方法連線

    startLeScan(UUID[], BluetoothAdapter.LeScanCallback)

但是實際中使用時,連線時會出錯,仍需要再次驗證。
我當時的做法是採用了另外一種方法,當時這種方法,要求 API 高於 21。

    private void scanLeDevice() {

        //50秒後停止掃描
        mHander.postDelayed(stopScanRunnable, 50000);

        List<ScanFilter> filters = new ArrayList<>();
        ScanFilter filter = new ScanFilter.Builder()
                //"D8:B0:4C:E8:66:DC" 測試MAC 1
                //"D8:B0:4C:E2:45:2A"  測試MAC 2
                .setDeviceAddress("D8:B0:4C:E2:45:2A")
                .build();
        filters.add(filter);

        // 掃描
        mScanner.startScan(filters, new ScanSettings.Builder().setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY).build(), mScanCallback);

    }

掃描結束後需要停止掃描

boolean startLeScan(BluetoothAdapter.LeScanCallback callback)

連線

裝置連線

通過掃描能夠獲得裝置 BluetoothDevice,包含地址和名字
通過裝置連線並獲取 BluetoothGatt,後面通過 BluetoothGatt 的例項來進行client的操作,如使用該例項去發現服務,獲取讀、寫、通知等屬性

public static BluetoothGatt mBluetoothGatt;

 mBluetoothGatt = device.connectGatt(context, false, mGattCallback);

通過連接回調來監聽連線的狀態,包含三種狀態,連線、斷開、正在連線,根據狀態可以傳送廣播,
在接收廣播的位置進行做相應的處理

    private final static BluetoothGattCallback mGattCallback = new BluetoothGattCallback() {
        @Override
        public void onConnectionStateChange(BluetoothGatt gatt, int status,
                                            int newState) {

            String intentAction;
            // GATT Server connected
            if (newState == BluetoothProfile.STATE_CONNECTED) {
                System.out.println("---------------------------->已經連線");
                intentAction = ACTION_GATT_CONNECTED;
                mConnectionState = STATE_CONNECTED;
                broadcastConnectionUpdate(intentAction);
            }
            // GATT Server disconnected
            else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
                System.out.println("---------------------------->連線斷開");
                intentAction = ACTION_GATT_DISCONNECTED;
                mConnectionState = STATE_DISCONNECTED;
                broadcastConnectionUpdate(intentAction);

            }
            // GATT Server disconnected
            else if (newState == BluetoothProfile.STATE_DISCONNECTING) {
                System.out.println("---------------------------->正在連線");
//                intentAction = ACTION_GATT_DISCONNECTING;
//                mConnectionState = STATE_DISCONNECTING;
//                broadcastConnectionUpdate(intentAction);
            }
        }
    }

當操作完成後,需要關閉連線,必須呼叫 BluetoothGatt#close 方法釋放連線資源

發現服務

由於有了 mBluetoothGatt,就可以去發現服務,再通過服務去獲取可以操作的屬性


mBluetoothGatt.discoverServices();

發現服務以及獲取其他屬性,如write和read,notify,Descriptor相關的屬性,均是在 BluetoothGattCallback 中有回撥,在回撥中就可以通過傳送廣播,然後在其他位置做處理,
如接收資料就會有回撥,然後將資料傳遞出去,對資料解析等


 @Override
        public void onServicesDiscovered(BluetoothGatt gatt, int status) {
            // GATT Services discovered
            //發現新的服務
            if (status == BluetoothGatt.GATT_SUCCESS) {
                System.out.println("---------------------------->發現服務");
                broadcastConnectionUpdate(ACTION_GATT_SERVICES_DISCOVERED);
            } 
        }

        //通過 Descriptor 寫監聽
        @Override 
        public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor,
                                      int status) {

        }

        // 通過 Descriptor 讀監聽
        @Override
        public void onDescriptorRead(BluetoothGatt gatt, BluetoothGattDescriptor descriptor,
                                     int status) {

        }


        @Override
        public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic
                characteristic, int status) {
            //write操作會呼叫此方法
            if (status == BluetoothGatt.GATT_SUCCESS) {
                System.out.println("onCharacteristicWrite ------------------->write success");
                Intent intent = new Intent(ACTION_GATT_CHARACTERISTIC_WRITE_SUCCESS);
                mContext.sendBroadcast(intent);
            } else {
                Intent intent = new Intent(ACTION_GATT_CHARACTERISTIC_ERROR);
                intent.putExtra(Constants.EXTRA_CHARACTERISTIC_ERROR_MESSAGE, "" + status);
                mContext.sendBroadcast(intent);
            }
        }

        @Override
        public void onCharacteristicRead(BluetoothGatt gatt,
                                         BluetoothGattCharacteristic characteristic, int status) {
            // 接收資料
        }

        @Override
        public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {

            //notify 會回撥用此方法
            broadcastNotifyUpdate(characteristic);
        }

        @Override
        public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) {
           super.onMtuChanged(gatt, mtu, status);

        }

資料傳輸

  1. 資料讀取

這裡有兩個方法:

方法一: 一般資料讀取的話,想到的是用 read 屬性,所以需要獲取特定通道的 BluetoothGattCharactristic。

1)BluetoothGatt#getService 得到服務

2)BluetoothGattService#getCharactristic 獲取 BluetoothGattCharactristic,這裡獲取的 BluetoothGattCharactristic 是有指定 UUID 的,也就是說不同的 Charactristic的 UUID 是不同的,讀和寫的通道不同,根據不同的操作,然後通過UUID獲取相應的通道

3)BluetoothGattCharactristic#readCharacteristic 方法可以通知系統去讀取特定的資料

4)BluetoothGattCallback#onCharacteristicRead 方法。通過 BluetoothGattCharacteristic#getValue 可以讀取到藍芽裝置的資料

方法二:採用 notify 屬性,客戶端傳送資料,服務端監聽屬性變化,然後根據 屬性的 UUID 判斷是否是 notify 的屬性,如果是的話,說確實是由遠端裝置發過來的資料。

1)BluetoothGatt#getService 得到服務

2)BluetoothGattService#getCharactristic 獲取 BluetoothGattCharactristic,這裡的這個屬性是 notify 屬性

3)獲得屬性後需要進行判斷裝置是否支援notify操作,然後再裝置開啟notify通知

void prepareBroadcastDataNotify(
            BluetoothGattCharacteristic characteristic) {

        final int charaProp = characteristic.getProperties();

        Toast.makeText(this, " " + charaProp, Toast.LENGTH_SHORT).show();

        if ((charaProp | BluetoothGattCharacteristic.PROPERTY_NOTIFY) > 0) {
            BluetoothLeService.setCharacteristicNotification(characteristic, true);
        }

    }

4) 設定屬性時,也要通知遠端裝置端也要開啟 notify 屬性

public static void setCharacteristicNotification(BluetoothGattCharacteristic characteristic, boolean enabled) {

        if (mBluetoothAdapter == null || mBluetoothGatt == null) {
            return;
        }
        //通知遠端端開啟 notify 
        if (characteristic.getDescriptor(UUID.fromString(GattAttributes.CLIENT_CHARACTERISTIC_CONFIG)) != null) {
            if (enabled == true) {
                BluetoothGattDescriptor descriptor = characteristic
                        .getDescriptor(UUID.fromString(GattAttributes.CLIENT_CHARACTERISTIC_CONFIG));
                descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
                mBluetoothGatt.writeDescriptor(descriptor);
            } else {
                BluetoothGattDescriptor descriptor = characteristic
                        .getDescriptor(UUID.fromString(GattAttributes.CLIENT_CHARACTERISTIC_CONFIG));
                descriptor.setValue(BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE);
                mBluetoothGatt.writeDescriptor(descriptor);
            }
        }
        mBluetoothGatt.setCharacteristicNotification(characteristic, enabled);
    }
  1. 資料寫入

對於 BLE 方式的資料傳輸來說,資料的大小是有限制的,一次性最多可以傳輸512個位元組,這也是BLE小資料量傳輸的特點,另外,對於每次傳輸,也有限制,每個資料包大小不超過20個位元組,超過20個位元組的話,需要分包處理。寫的步驟和讀取類似。

1)BluetoothGatt#getService 得到服務

2)BluetoothGattService#getCharactristic 獲取 BluetoothGattCharactristic,這裡的這個屬性是 write 屬性

3)寫入位元組資料


public static void writeCharacteristicGattDb(
            BluetoothGattCharacteristic characteristic, byte[] byteArray) {

        if (mBluetoothAdapter == null || mBluetoothGatt == null) {
            return;
        } else {
            byte[] valueByte = byteArray;
            characteristic.setValue(valueByte);
            mBluetoothGatt.writeCharacteristic(characteristic);
        }
    }

4)對於手機端,寫入資料後,遠端端會接受,同時回撥中也會能夠接收,也可以在回撥中做一下資料判斷,看是否是自己發出的資料


 @Override
        public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic
                characteristic, int status) {
            //write操作會呼叫此方法
            if (status == BluetoothGatt.GATT_SUCCESS) {
                System.out.println("onCharacteristicWrite ------------------->write success");
                Intent intent = new Intent(ACTION_GATT_CHARACTERISTIC_WRITE_SUCCESS);
                // 這裡通過屬效能夠讀取你傳送的資料,可以對此資料進行判斷
                characteristic.getValue();
                mContext.sendBroadcast(intent);
            } else {
                Intent intent = new Intent(ACTION_GATT_CHARACTERISTIC_ERROR);
                intent.putExtra(Constants.EXTRA_CHARACTERISTIC_ERROR_MESSAGE, "" + status);
                mContext.sendBroadcast(intent);
            }
        }

其他

  1. 一般在通訊過程中,需要有連線的心跳包,來檢測是否仍處於連線狀態,可以通過設定定時器,主機端定時 write 資料,客戶端定時 notify 資料
  2. 2.

斷開連線

操作完成,需要斷開藍芽並釋放資源,通過 BluetoothGatt#disconnect 斷開連線,然後回撥中會收到斷開的監聽,可以根據狀態釋放資源。BluetoothGattCallback#onConnectionStateChange回撥中通過這個方法的 newState 引數可以判斷是連線成功還是斷開成功的回撥,斷開成功的話,然後呼叫 BluetoothGatt#close 方法釋放資源

參考

官方文件