1. 程式人生 > >Android-低功耗藍芽(BLE)-客戶端(主機/中心裝置)和服務端(從機/外圍裝置)

Android-低功耗藍芽(BLE)-客戶端(主機/中心裝置)和服務端(從機/外圍裝置)

一.Android 低功耗藍芽(BLE)的API簡介

從Android 4.3(API 18)才支援低功耗藍芽(Bluetooth Low Energy, BLE)的核心功能,
BLE藍芽協議是GATT協議, BLE相關類不多, 全都位於android.bluetooth包和android.bluetooth.le包的幾個類:
android.bluetooth.
  .BluetoothGattService  包含多個Characteristic(屬性特徵值), 含有唯一的UUID作為標識
  .BluetoothGattCharacteristic  包含單個值和多個Descriptor, 含有唯一的UUID作為標識
  .BluetoothGattDescriptor  對Characteristic進行描述, 含有唯一的UUID作為標識

  .BluetoothGatt   客戶端相關
  .BluetoothGattCallback  客戶端連接回調     
  .BluetoothGattServer  服務端相關
  .BluetoothGattServerCallback 服務端連接回調

android.bluetooth.le.
  .AdvertiseCallback  服務端的廣播回撥
  .AdvertiseData  服務端的廣播資料
  .AdvertiseSettings 服務端的廣播設定
  .BluetoothLeAdvertiser 服務端的廣播

  .BluetoothLeScanner  客戶端掃描相關(Android5.0新增)
  .ScanCallback  客戶端掃描回撥
  .ScanFilter 客戶端掃描過濾
  .ScanRecord 客戶端掃描結果的廣播資料
  .ScanResult 客戶端掃描結果
  .ScanSettings 客戶端掃描設定

BLE裝置分為兩種裝置: 客戶端(也叫主機/中心裝置/Central), 服務端(也叫從機/外圍裝置/peripheral)
客戶端的核心類是 BluetoothGatt
服務端的核心類是 BluetoothGattServer 和 BluetoothLeAdvertiser
BLE資料的核心類是 BluetoothGattCharacteristic 和 BluetoothGattDescriptor

二.低功耗藍芽(BLE)-手機同時作為BLE客戶端和BLE服務端,讀寫Characteristic資料

1.藍芽許可權

BLE許可權增加了BEL支援檢查,其它與上篇的經典藍芽相同,不再寫了
(1).在manifest中新增許可權
<!-- true 表示手機必須支援BLE,否則無法安裝!
    這裡設為false, 執行後在Activity中檢查-->
<uses-feature
    android:name="android.hardware.bluetooth_le"
    android:required="false" />

(2).在Activity中設定藍芽
// 檢查是否支援BLE藍芽
if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
    Util.toast(this, "本機不支援低功耗藍芽!");
    finish();
    return;
}

2.BLE客戶端(也叫主機/中心裝置/Central)

(1).掃描BLE裝置(不包含經典藍芽)

注意: BLE裝置地址是動態變化(每隔一段時間都會變化),而經典藍芽裝置是出廠就固定不變了!
BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
// 下面使用Android5.0新增的掃描API,掃描返回的結果更友好,比如BLE廣播資料以前是byte[] scanRecord,而新API幫我們解析成ScanRecord類\
// 舊API是BluetoothAdapter.startLeScan(...)
final BluetoothLeScanner bluetoothLeScanner = bluetoothAdapter.getBluetoothLeScanner();
bluetoothLeScanner.startScan(mScanCallback);
mHandler.postDelayed(new Runnable() {
    @Override
    public void run() {
        bluetoothLeScanner.stopScan(mScanCallback); //停止掃描
        isScanning = false;
    }
}, 3000);

// 掃描結果Callback
private final ScanCallback mScanCallback = new ScanCallback() {
    @Override
    public void onScanResult(int callbackType, ScanResult result) {、
        BluetoothDevice dev = result.getDevice() 獲取BLE裝置資訊
        // result.getScanRecord() 獲取BLE廣播資料
    }
};

(2).建立連線

