Android 藍牙4.0低功耗(BLE)官方詳解(譯)

聲明:轉載請註明出處http://www.jianshu.com/p/54bc88207050

Android 4.3(API level 18)引入了藍牙低功耗(BLE)核心功能,並提供了API可供app來搜索設備,查詢服務以及傳輸信息。

藍牙低功耗通常用於以下幾個方面:

  • 在兩個相近的設備之間傳輸少量數據。
  • 與距離傳感器進行交互,比如Google Beacons。這樣可以基於當前位置來定製用戶個性化體驗。

相比經典藍牙,藍牙低功耗(BLE)可以顯著減少電量消耗。這使得Android應用可以與對電量有着更嚴格限制的設備通信,比如位置傳感器,心率監視器,以及健身設備。

關鍵術語和概念

以下是對BLE關鍵術語和概念的總結:

  • Generic Attribute Profile (GATT)--GATT profile是發送和接收少量數據的一種通用規範,正如大家知道的在BLE連接上的“attributes”。當前所有的低功耗應用profile都是基於GATT。

    • 藍牙技術聯盟爲低功耗設備定義了很多profile。profile就是指定設備在特定的應用中如何工作的一種規範 。舉個例子,一個設備可以包含一個心率監視器和一個電量狀態監測器。
  • Attribute Protocol (ATT)--GATT是建立在ATT的基礎之上。所以也稱之爲GATT/ATT。ATT在BLE設備上運行經過了優化,因此,它會儘可能使用更少的字節數組。每個屬性通過UUID來唯一確定,UUID是一種128位的標準化格式字符串ID來標識信息的唯一性。屬性會被格式化爲特徵(characteristics )與服務(services)後通過ATT來傳輸。

  • 特徵(Characteristic)--一個特徵包含了一個單獨的值和0-n個描述符(descriptors ,用來描述特徵的值)。一個特徵可以看做是一種類型,類似一個class。

  • 描述符(Descriptor)--描述符定義了屬性用來描述特徵值。比如,描述符可以是人們可讀的描述語句,可以是特徵值得可接受範圍,或者是特徵值的測量單位。

  • 服務(Service)--服務是特徵的集合。比如,你有一個叫做‘心率監視器’的服務,在這個服務裏包含一個“心率測量”的特徵。你可以在bluetooth.org上查看一系列基於GATT的profile與服務。

角色與職責

以下是Android設備與BLE設備交互時的角色和職責:

  • 中心 vs 外設。這適用於BLE自身的連接。中心設備掃描,搜索廣告,外設發送廣告。
  • GATT服務 vs GATT客戶端。這決定了兩個設備之間如何通信一旦他們建立了連接。

爲了更好的理解不同,想象下假如你有一個Android手機與一個具有藍牙低功耗的活動追蹤器設備。手機支持中心角色;活動追蹤器支持外設角色(爲了建立連接兩個設備必須是不同的角色,不能同時爲同一個角色)。
一旦手機與藍牙設備建立了連接。他們就會傳輸GATT媒體數據到另一設備。根據傳輸的數據,它們其中一個需要作爲服務端。比如,如果活動追蹤設備想要上傳傳感數據到手機,那麼該活動追蹤器就作爲服務端。如果活動追蹤設備想要接收手機傳來的數據,那麼手機就是服務端。

在文章中舉的例子中,Android應用是GATT客戶端。app從GATT服務端獲取數據,這個服務也就是支持 心率Profile 的心率監視器。當然你也可以是你的Android app扮演服務端角色。可以查閱BluetoothGattServer獲取更多信息。

BLE權限

爲了在你的應用中使用藍牙,你需要申明藍牙權限BLUETOOTH。你需要這個權限來實現任何的藍牙通信,比如請求連接,接收連接以及傳輸數據。

如果你想是你的app開啓設備搜索以及管理藍牙設置,你必須申明BLUETOOTH_ADMIN權限。注意:如果你申明瞭BLUETOOTH_ADMIN權限,那麼你必須同時申明BLUETOOTH權限。

