1. 程式人生 > >android 藍芽4.0(BLE)開發

android 藍芽4.0(BLE)開發

最近剛好專案需要手機與藍芽模組通訊,基於藍芽4.0,網上資料較少也有些小坑,故作一下總結。

關鍵術語和概念

  • 藍芽有傳統藍芽(3.0以下)和低功耗藍芽(BLE,又稱藍芽4.0)之分,而藍芽4.0開發需要android4.3版本(API 18)及以上才支援BLE API。相比傳統的藍芽,BLE更顯著的特點是低功耗。這一優點使android App可以與具有低功耗要求的BLE裝置通訊,如近距離感測器、心臟速率監視器、健身裝置等。
  • BLE 全稱 Bluetooth Low Energy
  • Generic Attribute Profile(GATT)—GATT配置檔案是一個通用規範,用於在BLE鏈路上傳送和接收被稱為“屬性”的資料塊。目前所有的BLE應用都基於GATT。 藍芽SIG規定了許多低功耗裝置的配置檔案。配置檔案是裝置如何在特定的應用程式中工作的規格說明。注意一個裝置可以實現多個配置檔案。例如,一個裝置可能包括心率監測儀和電量檢測。
  • Attribute Protocol(ATT)—GATT在ATT協議基礎上建立,也被稱為GATT/ATT。ATT對在BLE裝置上執行進行了優化,為此,它使用了儘可能少的位元組。每個屬性通過一個唯一的的統一識別符號(UUID)來標識,每個String型別UUID使用128 bit標準格式。屬性通過ATT被格式化為characteristics和services。
  • Characteristic 一個characteristic包括一個單一變數和0-n個用來描述characteristic變數的descriptor,characteristic可以被認為是一個型別,類似於類。
  • Descriptor Descriptor用來描述characteristic變數的屬性。例如,一個descriptor可以規定一個可讀的描述,或者一個characteristic變數可接受的範圍,或者一個characteristic變數特定的測量單位。
  • Service service是characteristic的集合。例如,你可能有一個叫“Heart Rate Monitor(心率監測儀)”的service,它包括了很多characteristics,如“heart rate measurement(心率測量)”等。你可以在bluetooth.org 找到一個目前支援的基於GATT的配置檔案和服務列表。

藍芽4.0的結構

簡單的說,就是BLE是基於GATT實現的,BLE分為三個部分Service、Characteristic、Descriptor,每個部分都擁有不同的 UUID來標識。一個BLE裝置可以擁有多個Service,一個Service可以包含多個Characteristic, 一個Characteristic包含一個Value和多個Descriptor,一個Descriptor包含一個Value。 通訊資料一般儲存在Characteristic內,目前一個Characteristic中儲存的資料最大為20 byte。 與Characteristic相關的許可權欄位主要有READ、WRITE、WRITE_NO_RESPONSE、NOTIFY。 Characteristic具有的許可權屬性可以有一個或者多個。

用一張圖來說明藍芽4.0的組成:
藍芽4.0的組成

開發流程

1. 獲取相關許可權

在AndroidManifest.xml宣告相關許可權

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

<!-- 似乎android M需要位置許可權才能掃描 -->
<uses-permission 
android:name="android.permission.WRITE_SETTINGS" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

2. 獲取BluetoothManager和BluetoothAdapter

BluetoothManager manager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
BluetoothAdapter bleAdapter = manager.getAdapter();

// 也可以通過以下方式獲取BluetoothAdapter
BluetoothAdapter bleAdapter = BluetoothAdapter.getDefaultAdapter();

3. 檢測藍芽是否開啟或可用

public boolean check() {
    return (null != bleAdapter && bleAdapter.isEnabled() && !bleAdapter.isDiscovering());
}

4. 掃描藍芽並實現回撥介面LeScanCallback

// 開始掃描
public void startScan(long scanDelay) {
    if(!check()) return;  // 檢測藍芽
    BleScanCallback leScanCallback = new BleScanCallback();  // 回撥介面
        if (bleAdapter.startLeScan(leScanCallback)) {
        timer.schedule(new TimerTask() {  // 掃描一定時間"scanDelay"就停止。
            @Override
            public void run() {
                stopScan();
            }
        }, scanDelay);
    }
}