// 獲取掃描裝置,建立連線
closeConn();
BluetoothDevice dev = result.getDevice()
mBluetoothGatt = dev.connectGatt(BleClientActivity.this, false, mBluetoothGattCallback); // 連線藍芽裝置

// BLE中心裝置連線外圍裝置的數量有限(大概2~7個),在建立新連線之前必須釋放舊連線資源,否則容易出現連線錯誤133
private void closeConn() {
    if (mBluetoothGatt != null) {
        mBluetoothGatt.disconnect();
        mBluetoothGatt.close();
    }
}

// 與服務端連線的Callback
public BluetoothGattCallback mBluetoothGattCallback = new BluetoothGattCallback() {
    @Override
    public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
        BluetoothDevice dev = gatt.getDevice();
        Log.i(TAG, String.format("onConnectionStateChange:%s,%s,%s,%s", dev.getName(), dev.getAddress(), status, newState));
        if (status == BluetoothGatt.GATT_SUCCESS && newState == BluetoothProfile.STATE_CONNECTED) {
            isConnected = true;
            gatt.discoverServices(); //啟動服務發現
        } else {
            isConnected = false;
            closeConn();
        }
        logTv(String.format(status == 0 ? (newState == 2 ? "與[%s]連線成功" : "與[%s]連線斷開") : ("與[%s]連接出錯,錯誤碼:" + status), dev));
    }

    @Override
    public void onServicesDiscovered(BluetoothGatt gatt, int status) {
        Log.i(TAG, String.format("onServicesDiscovered:%s,%s,%s", gatt.getDevice().getName(), gatt.getDevice().getAddress(), status));
        if (status == BluetoothGatt.GATT_SUCCESS) { //BLE服務發現成功
            // 遍歷獲取BLE服務Services/Characteristics/Descriptors的全部UUID
            for (BluetoothGattService service : gatt.getServices()) {
                StringBuilder allUUIDs = new StringBuilder("UUIDs={\nS=" + service.getUuid().toString());
                for (BluetoothGattCharacteristic characteristic : service.getCharacteristics()) {
                    allUUIDs.append(",\nC=").append(characteristic.getUuid());
                    for (BluetoothGattDescriptor descriptor : characteristic.getDescriptors())
                        allUUIDs.append(",\nD=").append(descriptor.getUuid());
                }
                allUUIDs.append("}");
                Log.i(TAG, "onServicesDiscovered:" + allUUIDs.toString());
                logTv("發現服務" + allUUIDs);
            }
        }
    }

    @Override
    public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
        UUID uuid = characteristic.getUuid();
        String valueStr = new String(characteristic.getValue());
        Log.i(TAG, String.format("onCharacteristicRead:%s,%s,%s,%s,%s", gatt.getDevice().getName(), gatt.getDevice().getAddress(), uuid, valueStr, status));
        logTv("讀取Characteristic[" + uuid + "]:\n" + valueStr);
    }

    @Override
    public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
        UUID uuid = characteristic.getUuid();
        String valueStr = new String(characteristic.getValue());
        Log.i(TAG, String.format("onCharacteristicWrite:%s,%s,%s,%s,%s", gatt.getDevice().getName(), gatt.getDevice().getAddress(), uuid, valueStr, status));
        logTv("寫入Characteristic[" + uuid + "]:\n" + valueStr);
    }

    @Override
    public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
        UUID uuid = characteristic.getUuid();
        String valueStr = new String(characteristic.getValue());
        Log.i(TAG, String.format("onCharacteristicChanged:%s,%s,%s,%s", gatt.getDevice().getName(), gatt.getDevice().getAddress(), uuid, valueStr));
        logTv("通知Characteristic[" + uuid + "]:\n" + valueStr);
    }

    @Override
    public void onDescriptorRead(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
        UUID uuid = descriptor.getUuid();
        String valueStr = Arrays.toString(descriptor.getValue());
        Log.i(TAG, String.format("onDescriptorRead:%s,%s,%s,%s,%s", gatt.getDevice().getName(), gatt.getDevice().getAddress(), uuid, valueStr, status));
        logTv("讀取Descriptor[" + uuid + "]:\n" + valueStr);
    }

    @Override
    public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
        UUID uuid = descriptor.getUuid();
        String valueStr = Arrays.toString(descriptor.getValue());
        Log.i(TAG, String.format("onDescriptorWrite:%s,%s,%s,%s,%s", gatt.getDevice().getName(), gatt.getDevice().getAddress(), uuid, valueStr, status));
        logTv("寫入Descriptor[" + uuid + "]:\n" + valueStr);
    }
};