在你的manifest文件裏申明權限,如下:

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

如果你想你的應用只能在支持BLE的設備上使用,你可以添加以下申明:

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

然而,如果你想在不支持BLE的設備上使用你的app,你也需要添加上面的申明,只是需要將required設爲false。這樣你可以在運行時通過調用 PackageManager.hasSystemFeature()來判斷該設備是否支持BLE。

// 用以下方式來判斷設備是否支持BLE,從而選擇性的禁用BLE相關特性
if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
    Toast.makeText(this, R.string.ble_not_supported, Toast.LENGTH_SHORT).show();
    finish();
}

注意:BLE經常與位置有聯繫。所以爲了不帶過濾器的使用
BluetoothLeScanner,你必須申明ACCESS_COARSE_LOCATION或者ACCESS_FINE_LOCATION權限來訪問用戶的位置信息。如果沒有這些權限,那麼掃描將會沒有任何結果。

設置BLE

在你的應用使用BLE通信之前,你需要確認你的設備是否支持BLE,如果支持,那麼確保藍牙是打開的。注意這個判斷只有在<uses-feature/>標籤設爲false的時候纔是必須的。

如果設備不支持BLE,那麼你應該優雅的禁用任何藍牙功能。如果支持BLE,但還沒開啓的話,你可以讓用戶在應用內打開藍牙。你可以使用BluetoothAdapter通過以下兩步來完成該操作:
1.獲取BluetoothAdapter
BluetoothAdapter對於所有藍牙活動都是必須。BluetoothAdapter代表設備自己的藍牙適配器。整個設備系統中只有一個藍牙適配器,你的應用可以用這個對象來和它交互。以下代碼片展示瞭如何獲取這個適配器。注意這裏使用getSystemService()來返回一個BluetoothManager實例,這可以用來獲取適配器。Android 4.3(API 18)才引入的BluetoothManager:

