Android低功耗藍牙開發


參考

https://developer.android.com/guide/topics/connectivity/bluetooth-le

https://www.jianshu.com/p/3a372af38103

簡介

最近公司有個連接設備商藍牙的小功能,於是把藍牙相關的api簡單過了一下,基本可以開發了。

 

Android 4.3(api 18)引入了 藍牙低功耗的支持,並提供了能夠用來發現設備,查詢service,傳輸信息的api。

當一個用戶用他的設備 用ble和其他的設備配對時,兩個設備間的數據傳輸是能被用戶設備的所有app訪問到的。因此,如果你的應用程序捕獲敏感數據,你應該實現自己的應用層的協議來保護這些數據的隱私。

 

開發流程

權限聲明

首先你需要聲明bluetooth權限,這個權限是藍牙的基礎權限,其他的操作藍牙都需要先聲明此權限。

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

 

如果你要使用藍牙掃描周圍設備(BluetoothAdapter.startDiscovery,或BluetoothLeScanner.startScan),或者操作藍牙的設置,你就需要此權限。

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

 

另外,如果你的app是運行在Android8.0以下的設備上時,因為可以被發現的設備可能會暴露其位置,你需要ACCESS_FINE_LOCATION權限,

<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
  • 此權限是危險權限,所以需要運行時動態申請。
  • 既然是訪問定位,那么系統的定位功能必須的打開才行。

 

如果你的app是運行在Android8.0+的設備上時,你可以使用CompanionDeviceManager的api,CompanionDeviceManager將代表您的應用程序對附近設備執行藍牙或Wi-Fi掃描,而無需訪問ACCESS_FINE_LOCATION或BLUETOOTH_ADMIN權限。當然如果不使用CompanionDeviceManager的話就需要按上邊的申請權限了。

 

如果你的app是需要支持ble的設備才能運行,就需要在清單文件中聲明,

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

但如果你也想讓不支持ble的設備也能運行,也需要上邊的聲明,並把required設置為false,但你需要在代碼中自行判斷:

if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
    Toast.makeText(this, R.string.ble_not_supported, Toast.LENGTH_SHORT).show();
    return;
}

 

啟動藍牙

1. 在Android4.3通過getSystemService獲取

finaBluetoothManager bluetoothManager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
BluetoothAdapter bluetoothAdapter = bluetoothManager.getAdapter();

 

2. 然后啟用藍牙

if (bluetoothAdapter == nul|| !bluetoothAdapter.isEnabled()) {
    Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
    startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
}

之后會啟動一個系統彈窗,如下圖:

wps13

REQUEST_ENABLE_BT是我們自定義的>=0,之后會在onActivityForResult中收到啟動結果,RESULT_OK表示啟動成功,RESULT_CANCELED表示取消。

 

獲取當前已配對的外圍設備

第一次和外部藍牙設備連接時,配對請求就會自動的顯示給用戶,當配對成功后,設備的基本信息(如設備名,設備類型,mac地址等信息)就會被存儲到手機中,可以通過getBondedDevices(需要先開啟藍牙)訪問到。

之后再次連接時,就可以直接使用已配對的已知的mac地址直接和遠程設備建立連接,而不需要先進行掃描(並不是真的不掃描,只是我們不用手動調用),當然前提時外圍設備在我們能掃描到的范圍內。

 

配對和連接的區別:

  • 配對是兩個設備之間互相存儲了對方的設備信息,以及一個溝通的密鑰。
  • 連接是兩個設備間共享同一個RFCOMM通道,可以進行互傳數據。在Android api中,在連接之前會自動的先配對。

 

所以在掃描之前有必要先獲取當前已配對的設備,看有沒有我們想要進行連接的設備。

Set<BluetoothDevice> pairedDevices = bluetoothAdapter.getBondedDevices();

if (pairedDevices.size() > 0) {
    // There are paired devices. Get the name and address of each paired device.
    for (BluetoothDevice device : pairedDevices) {
        String deviceName = device.getName();
        String deviceHardwareAddress = device.getAddress(); // MAC address
    }
}

 