(3).傳輸資料(讀寫Characteristic和Descriptor)

注意:
    1.每次讀寫資料最多20個位元組,如果超過,只能分包
    2.連續頻繁讀寫資料容易失敗,讀寫操作間隔最好200ms以上,或等待上次回撥完成後再進行下次讀寫操作!
// 讀取資料成功會回撥->onCharacteristicChanged()
public void read(View view) {
    BluetoothGattService service = getGattService(BleServerActivity.UUID_SERVICE);
    if (service != null) {
        BluetoothGattCharacteristic characteristic = service.getCharacteristic(BleServerActivity.UUID_CHAR_READ_NOTIFY);//通過UUID獲取可讀的Characteristic
        mBluetoothGatt.readCharacteristic(characteristic);
    }
}

// 寫入資料成功會回撥->onCharacteristicWrite()
public void write(View view) {
    BluetoothGattService service = getGattService(BleServerActivity.UUID_SERVICE);
    if (service != null) {
        String text = mWriteET.getText().toString();
        BluetoothGattCharacteristic characteristic = service.getCharacteristic(BleServerActivity.UUID_CHAR_WRITE);//通過UUID獲取可寫的Characteristic
        characteristic.setValue(text.getBytes()); //單次最多20個位元組
        mBluetoothGatt.writeCharacteristic(characteristic);
    }
}

// 獲取Gatt服務
private BluetoothGattService getGattService(UUID uuid) {
    BluetoothGattService service = mBluetoothGatt.getService(uuid);
    if (service == null)
        Util.toast(this, "沒有找到服務UUID=" + uuid);
    return service;
}

(4).設定通知,實時監聽Characteristic變化

// Characteristic變化會回撥->onCharacteristicChanged()
BluetoothGattService service = getGattService(BleServerActivity.UUID_SERVICE);
if (service != null) {
    // 設定Characteristic通知
    BluetoothGattCharacteristic characteristic = service.getCharacteristic(BleServerActivity.UUID_CHAR_READ_NOTIFY);//通過UUID獲取可通知的Characteristic
    mBluetoothGatt.setCharacteristicNotification(characteristic, true);

    // 向Characteristic的Descriptor屬性寫入通知開關,使藍芽裝置主動向手機發送資料
    BluetoothGattDescriptor descriptor = characteristic.getDescriptor(BleServerActivity.UUID_DESC_NOTITY);
    // descriptor.setValue(BluetoothGattDescriptor.ENABLE_INDICATION_VALUE);//和通知類似,但服務端不主動發資料,只指示客戶端讀取資料
    descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
    mBluetoothGatt.writeDescriptor(descriptor);
}

3.BLE服務端(也叫從機/外圍裝置/peripheral)

public static final UUID UUID_SERVICE = UUID.fromString("10000000-0000-0000-0000-000000000000"); //自定義UUID
public static final UUID UUID_CHAR_READ_NOTIFY = UUID.fromString("11000000-0000-0000-0000-000000000000");
public static final UUID UUID_DESC_NOTITY = UUID.fromString("11100000-0000-0000-0000-000000000000");
public static final UUID UUID_CHAR_WRITE = UUID.fromString("12000000-0000-0000-0000-000000000000");
private BluetoothLeAdvertiser mBluetoothLeAdvertiser; // BLE廣播
private BluetoothGattServer mBluetoothGattServer; // BLE服務端