// 實現掃描回撥介面
private class BleScanCallback implements BluetoothAdapter.LeScanCallback {
    // 掃描到新裝置時,會回撥該介面。可以將新裝置顯示在ui中,看具體需求
    @Override
    public void onLeScan(BluetoothDevice device, int rssi, byte[] scanRecord) {
        Log.e(TAG, "found device name : " + device.getName() + " address : " + device.getAddress());
    }
}

有一個地方可以注意下,在API 21及以上,不推薦使用BluetoothAdapter.startLeScan和stopLeScan。而是新引入了BluetoothLeScanner類ScanCallback回撥介面。由於需要相容API 19,所以這裡不作過多的闡述,有需要可以檢視官方文件瞭解。

5. 連線藍芽裝置並實現連接回調介面

// 連線藍芽裝置,device為之前掃描得到的
public void connect(BluetoothDevice device) {
    if(!check()) return;  // 檢測藍芽
    if (null != bleAdapter) {
        bleAdapter.stopLeScan(leScanCallback);
    }
    if (bleGatt.connect()) {  // 已經連線了其他裝置
        // 如果是先前連線的裝置,則不做處理            
        if (TextUtils.equals(device.getAddress(), bleGatt.getDevice().getAddress())) {         
             return;
        } else {
            disconnect();  // 否則斷開連線
        }
    }
    // 連線裝置,第二個引數為是否自動連線,第三個為回撥函式
    bleGatt = device.connectGatt(context, false, bleGattCallback);
}

// 實現連接回調介面[關鍵]
private class BleGattCallback extends BluetoothGattCallback {

    // 連線狀態改變(連線成功或失敗)時回撥該介面
    @Override
    public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
        if (newState == BluetoothGatt.STATE_CONNECTED) {   // 連線成功
            Intent intent = new Intent(ActionBle.CONNECTED);
            context.sendBroadcast(intent);    // 這裡是通過廣播通知連線成功,依各自的需求決定
            gatt.discoverServices();   // 則去搜索裝置的服務(Service)和服務對應Characteristic
        } else {   // 連線失敗
            Intent intent = new Intent(ActionBle.CONNECT_FAIL);
            context.sendBroadcast(intent);
        }
    }

    // 發現裝置的服務(Service)回撥,需要在這裡處理訂閱事件。
    @Override
    public void onServicesDiscovered(BluetoothGatt gatt, int status) {
        if (status == BluetoothGatt.GATT_SUCCESS) {
            services = gatt.getServices();
            characteristics = new ArrayList<>();
            descriptors = new ArrayList<>();

            for (BluetoothGattService service : services) {
                Log.e(TAG, "-- service uuid : " + service.getUuid().toString() + " --");
                for (BluetoothGattCharacteristic characteristic : service.getCharacteristics()) {
                    Log.e(TAG, "---- characteristic uuid : " + characteristic.getUuid() + " ----");
                    characteristics.add(characteristic);

                    if (characteristic.getUuid().toString().equals(Command.READ_UUID)) {
                        // 訂閱資訊,否則接收不到資料「關鍵!」
                        setCharacteristicNotification(characteristic, true);
                    }
                    for (BluetoothGattDescriptor descriptor : characteristic.getDescriptors()) {
                        Log.e(TAG, "-------- descriptor uuid : " + characteristic.getUuid() + " --------");
                        descriptors.add(descriptor);
                    }
                }
            }
            Intent intent = new Intent(ActionBle.DISCOVER_SERVICES_SUCCESS);
            context.sendBroadcast(intent);
        } else {
            Intent intent = new Intent(ActionBle.DISCOVER_SERVICES_FAIL);
            context.sendBroadcast(intent);
        }
    }
    // 傳送訊息結果回撥
    @Override
    public void onCharacteristicWrite(BluetoothGatt gatt,
                                      BluetoothGattCharacteristic characteristic,
                                      int status) {
        Intent intent;
        if (BluetoothGatt.GATT_SUCCESS == status) {   // 傳送成功
            intent = new Intent(ActionBle.WRITE_DATA_SUCCESS);
        } else {    // 傳送失敗
            intent = new Intent(ActionBle.WRITE_DATA_FAIL);
        }
        context.sendBroadcast(intent);
    }

    // 當訂閱的Characteristic接收到訊息時回撥 
    @Override
    public void onCharacteristicChanged(BluetoothGatt gatt,
                                        BluetoothGattCharacteristic characteristic) {
        // 資料為 characteristic.getValue())
        Log.e(TAG, "onCharacteristicChanged: " + Arrays.toString(characteristic.getValue()));
    }
}