private BluetoothAdapter mBluetoothAdapter;
...
// 初始化藍牙適配器
final BluetoothManager bluetoothManager =
        (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
mBluetoothAdapter = bluetoothManager.getAdapter();

2.開啓藍牙
接下來,你需要確認藍牙是否打開。調用
isEnabled()方法來確認當前藍牙是否打開。如果返回false,則藍牙未開啓。下面的代碼片判斷藍牙是否開啓。如果沒有,則提示用戶開啓藍牙:

// 確保設備支持藍牙,並已開啓,如果未開啓,則顯示一個彈窗來獲取用戶權限打開藍牙
if (mBluetoothAdapter == null || !mBluetoothAdapter.isEnabled()) {
    Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
    startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
}

注意:傳入startActivityForResult的常量REQUEST_ENABLE_BT 是定義在本地的int類型,系統會將這個常量作爲requestCode參數回傳到onActivityResult方法。

搜索BLE設備

你可以使用 startLeScan()方法來搜索設備。這個方法需要傳入一個回調參數BluetoothAdapter.LeScanCallback。你必須實現這個回調接口,因爲搜索結果會通過這個回調返回。由於掃描是耗電的,你需要遵循以下準則:

  • 當搜索到你想要的設備就應該停止掃描
  • 不能無限的掃描下去,需要給掃描做個時間限制。之前可以訪問的設備可能超出距離限制,繼續掃描只會消耗電量。

以下代碼片展示瞭如何開始以及結束掃描:

/**
 * 一個用於掃描的Activity並展示搜到的設備
 */
public class DeviceScanActivity extends ListActivity {

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

如果你想掃描特定的外設,你可以調用startLeScan(UUID[], BluetoothAdapter.LeScanCallback)方法,需要傳入你app支持的特定GATT服務的UUID對象數組。
以下是 BluetoothAdapter.LeScanCallback接口實現,用來返回掃描結果:

private LeDeviceListAdapter mLeDeviceListAdapter;
...
// 設備掃描回調
private BluetoothAdapter.LeScanCallback mLeScanCallback =
        new BluetoothAdapter.LeScanCallback() {
    @Override
    public void onLeScan(final BluetoothDevice device, int rssi,
            byte[] scanRecord) {
        runOnUiThread(new Runnable() {
           @Override
           public void run() {
               mLeDeviceListAdapter.addDevice(device);
               mLeDeviceListAdapter.notifyDataSetChanged();
           }
       });
   }
};

注意:正如在文章 Bluetooth中描述的,你只能掃描BLE設備或者經典藍牙設備其中一種,不能同時掃描。

連接GATT服務

和BLE設備通信的第一步就是連上它--更具體的講就是連接上設備的GATT服務。你可以使用connectGatt()方法來連接設備上的GATT服務。這個方法需要傳入三個參數:一個Context對象,autoConnect(用來標記是否當BLE設備被搜到時就立刻連接上),還有個BluetoothGattCallback回調參數:

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

上面代碼會連接上BLE設備中的GATT服務,並返回一個BluetoothGatt實例,你可以用這個實例來做GATT客戶端的操作。調用者(Android app)是GATT客戶端。 BluetoothGattCallback回調用來向客戶端返回結果,比如連接狀態以及更多的GATT客戶端操作。

這個本文的例子中,BLE app提供一個Activity(DeviceControlActivity
)來連接,展示數據,以及展示藍牙設備所支持的GATT服務與特徵。基於用戶的輸入,這個活動會和BluetoothLeService服務通信,這個服務通過Android BLE API來和BLE設備交互:

// 通過BLE API和設備交互的服務
public class BluetoothLeService extends Service {
    private final static String TAG = BluetoothLeService.class.getSimpleName();

    private BluetoothManager mBluetoothManager;
    private BluetoothAdapter mBluetoothAdapter;
    private String mBluetoothDeviceAddress;
    private BluetoothGatt mBluetoothGatt;
    private int mConnectionState = STATE_DISCONNECTED;

    private static final int STATE_DISCONNECTED = 0;
    private static final int STATE_CONNECTING = 1;
    private static final int STATE_CONNECTED = 2;

    public final static String ACTION_GATT_CONNECTED =
            "com.example.bluetooth.le.ACTION_GATT_CONNECTED";
    public final static String ACTION_GATT_DISCONNECTED =
            "com.example.bluetooth.le.ACTION_GATT_DISCONNECTED";
    public final static String ACTION_GATT_SERVICES_DISCOVERED =
            "com.example.bluetooth.le.ACTION_GATT_SERVICES_DISCOVERED";
    public final static String ACTION_DATA_AVAILABLE =
            "com.example.bluetooth.le.ACTION_DATA_AVAILABLE";
    public final static String EXTRA_DATA =
            "com.example.bluetooth.le.EXTRA_DATA";

    public final static UUID UUID_HEART_RATE_MEASUREMENT =
            UUID.fromString(SampleGattAttributes.HEART_RATE_MEASUREMENT);

    // BLE API定義的各種回調方法
    private final BluetoothGattCallback mGattCallback =
            new BluetoothGattCallback() {
        @Override
        public void onConnectionStateChange(BluetoothGatt gatt, int status,
                int newState) {
            String intentAction;
            if (newState == BluetoothProfile.STATE_CONNECTED) {
                intentAction = ACTION_GATT_CONNECTED;
                mConnectionState = STATE_CONNECTED;
                broadcastUpdate(intentAction);
                Log.i(TAG, "Connected to GATT server.");
                Log.i(TAG, "Attempting to start service discovery:" +
                        mBluetoothGatt.discoverServices());

            } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
                intentAction = ACTION_GATT_DISCONNECTED;
                mConnectionState = STATE_DISCONNECTED;
                Log.i(TAG, "Disconnected from GATT server.");
                broadcastUpdate(intentAction);
            }
        }

        @Override
        // 發現新的服務是回調
        public void onServicesDiscovered(BluetoothGatt gatt, int status) {
            if (status == BluetoothGatt.GATT_SUCCESS) {
                broadcastUpdate(ACTION_GATT_SERVICES_DISCOVERED);
            } else {
                Log.w(TAG, "onServicesDiscovered received: " + status);
            }
        }

        @Override
        // 讀取特徵回調
        public void onCharacteristicRead(BluetoothGatt gatt,
                BluetoothGattCharacteristic characteristic,
                int status) {
            if (status == BluetoothGatt.GATT_SUCCESS) {
                broadcastUpdate(ACTION_DATA_AVAILABLE, characteristic);
            }
        }
     ...
    };
...
}

當某個特定的回調觸發時,會調用broadcastUpdate()方法,並傳入一個action。注意這裏的數據解析是對應於本例子中的藍牙心率測量儀:

private void broadcastUpdate(final String action) {
    final Intent intent = new Intent(action);
    sendBroadcast(intent);
}

private void broadcastUpdate(final String action,
                             final BluetoothGattCharacteristic characteristic) {
    final Intent intent = new Intent(action);

    // 這是心率測量儀profile特有的數據解析方式
    if (UUID_HEART_RATE_MEASUREMENT.equals(characteristic.getUuid())) {
        int flag = characteristic.getProperties();
        int format = -1;
        if ((flag & 0x01) != 0) {
            format = BluetoothGattCharacteristic.FORMAT_UINT16;
            Log.d(TAG, "Heart rate format UINT16.");
        } else {
            format = BluetoothGattCharacteristic.FORMAT_UINT8;
            Log.d(TAG, "Heart rate format UINT8.");
        }
        final int heartRate = characteristic.getIntValue(format, 1);
        Log.d(TAG, String.format("Received heart rate: %d", heartRate));
        intent.putExtra(EXTRA_DATA, String.valueOf(heartRate));
    } else {
        // 對於其他profile,將數據轉換爲16進制
        final byte[] data = characteristic.getValue();
        if (data != null && data.length > 0) {
            final StringBuilder stringBuilder = new StringBuilder(data.length);
            for(byte byteChar : data)
                stringBuilder.append(String.format("%02X ", byteChar));
            intent.putExtra(EXTRA_DATA, new String(data) + "\n" +
                    stringBuilder.toString());
        }
    }
    sendBroadcast(intent);
}

回到DeviceControlActivity,上面的廣播會被BroadcastReceiver接收到:

// 處理各種事件
// ACTION_GATT_CONNECTED: 連接到設備
// ACTION_GATT_DISCONNECTED:斷開連接
// ACTION_GATT_SERVICES_DISCOVERED: 發現GATT服務
// ACTION_DATA_AVAILABLE: 從設備接收數據,這可以是讀操作或 通知的結果
private final BroadcastReceiver mGattUpdateReceiver = new BroadcastReceiver() {
    @Override
    public void onReceive(Context context, Intent intent) {
        final String action = intent.getAction();
        if (BluetoothLeService.ACTION_GATT_CONNECTED.equals(action)) {
            mConnected = true;
            updateConnectionState(R.string.connected);
            invalidateOptionsMenu();
        } else if (BluetoothLeService.ACTION_GATT_DISCONNECTED.equals(action)) {
            mConnected = false;
            updateConnectionState(R.string.disconnected);
            invalidateOptionsMenu();
            clearUI();
        } else if (BluetoothLeService.
                ACTION_GATT_SERVICES_DISCOVERED.equals(action)) {
            //向用戶展示所有支持的服務與特徵
            displayGattServices(mBluetoothLeService.getSupportedGattServices());
        } else if (BluetoothLeService.ACTION_DATA_AVAILABLE.equals(action)) {
            displayData(intent.getStringExtra(BluetoothLeService.EXTRA_DATA));
        }
    }
};

讀取BLE屬性

一旦你的應用連接上GATT服務並搜到服務,那麼就可以讀寫所支持的屬性。比如,以下代碼片就是迭代設備的服務與特徵並將它們展現在UI上:

public class DeviceControlActivity extends Activity {
    ...
    //演示瞭如何遍歷服務和特徵,在這個例子中將數據結構展示到樹形列表上
    private void displayGattServices(List<BluetoothGattService> gattServices) {
        if (gattServices == null) return;
        String uuid = null;
        String unknownServiceString = getResources().
                getString(R.string.unknown_service);
        String unknownCharaString = getResources().
                getString(R.string.unknown_characteristic);
        ArrayList<HashMap<String, String>> gattServiceData =
                new ArrayList<HashMap<String, String>>();
        ArrayList<ArrayList<HashMap<String, String>>> gattCharacteristicData
                = new ArrayList<ArrayList<HashMap<String, String>>>();
        mGattCharacteristics =
                new ArrayList<ArrayList<BluetoothGattCharacteristic>>();

        // 循環遍歷服務
        for (BluetoothGattService gattService : gattServices) {
            HashMap<String, String> currentServiceData =
                    new HashMap<String, String>();
            uuid = gattService.getUuid().toString();
            currentServiceData.put(
                    LIST_NAME, SampleGattAttributes.
                            lookup(uuid, unknownServiceString));
            currentServiceData.put(LIST_UUID, uuid);
            gattServiceData.add(currentServiceData);

            ArrayList<HashMap<String, String>> gattCharacteristicGroupData =
                    new ArrayList<HashMap<String, String>>();
            List<BluetoothGattCharacteristic> gattCharacteristics =
                    gattService.getCharacteristics();
            ArrayList<BluetoothGattCharacteristic> charas =
                    new ArrayList<BluetoothGattCharacteristic>();
           // 循環遍歷特徵
            for (BluetoothGattCharacteristic gattCharacteristic :
                    gattCharacteristics) {
                charas.add(gattCharacteristic);
                HashMap<String, String> currentCharaData =
                        new HashMap<String, String>();
                uuid = gattCharacteristic.getUuid().toString();
                currentCharaData.put(
                        LIST_NAME, SampleGattAttributes.lookup(uuid,
                                unknownCharaString));
                currentCharaData.put(LIST_UUID, uuid);
                gattCharacteristicGroupData.add(currentCharaData);
            }
            mGattCharacteristics.add(charas);
            gattCharacteristicData.add(gattCharacteristicGroupData);
         }
    ...
    }
...
}

接收GATT通知

通常當設備上的某個特定特徵改變了就需要通知BLE app。以下代碼片展示瞭如何給特徵設置通知,調用setCharacteristicNotification()方法:

private BluetoothGatt mBluetoothGatt;
BluetoothGattCharacteristic characteristic;
boolean enabled;
...
mBluetoothGatt.setCharacteristicNotification(characteristic, enabled);
...
BluetoothGattDescriptor descriptor = characteristic.getDescriptor(
        UUID.fromString(SampleGattAttributes.CLIENT_CHARACTERISTIC_CONFIG));
descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
mBluetoothGatt.writeDescriptor(descriptor);

一旦特徵設置了通知,那麼當遠程設備的特徵發生改變時就會回調 onCharacteristicChanged() 方法:

@Override
// 特徵通知
public void onCharacteristicChanged(BluetoothGatt gatt,
        BluetoothGattCharacteristic characteristic) {
    broadcastUpdate(ACTION_DATA_AVAILABLE, characteristic);
}

關閉App客戶端

一旦你的應用結束使用BLE設備,你就應該調用close方法來釋放系統資源:

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

好了以上就是官方關於藍牙BLE的介紹,可能還是比較籠統,下一篇我會結合一個實際的例子來介紹下。

官方原文:https://developer.android.com/guide/topics/connectivity/bluetooth-le.html#close

.

.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章