@Override
protected void onCreate(Bundle savedInstanceState) {
    ......
    BluetoothManager bluetoothManager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
    // BluetoothAdapter bluetoothAdapter = bluetoothManager.getAdapter();
    BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();

    // ============啟動BLE藍芽廣播(廣告) =================================================================================
    //廣播設定(必須)
    AdvertiseSettings settings = new AdvertiseSettings.Builder()
            .setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_LOW_LATENCY) //廣播模式: 低功耗,平衡,低延遲
            .setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_HIGH) //發射功率級別: 極低,低,中,高
            .setConnectable(true) //能否連線,廣播分為可連線廣播和不可連線廣播
            .build();
    //廣播資料(必須,廣播啟動就會發送)
    AdvertiseData advertiseData = new AdvertiseData.Builder()
            .setIncludeDeviceName(true) //包含藍芽名稱
            .setIncludeTxPowerLevel(true) //包含發射功率級別
            .addManufacturerData(1, new byte[]{23, 33}) //裝置廠商資料,自定義
            .build();
    //掃描響應資料(可選,當客戶端掃描時才傳送)
    AdvertiseData scanResponse = new AdvertiseData.Builder()
            .addManufacturerData(2, new byte[]{66, 66}) //裝置廠商資料,自定義
            .addServiceUuid(new ParcelUuid(UUID_SERVICE)) //服務UUID
    //      .addServiceData(new ParcelUuid(UUID_SERVICE), new byte[]{2}) //服務資料,自定義
            .build();
    mBluetoothLeAdvertiser = bluetoothAdapter.getBluetoothLeAdvertiser();
    mBluetoothLeAdvertiser.startAdvertising(settings, advertiseData, scanResponse, mAdvertiseCallback);

    // 注意:必須要開啟可連線的BLE廣播,其它裝置才能發現並連線BLE服務端!
    // =============啟動BLE藍芽服務端=====================================================================================
    BluetoothGattService service = new BluetoothGattService(UUID_SERVICE, BluetoothGattService.SERVICE_TYPE_PRIMARY);
    //新增可讀+通知characteristic
    BluetoothGattCharacteristic characteristicRead = new BluetoothGattCharacteristic(UUID_CHAR_READ_NOTIFY,
            BluetoothGattCharacteristic.PROPERTY_READ | BluetoothGattCharacteristic.PROPERTY_NOTIFY, BluetoothGattCharacteristic.PERMISSION_READ);
    characteristicRead.addDescriptor(new BluetoothGattDescriptor(UUID_DESC_NOTITY, BluetoothGattCharacteristic.PERMISSION_WRITE));
    service.addCharacteristic(characteristicRead);
    //新增可寫characteristic
    BluetoothGattCharacteristic characteristicWrite = new BluetoothGattCharacteristic(UUID_CHAR_WRITE,
            BluetoothGattCharacteristic.PROPERTY_WRITE, BluetoothGattCharacteristic.PERMISSION_WRITE);
    service.addCharacteristic(characteristicWrite);
    if (bluetoothManager != null)
        mBluetoothGattServer = bluetoothManager.openGattServer(this, mBluetoothGattServerCallback);
    mBluetoothGattServer.addService(service);
}

// BLE廣播Callback
private AdvertiseCallback mAdvertiseCallback = new AdvertiseCallback() {
    @Override
    public void onStartSuccess(AdvertiseSettings settingsInEffect) {
        logTv("BLE廣播開啟成功");
    }

    @Override
    public void onStartFailure(int errorCode) {
        logTv("BLE廣播開啟失敗,錯誤碼:" + errorCode);
    }
};

