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
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
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);
        }

    }

  1. 設置屬性時,也要通知遠程設備端也要開啟 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 數據

斷開連接

操作完成,需要斷開藍牙并釋放資源,通過 BluetoothGatt#disconnect 斷開連接,然后回調中會收到斷開的監聽,可以根據狀態釋放資源。BluetoothGattCallback#onConnectionStateChange回調中通過這個方法的 newState 參數可以判斷是連接成功還是斷開成功的回調,斷開成功的話,然后調用 BluetoothGatt#close 方法釋放資源

參考

官方文檔

Android 開發入門介紹

通用屬性配置文件(GATT)及其服務,特性與屬性介紹

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

推薦閱讀更多精彩內容