// 訂閱特徵集!「關鍵,才能接收到資料」
// public static final String DESCRIPTORS_UUID = "00002902-0000-1000-8000-00805f9b34fb";
// 這個uuid一般是固定的,不一樣視情況改變
private boolean setCharacteristicNotification(BluetoothGattCharacteristic characteristic, boolean enabled) {
    if (bleGatt == null) {
        return false;
    }
    BluetoothGattDescriptor localBluetoothGattDescriptor = characteristic.getDescriptor(UUID.fromString(DESCRIPTORS_UUID));
    if (enabled) {
        localBluetoothGattDescriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
    } else {
        localBluetoothGattDescriptor.setValue(BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE);
    }
    bleGatt.writeDescriptor(localBluetoothGattDescriptor);
    return bleGatt.setCharacteristicNotification(characteristic, enabled);
}

連線裝置的回撥函式很關鍵,有幾個點需要注意:
1. 當裝置連線狀態改變(連線成功或失敗)時,會回撥onConnectionStateChange介面,需要在連線成功的時候呼叫gatt.discoverServices();去搜索裝置的服務Service集合。
2. 當裝置搜尋服務狀態(成功找到服務或失敗)時,會回撥onServicesDiscovered介面,這裡面很關鍵,需要處理訂閱相關特徵Characteristic:
bleGatt.setCharacteristicNotification(characteristic, true);
這個需要依據藍芽模組協議而定,只有在這裡訂閱了,才能接收到藍芽模組傳送過來的資料。
這裡還有個大坑,具體看setCharacteristicNotification。
3. 訂閱的特徵Characteristic接收到訊息,也就是藍芽模組傳送資料過來時,會回撥onCharacteristicChanged介面,資料的處理就在該介面處理。

6. 向藍芽模組傳送資料

其實也很簡單,根據協議往相關特徵寫入資料即可。

public boolean writeCharacteristic(String uuid, byte[] bytes) {
    if (null == characteristics) {
        return false;
    }
    Log.e(TAG, "writeCharacteristic to " + uuid + " : " + Arrays.toString(bytes));
    if (null != bleGatt) {
        // characteristics儲存了藍芽模組所有的特徵Characteristic
        for (BluetoothGattCharacteristic characteristic : characteristics) {
            // 判斷是否為協議約定的特徵Characteristic
            if (TextUtils.equals(uuid, characteristic.getUuid().toString())) {
                // 找到特徵,設定要寫入的資料
                characteristic.setValue(bytes);
                // 寫入資料,藍芽模組就接收到啦
                return bleGatt.writeCharacteristic(characteristic);
            }
        }
    }
    return false;
}

注意這裡會回撥上面實現的BluetoothGattCallback.onCharacteristicWrite介面
這裡我用的藍芽模組約定傳送和接收的特徵uuid如下:

String READ_UUID =  "6e400003-b5a3-f393-e0a9-e50e24dcca9e"; // 讀
String WRITE_UUID = "6e400002-b5a3-f393-e0a9-e50e24dcca9e"; // 寫

具體藍芽模組看具體的協議而定。

7. 斷開連線

public void disconnect() {    
    if (null != bleGatt) {
        bleGatt.disconnect();
    }  
    Log.e(TAG, "disconnect");
}

8. 資料的轉換

與藍芽模組的通訊一般都是採用16進位制,byte[]傳輸,因此需提供幾個格式轉換的方法。

// byte轉十六進位制字串
public static String bytes2HexString(byte[] bytes) {
    String ret = "";
    for (byte item : bytes) {
        String hex = Integer.toHexString(item & 0xFF);
        if (hex.length() == 1) {
            hex = '0' + hex;
        }
        ret += hex.toUpperCase(Locale.CHINA);
    }
    return ret;
}

// 將16進位制的字串轉換為位元組陣列 
public static byte[] getHexBytes(String message) {
    int len = message.length() / 2;
    char[] chars = message.toCharArray();
    String[] hexStr = new String[len];
    byte[] bytes = new byte[len];
    for (int i = 0, j = 0; j < len; i += 2, j++) {
        hexStr[j] = "" + chars[i] + chars[i + 1];
        bytes[j] = (byte) Integer.parseInt(hexStr[j], 16);
    }
    return bytes;
}

以上就是android 藍芽4.0的基本操作啦,可以實現基本的通訊啦,瑟瑟發抖。