// BLE服務端Callback
private BluetoothGattServerCallback mBluetoothGattServerCallback = new BluetoothGattServerCallback() {
    @Override
    public void onConnectionStateChange(BluetoothDevice device, int status, int newState) {
        Log.i(TAG, String.format("onConnectionStateChange:%s,%s,%s,%s", device.getName(), device.getAddress(), status, newState));
        logTv(String.format(status == 0 ? (newState == 2 ? "與[%s]連線成功" : "與[%s]連線斷開") : ("與[%s]連接出錯,錯誤碼:" + status), device));
    }

    @Override
    public void onServiceAdded(int status, BluetoothGattService service) {
        Log.i(TAG, String.format("onServiceAdded:%s,%s", status, service.getUuid()));
        logTv(String.format(status == 0 ? "新增服務[%s]成功" : "新增服務[%s]失敗,錯誤碼:" + status, service.getUuid()));
    }

    @Override
    public void onCharacteristicReadRequest(BluetoothDevice device, int requestId, int offset, BluetoothGattCharacteristic characteristic) {
        Log.i(TAG, String.format("onCharacteristicReadRequest:%s,%s,%s,%s,%s", device.getName(), device.getAddress(), requestId, offset, characteristic.getUuid()));
        String response = "CHAR_" + (int) (Math.random() * 100); //模擬資料
        mBluetoothGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, response.getBytes());// 響應客戶端
        logTv("客戶端讀取Characteristic[" + characteristic.getUuid() + "]:\n" + response);
    }

    @Override
    public void onCharacteristicWriteRequest(BluetoothDevice device, int requestId, BluetoothGattCharacteristic characteristic, boolean preparedWrite, boolean responseNeeded, int offset, byte[] requestBytes) {
        // 獲取客戶端發過來的資料
        String requestStr = new String(requestBytes);
        Log.i(TAG, String.format("onCharacteristicWriteRequest:%s,%s,%s,%s,%s,%s,%s,%s", device.getName(), device.getAddress(), requestId, characteristic.getUuid(),
                preparedWrite, responseNeeded, offset, requestStr));
        mBluetoothGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, requestBytes);// 響應客戶端
        logTv("客戶端寫入Characteristic[" + characteristic.getUuid() + "]:\n" + requestStr);
    }

    @Override
    public void onDescriptorReadRequest(BluetoothDevice device, int requestId, int offset, BluetoothGattDescriptor descriptor) {
        Log.i(TAG, String.format("onDescriptorReadRequest:%s,%s,%s,%s,%s", device.getName(), device.getAddress(), requestId, offset, descriptor.getUuid()));
        String response = "DESC_" + (int) (Math.random() * 100); //模擬資料
        mBluetoothGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, response.getBytes()); // 響應客戶端
        logTv("客戶端讀取Descriptor[" + descriptor.getUuid() + "]:\n" + response);
    }

    @Override
    public void onDescriptorWriteRequest(final BluetoothDevice device, int requestId, BluetoothGattDescriptor descriptor, boolean preparedWrite, boolean responseNeeded, int offset, byte[] value) {
        // 獲取客戶端發過來的資料
        String valueStr = Arrays.toString(value);
        Log.i(TAG, String.format("onDescriptorWriteRequest:%s,%s,%s,%s,%s,%s,%s,%s", device.getName(), device.getAddress(), requestId, descriptor.getUuid(),
                preparedWrite, responseNeeded, offset, valueStr));
        mBluetoothGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, value);// 響應客戶端
        logTv("客戶端寫入Descriptor[" + descriptor.getUuid() + "]:\n" + valueStr);

        // 簡單模擬通知客戶端Characteristic變化
        if (Arrays.toString(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE).equals(valueStr)) { //是否開啟通知
            final BluetoothGattCharacteristic characteristic = descriptor.getCharacteristic();
            new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < 5; i++) {
                        SystemClock.sleep(3000);
                        String response = "CHAR_" + (int) (Math.random() * 100); //模擬資料
                        characteristic.setValue(response);
                        mBluetoothGattServer.notifyCharacteristicChanged(device, characteristic, false);
                        logTv("通知客戶端改變Characteristic[" + characteristic.getUuid() + "]:\n" + response);
                    }
                }
            }).start();
        }
    }

    @Override
    public void onExecuteWrite(BluetoothDevice device, int requestId, boolean execute) {
        Log.i(TAG, String.format("onExecuteWrite:%s,%s,%s,%s", device.getName(), device.getAddress(), requestId, execute));
    }

    @Override
    public void onNotificationSent(BluetoothDevice device, int status) {
        Log.i(TAG, String.format("onNotificationSent:%s,%s,%s", device.getName(), device.getAddress(), status));
    }

    @Override
    public void onMtuChanged(BluetoothDevice device, int mtu) {
        Log.i(TAG, String.format("onMtuChanged:%s,%s,%s", device.getName(), device.getAddress(), mtu));
    }
};

};