掃描ble設備

如果已配對的設備沒有我們想要的,那么再進行掃描周圍的藍牙設備。

 

您只能掃描Bluetooth LE設備 或 傳統藍牙設備,如Bluetooth中所述。 您不能同時掃描Bluetooth LE和傳統設備。

 

因為掃描是一個耗電的操作,所以您應該遵守以下准則:

  • 一旦你找到期望的設備就應該停止掃描。
  • 切勿循環掃描,並為掃描設置時間限制。 先前可用的設備可能已超出范圍,並且繼續掃描會耗盡電池電量。
private BluetoothLeScanner bluetoothLeScanner =
        BluetoothAdapter.getDefaultAdapter().getBluetoothLeScanner();
private boolean mScanning;
private Handler handler = new Handler();

// Stops scanning after 10 seconds.
private static final long SCAN_PERIOD = 10000;

private void scanLeDevice() {
    if (!mScanning) {
        // Stops scanning after a pre-defined scan period.
        handler.postDelayed(new Runnable() {
            @Override
            public void run() {
                mScanning = false;
                bluetoothLeScanner.stopScan(leScanCallback);
            }
        }, SCAN_PERIOD);

        mScanning = true;
        bluetoothLeScanner.startScan(leScanCallback);
    } else {
        mScanning = false;
        bluetoothLeScanner.stopScan(leScanCallback);
    }
}
  • bluetoothLeScanner.startScan有幾個重載方法,都是用來過濾掃描到的設備的。

startScan(ScanCallback)是不進行過濾,那么當屏幕關閉時為了省電會暫停掃描,屏幕再打開時恢復掃描,為了防止這樣,請使用帶有過濾的startScan。

 

  • bluetoothLeScanner.stopScan時必須要傳入和startScan傳入的callback一樣的callback才能停止。

 

ScanCallback:

private LeDeviceListAdapter leDeviceListAdapter;

// Device scan callback.
private ScanCallback leScanCallback =
        new ScanCallback() {
            @Override
            public void onScanResult(int callbackType, ScanResult result) {
                super.onScanResult(callbackType, result);
                leDeviceListAdapter.addDevice(result.getDevice());
                leDeviceListAdapter.notifyDataSetChanged();
            }
        };
  • 此回調是在主線程的。
  • callbackType:表示這個回調是如何觸發的,它是在ScanSettings中配置的callbackType,
  • ScanResult包含了下邊幾個重要數據:
    • BluetoothDevice:掃描到的藍牙設備的抽象,可以用此對象進行配對連接。
    • int Rssi:received signastrength in dBm,接收到的此藍牙設備的信號強度
    • ScanRecord:表示掃描到的藍牙設備的廣播數據,

 

連接GATT server

和傳統藍牙的連接不同的是,我們可以使用已經封裝好的Android api來進行連接ble設備,而不用再去創建通道,然后建立socket等操作。

與BLE設備交互的第一步是連接到它——更具體地說,連接到設備上的GATT服務器。

 

在第一次連接的時候會彈 一個系統彈窗 或 通知,讓用戶進行配對,

wps14

 

 

要連接到BLE設備上的GATT服務器,可以使用BluetoothDevice.connectGatt()方法。

public BluetoothGatt connectGatt(Context context, boolean autoConnect, BluetoothGattCallback callback)
  • autoConnect:表示是否在BLE設備可用時自動連接。

關於autoConnect參數為true的意義?

在藍牙核心文檔Vol3: Core System Package[Host volume]->Part C: Generic Access Profile的Connection Modes and Procedures章節中有涉及到自動連接建立規程(Auto Connection Establishment Procedure)的定義。

自動連接建立規程用來向多個設備同時發起連接。一個中央設備的主機與多個外圍設備綁定,只要它們開始廣播,便立刻與其建立連接。跟多細節請參考藍牙核心文檔和協議棧源碼。

 

  • BluetoothGattCallback :是用來把一些信息返回給GATT client,比如連接狀態的改變,GATT client的一些操作的結果。所有的回調都是后台線程上的。

 

 

當調用藍牙的連接方法之后,藍牙會異步執行藍牙連接的操作,如果連接成功會回調 BluetoothGattCalbackl#onConnectionStateChange 方法。這個方法運行的線程是一個 Binder 線程,所以不建議直接在這個線程處理耗時的任務,因為這可能導致藍牙相關的線程被阻塞。

public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState)
  • status:代表是否成功執行了連接操作,如果為 BluetoothGatt.GATT_SUCCESS 表示成功執行連接操作,第三個參數才有效,否則說明這次連接嘗試不成功。有時候,我們會遇到 status == 133 的情況,根據網上大部分人的說法,這是因為 Android 最多支持連接 6 到 7 個左右的藍牙設備,如果超出了這個數量就無法再連接了。所以當我們斷開藍牙設備的連接時,還必須調用 BluetoothGatt#close 方法釋放連接資源。否則,在多次嘗試連接藍牙設備之后很快就會超出這一個限制,導致出現這一個錯誤再也無法連接藍牙設備。

 

  • newState:代表當前設備的連接狀態,如果 newState == BluetoothProfile.STATE_CONNECTED 說明設備已經連接,可以進行下一步的操作了(發現藍牙服務,也就是 Service)。當藍牙設備斷開連接時,這一個方法也會被回調,其中的 newState == BluetoothProfile.STATE_DISCONNECTED。

 

獲取service

1. 在連接成功后,可以通過BluetoothGatt.discoverServices()來查找外圍設備上支持的service,

2. 此操作是異步操作,之后會在 BluetoothGattCalbackl#onServicesDiscovered回調:

3. 然后我們可以通過BluetoothGatt.getService(UUID uuid)來獲取我們期望的service。

private final BluetoothGattCallback gattCallback =
        new BluetoothGattCallback() {
            @Override
            public void onConnectionStateChange(BluetoothGatt bluetoothGatt, int status,
                                                int newState) {
                String intentAction;
                if (newState == BluetoothProfile.STATE_CONNECTED) {
                    bluetoothGatt.discoverServices();

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

            @Override
            // New services discovered
            public void onServicesDiscovered(BluetoothGatt bluetoothGatt, int status) {
                if (status == BluetoothGatt.GATT_SUCCESS) {
                    bluetoothGatt.getService(uuid);
                } else {
                    Log.w(TAG, "onServicesDiscovered received: " + status);
                }
            }

            @Override
            // Result of a characteristic read operation
            public void onCharacteristicRead(BluetoothGatt gatt,
                                             BluetoothGattCharacteristic characteristic, int status) {
                if (status == BluetoothGatt.GATT_SUCCESS) {

                }
            }
        };

 

獲取Charactristic數據

接着通過 BluetoothGattService#getCharacteristic(UUID uuid)、getCharacteristics()獲取 BluetoothGattCharacteristic。

此時獲取的別沒有Characteristic的value,只有uuid信息。調用BluetoothGatt.readCharacteristic來讀取,之后會在BluetoothGattCallback.onCharacteristicRead回調:

@Override
public void onCharacteristicRead(final BluetoothGatt gatt,
                                    final BluetoothGattCharacteristic characteristic,
                                    final int status) {

    Log.d(TAG, "callback characteristic read status " + status
            + " in thread " + Thread.currentThread());
    if (status == BluetoothGatt.GATT_SUCCESS) {
        Log.d(TAG, "read value: " + characteristic.getValue());
    }
}


BluetoothGattService service = gattt.getService(SERVICE_UUID);
BluetoothGattCharacteristic characteristic = gatt.getCharacteristic(CHARACTER_UUID);
gatt.readCharacteristic(characteristic);

 

向Charactristic寫入數據

1. 調用 BluetoothGattCharactristic#setValue 傳入需要寫入的數據(藍牙最多單次支持 20 個字節數據的傳輸,如果需要傳輸的數據大於這一個字節則需要分包傳輸)。

2. 調用 BluetoothGattCharactristic#writeCharacteristic 方法通知系統異步往設備寫入數據。

3. 系統回調 BluetoothGattCallback#onCharacteristicWrite 方法通知數據已經完成寫入。此時,我們需要執行 BluetoothGattCharactristic#getValue 方法檢查一下寫入的數據是否我們需要發送的數據,如果不是按照項目的需要判斷是否需要重發。

@Override
public void onCharacteristicWrite(final BluetoothGatt gatt,
                                    final BluetoothGattCharacteristic characteristic,
                                    final int status) {
    Log.d(TAG, "callback characteristic write in thread " + Thread.currentThread());
    if(!characteristic.getValue().equal(sendValue)) {
        // 執行重發策略
        gatt.writeCharacteristic(characteristic);
    }
}

//往藍牙數據通道的寫入數據
BluetoothGattService service = gattt.getService(SERVICE_UUID);
BluetoothGattCharacteristic characteristic = gatt.getCharacteristic(CHARACTER_UUID);
characteristic.setValue(sendValue);
gatt.writeCharacteristic(characteristic);

 

超過20字節分包問題

https://stackoverflow.com/questions/24135682/android-sending-data-20-bytes-by-ble

做法是:

  1. 發送第一個20字節后,在onCharacteristicWrite接收到發送成功回調,
  2. 然后在接着發下一個20字節,然后再在onCharacteristicWrite接收到發送成功回調,循環如此直到發完為止。

另外可能和ble設備性能有關,有的ble設備在發送完上次20字節后需要等待幾十毫秒(sleep),才能再發。

 

監聽characteristic 的改變

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

值得注意的是,除了通過 BluetoothGatt#setCharacteristicNotification 開啟 Android 端接收通知的開關,

還需要往 Characteristic 的 Descriptor 屬性寫入開啟通知的數據開關使得當硬件的數據改變時,主動往手機發送數據。

 

之后如果監聽的characteristic的值發生改變,就會在BluetoothGattCallback.onCharacteristicChanged(gatt, characteristic)接收到。

 

斷開連接

當我們連接藍牙設備完成一系列的藍牙操作之后就可以斷開藍牙設備的連接了。

1. BluetoothGatt#disconnect

通過 BluetoothGatt#disconnect 可以斷開正在連接的藍牙設備。當這一個方法被調用之后,系統會異步回調 BluetoothGattCallback#onConnectionStateChange 方法。通過這個方法的 newState 參數可以判斷是連接成功還是斷開成功的回調。

 

2. BluetoothGatt#close

由於 Android 藍牙連接設備的資源有限,當我們執行斷開藍牙操作之后必須執行 BluetoothGatt#close 方法釋放資源。

需要注意的是通過 BluetoothGatt#close 方法也可以執行斷開藍牙的操作,不過 BluetoothGattCallback#onConnectionStateChange 將不會收到任何回調。此時如果執行 BluetoothGatt#connect 方法會得到一個藍牙 API 的空指針異常。所以,我們推薦的寫法是當藍牙成功連接之后,通過 BluetoothGatt#disconnect 斷開藍牙的連接,緊接着在 BluetoothGattCallback#onConnectionStateChange 執行 BluetoothGatt#close 方法釋放資源。

@Override
public void onConnectionStateChange(final BluetoothGatt gatt, final int status,
                                    final int newState) {
    Log.d(TAG, "onConnectionStateChange: thread "
            + Thread.currentThread() + " status " + newState);

    if (status != BluetoothGatt.GATT_SUCCESS) {
        String err = "Cannot connect device with error status: " + status;
        // 當嘗試連接失敗的時候調用 disconnect 方法是不會引起這個方法回調的,所以這里
        //   直接回調就可以了。
        gatt.close();
        Log.e(TAG, err);
        return;
    }

    if (newState == BluetoothProfile.STATE_CONNECTED) {
        gatt.discoverService();
    } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
        gatt.close();
    }
}

 

 

監聽藍牙的狀態的改變

你可以監聽BluetoothAdapter.ACTION_STATE_CHANGED廣播,當系統藍牙的開關狀態發生改變時會通知你,

廣播包含兩個int數據,

  • BluetoothAdapter.EXTRA_STATE:現在的state
  • BluetoothAdapter.EXTRA_PREVIOUS_STATE:表示之前的state

 

state的取值如下:

  • BluetoothAdapter.STATE_TURNING_ON
  • BluetoothAdapter.STATE_ON
  • BluetoothAdapter.STATE_TURNING_OFF
  • BluetoothAdapter.STATE_OFF

 

其他方法

獲取已連接的藍牙設備

  • 直接通過bluetoothManager.getConnectedDevices是獲取不到的,
  • 通過bluetoothManager.getConnectionState也不行
  • 通過反射獲取,
List<BluetoothDevice> connectedDevices = new ArrayList<>();
Class<BluetoothAdapter> bluetoothAdapterClass = BluetoothAdapter.class;//得到BluetoothAdapter的Class對象
try {
    // getConnectionState方法是獲取設備是否和任何一個藍牙設備連接
    Method method = bluetoothAdapterClass.getDeclaredMethod("getConnectionState", (Class[]) null);
    method.setAccessible(true);
    int state = (int) method.invoke(bluetoothAdapter, (Object[]) null);

    if (state == BluetoothAdapter.STATE_CONNECTED) {
        Log.i("BLUETOOTH", "BluetoothAdapter.STATE_CONNECTED");
        // 已連接的設備肯定已經配對過了,所以從這里直接獲取已配對的設備
        Set<BluetoothDevice> devices = bluetoothAdapter.getBondedDevices();

        for (BluetoothDevice device : devices) {
            Method isConnectedMethod = BluetoothDevice.class.getDeclaredMethod("isConnected", (Class[]) null);
            method.setAccessible(true);
            boolean isConnected = (boolean) isConnectedMethod.invoke(device, (Object[]) null);
            if (isConnected) {
                Log.i("BLUETOOTH", "connected:" + device.getName());
                connectedDevices.add(device);
            }
        }
    }
} catch (Exception e) {
    e.printStackTrace();
}

 

問題

startDiscovery、startScan的區別

android - BluetoothAdapter.startScan() vs BluetoothAdapter.startLeScan() - Stack Overflow

 

  • startDiscovery()用來掃描 傳統藍牙設備,
  • startLeScan()用來掃描 低功耗藍牙設備。

對藍牙適配器來說,執行設備發現是一個繁重的過程,將消耗大量資源。

Edit:

On LG Nexus 4 with Android 4.4.2 startDiscovery() finds Bluetooth LE devices.

On Samsung Galaxy S3 with Android 4.3 startDiscovery() doesn't find Bluetooth LE devices.

 

API常見錯誤碼

https://www.cnblogs.com/Free-Thinker/p/11507349.html

android上層api並沒有把所有的連接status都給公開(BluetoothGatt中),所以有時會收到api中沒有的status,下邊是幾個可能會遇到的:

  • GATT_ERROR    0x85    //133任何不懼名字的錯誤都出現這個錯誤碼,出現了就認慫吧,重新連接吧。
  • GATT_CONN_TIMEOUT    0x08    //8  連接超時,大多數情況是設備離開可連接范圍,然后手機端連接超時斷開返回此錯誤碼。
  • GATT_CONN_TERMINATE_PEER_USER     0x13    //19  連接被對端設備終止,直白點就是手機去連接外圍設備,外圍設備任性不讓連接執行了斷開。
  • GATT_CONN_TERMINATE_LOCAL_HOST    0x16    //22  連接被本地主機終止,可以解釋為手機連接外圍設備,但是連接過程中出現一些比如鑒權等問題,無法繼續保持連接,主動執行了斷開操作。
  • GATT_CONN_FAIL_ESTABLISH      03E    //62  連接建立失敗。

封裝好的庫

在做功能時順便也做了個藍牙工具庫的module,之后上傳到gayhub上再來更新地址。

 

其他第三方庫

https://github.com/Jasonchenlijian/FastBle

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM