【Android】藍牙開發入門筆記

開頭語

該文章是我在學習藍牙開發後自己總結出來的一些常見的方法。由於是筆記,因此可能會缺少一些細節說明,大體上僅記錄一下思路。

1. 經典藍牙

服務端

開啓服務端

開啓服務端需要調用BluetoothAdapter.listenUsingInsecureRfcommWithServiceRecord()

方法接收兩個參數,關鍵參數爲uuid。客戶端連接到服務端時,需要知道uuid才能正確的連接上服務端。

/**
 * 創建一個經典藍牙服務端
 * @param name 這個參數暫時不清楚有什麼意義,可隨意傳
 * @param uuid 該參數需要客戶端一致才能連接到服務端
*/
public BluetoothServerSocket listenUsingInsecureRfcommWithServiceRecord(String name, UUID uuid)
        throws IOException {
    return createNewRfcommSocketAndRecord(name, uuid, false, false);
}

該方法返回一個BluetoothServerSocket對象。該對象用於監聽客戶端的連接。

調用BluetoothServerSocket.accept(),可以阻塞當前線程直到超時或獲取到一個客戶端的連接。

注意:該方法可在獲取到連接後再次調用。因此是允許多個客戶端連接到一個服務端的。

/**
 * @param timeout 超時時間,-1表示爲無限時
*/
public BluetoothSocket accept(int timeout) throws IOException {
    return mSocket.accept(timeout);
}

該方法返回一個BluetoothSocket對象。利用該對象,可以和客戶端以IO流的形式進行數據通信。

和客戶端通信

調用BluetoothSocket.getInputStream()BluetoothSocket.getOutputStream()即可獲得輸入流和輸出流。後續的通信操作與常見的IO通信一樣。

關閉

關閉客戶端的連接可以調用BluetoochSocket.close()

關閉服務端可以調用BluetoothServerSocket.close()。關閉服務端之後,已連接的客戶端依然可以繼續通信。只是後續無法再讓新的客戶端連接上來。要中斷與客戶端的連接,請調用上一行提到的方法。

客戶端

掃描服務端

掃描經典藍牙設備,需要註冊廣播BluetoothDevice.ACTION_FOUND。然後再調用BluetoothAdapter.startDiscovery()

注意:6.0之後需要定位權限才能掃描到設備。

此時,發現的設備會通過廣播的形式接收到。

// 廣播接收器
private val receiver = object : BroadcastReceiver() {
    override fun onReceive(context: Context?, intent: Intent?) {
        if (BluetoothDevice.ACTION_FOUND == intent?.action) {
            // 發現設備
            val device =
              intent.getParcelableExtra<BluetoothDevice>(BluetoothDevice.EXTRA_DEVICE)
        }
    }
}

連接服務端

連接服務端需要調用BluetoothDevice.createInsecureRfcommSocketToServiceRecord()。方法接收一個參數uuiduuid需要和服務端開啓時使用的uuid一致。

public BluetoothSocket createInsecureRfcommSocketToServiceRecord(UUID uuid) throws IOException {
    // 檢查藍牙是否可用
    if (!isBluetoothEnabled()) {
        Log.e(TAG, "Bluetooth is not enabled");
        throw new IOException();
    }
    return new BluetoothSocket(BluetoothSocket.TYPE_RFCOMM, -1, false, false, this, -1,new ParcelUuid(uuid));
}

和服務端通信

和上面服務端介紹的一樣。通過inputstreamoutputstream進行IO流通信。

關閉

調用BluetoothSocket.close()可關閉與服務端的連接。

2. 低功耗藍牙BLE

基礎知識

Gatt

BluetoothGatt來表示BLE設備。

關於Gatt需要詳細瞭解的話可以看看這篇文章:《藍牙BLE: GATT Profile 簡介(GATT 與 GAP)》

可以簡單的將其理解爲BLE設備即可。所有BLE設備之間的通信都需要通過BluetoothGatt來實現。

Service

翻譯爲服務,可以簡單理解爲表示該BLE設備支持哪些功能。通常服務和特性(Characteristics)是成對出現的。需要詳細瞭解的話,可以看看這篇文章:《BLE4.0教程二 藍牙協議之服務與特徵值分析》

Characteristics

翻譯爲特性、特徵。該對象需要依託於Service存在。通常,進行數據交互的時候實際操作的對象就是Characteristics。該對象有以下幾個比較重要的參數:

  1. 屬性proprty,描述該特性的功能。常見取值有:PROPERTY_READ(0x02)、PROPERTY_WRITE_NO_RESPONSE(0x04)、PROPERTY_WRITE(0x08)、PROPERTY_NOTIFY(0x10)、PROPERTY_INDICATE(0x20)
  2. 權限permission,和屬性基本是一一對應的。常見取值有:PERMISSION_READ(0x01)、PERMISSION_WRITE(0x10)
  3. 值value,特性的數據內容。格式爲byte數組。

服務端

開啓服務端

開啓服務端需要調用BluetoothManager.openGattServer()

/** 
 * 開啓BLE服務端
 * @param context 上下文
 * @param callback 回調參數,關鍵!
 * @return BluetoothGattServer instance
 */
public BluetoothGattServer openGattServer(Context context,
        BluetoothGattServerCallback callback) {

    return (openGattServer(context, callback, BluetoothDevice.TRANSPORT_AUTO));
}

該方法接收兩個參數,最關鍵參數爲callback。該參數用於接收客戶端發來的請求以及客戶端與服務端的連接狀態。

下面爲幾個比較常用的callback的方法。

BluetoothGattServerCallback() {
    /**
     * 接收到客戶端發來的讀特性請求,該請求需要服務端回覆客戶端,否則客戶端會斷開連接
     * 
     * @param device 客戶端設備
     * @param requestId 標識請求的id
     * @param offset 數據偏移量
     * @param characteristic 客戶端希望讀取的特性
    */
    override fun onCharacteristicReadRequest(
        device: BluetoothDevice?,
        requestId: Int,
        offset: Int,
        characteristic: BluetoothGattCharacteristic?
    ) {}

    /**
     * 接收到客戶端發來的寫特性請求
     *
     * @param device 客戶端設備
     * @param requestId 標識請求的id
     * @param charateristic 客戶端希望寫的特性
     * @param preparedWrite 是否可以延時寫入
     * @param responseNeeded 該請求是否需要服務端回覆
     * @param offset 數據偏移量
     * @param value 要寫入的數據
     */
    override fun onCharacteristicWriteRequest(
        device: BluetoothDevice?,
        requestId: Int,
        characteristic: BluetoothGattCharacteristic?,
        preparedWrite: Boolean,
        responseNeeded: Boolean,
        offset: Int,
        value: ByteArray?
    ) {}

    /**
     * 客戶端設備連接狀態改變
     * @param device 客戶端設備
     * @param status 當前狀態
     * @param newState 只有兩種情況,已連接BluetoothProfile#STATE_CONNECTED;斷開連接			 * BluetoothProfile#STATE_DISCONNECTED
     */
    override fun onConnectionStateChange(device: BluetoothDevice?, status: Int, newState: Int) {}

    /**
     * 新增服務成功
     * @param status BluetoothGatt#GATT_SUCCESS表示成功,否則爲失敗
     * @param service 新增的服務
     */
    override fun onServiceAdded(status: Int, service: BluetoothGattService?) {
        val isSuccess = status == 0
        listener?.onServiceAdded(service, isSuccess)
    }
}

開啓廣播

需要開啓廣播,客戶端才能使用BLE的掃描方式掃描到設備。但是廣播可以設置爲不可連接,這樣客戶端即使掃描到設備,也不能連接到服務端。

調用BluetoothGattServer.getBluetoothLeAdvertiser().startAdvertising()。方法接收最多4個參數。

/**
 *
 * @param settings 廣播設置
 * @param advertiseData 廣播內容
 * @param scanResponse 掃描回覆包,可不傳
 * @param callback 回調函數,用於判斷是否廣播成功
 */
public void startAdvertising(AdvertiseSettings settings,
        AdvertiseData advertiseData, AdvertiseData scanResponse,
        final AdvertiseCallback callback) {}
廣播設置
val advertiseSettings = AdvertiseSettings.Builder()
    .setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_LOW_LATENCY) //廣播模式: 低功耗,平衡,低延遲
    .setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_HIGH) //發射功率級別: 極低,低,中,高
    .setConnectable(true) //能否連接,廣播分爲可連接廣播和不可連接廣播
    .build()
廣播內容
val advertiseData = AdvertiseData.Builder()
    .setIncludeDeviceName(true) //包含藍牙名稱
    .setIncludeTxPowerLevel(true) //包含發射功率級別
    .addManufacturerData(1, byteArrayOf(66, 66)) //設備廠商數據,自定義
    .build()
掃描回覆包
val scanResponse = AdvertiseData.Builder()
    .addManufacturerData(2, byteArrayOf(66, 66)) //設備廠商數據,自定義
    .addServiceUuid(ParcelUuid(你的服務uuid)) //服務UUID
    .addServiceData(new ParcelUuid(你的服務uuid), new byte[]{2}) //服務數據,自定義
    .build()

新增服務

調用BluetoothGattServer.addService()即可新增一個服務。實例化一個服務對象BluetoothGattService()需要一個UUID。服務可以沒有特性,那麼該服務無法提供數據傳輸的功能。

新增服務的結果會在BluetoothGattServerCallback.onServiceAdded()中接收。

新增特性

調用BluetoothGattService.addCharacteristic()即可。實例化一個特性對象BluetoothGattCharacteristic()需要傳入三個參數。

/**
 * 特性構造函數
 * @param uuid UUID標識
 * @param properties 特性屬性
 * @param permissions 特性權限
 */
public BluetoothGattCharacteristic(UUID uuid, int properties, int permissions) {
    initCharacteristic(null, uuid, 0, properties, permissions);
}

特性獨立存在沒有任何作用。需要將特性添加到一個服務中,並將該服務添加到BLE服務端才能生效。注意,如果是向已存在的服務追加特性的話,需要將服務從服務端中移除BluetoothGattServer.removeService(),然後再將追加特性後的服務添加到服務端BluetoothGattServer.addService()

回覆客戶端請求

當收到讀請求,即BluetoothGattServerCallback.onCharacteristicReadRequest()時。需要及時回覆請求,否則客戶端將會斷開連接。具體時間暫時未在源碼找到。

調用BluetoothGattServer.sendResponse()回覆請求。

/**
 * 回覆客戶端請求
 * @param device 客戶端設備
 * @param requestId 請求的標識id,在接收請求的時候拿到
 * @param status 狀態碼
 * @param offset 數據偏移量
 * @param value 回覆數據
 * @return 返回true表示回覆已經發送成功(不代表客戶端一定接收到)
 */
public boolean sendResponse(BluetoothDevice device, int requestId,
        int status, int offset, byte[] value) {
    if (VDBG) Log.d(TAG, "sendResponse() - device: " + device.getAddress());
    if (mService == null || mServerIf == 0) return false;

    try {
        mService.sendResponse(mServerIf, device.getAddress(), requestId,
                status, offset, value);
    } catch (RemoteException e) {
        Log.e(TAG, "", e);
        return false;
    }
    return true;
}

主動通知客戶端

服務端改變特性的內容BluetoothGattCharacteristic.setValue(),然後調用BluetoothGattServer.notifyCharacteristicChanged()

/** 
 * 
 * @param device 客戶端設備
 * @param characteristic 改變的特性
 * @param confirm true的話爲indication,false爲notification
 * @return true, if the notification has been triggered successfully
 * @throws IllegalArgumentException
 */
public boolean notifyCharacteristicChanged(BluetoothDevice device,
        BluetoothGattCharacteristic characteristic, boolean confirm) {}

這個方法需要注意的是第三個參數confirm。作用是客戶端是否需要主動查詢一次信息。爲true的時候,客戶端應當發起一次查詢特性請求,然後才把特性的內容回覆給客戶端。爲false的時候,則客戶端可以直接讀取特性中的內容,無需再發送請求。

但是經過我自己測試發現,似乎無論confirm傳true還是false,客戶端都可直接在在回調中接收到數據。emmm。

關閉服務端

調用BluetoothGattServer.close()即可。

客戶端

掃描服務端

調用BluetoothAdapter.getBluetoothLeScanner().startScan()。該方法接收一個ScanCallback作爲回調函數。

ScanCallback() {
    override fun onScanFailed(errorCode: Int) {
        Log.e(tag, "掃描BLE設備失敗,錯誤碼:$errorCode")
    }

    override fun onScanResult(callbackType: Int, result: ScanResult?) {
        // 拿到正在廣播的設備
        val device = result?.device
        // result.isConnectable用於判斷該廣播是否可以用來連接,API26以上
    }
}

連接服務端

調用BluetoothDevice.connectGatt()

/** 
 *
 * @param callback 回調
 * @param autoConnect 是否要自動連接,一般爲false,不然的話連接速度大大降低
 */
public BluetoothGatt connectGatt(Context context, boolean autoConnect,
        BluetoothGattCallback callback) {}

callback是關鍵參數。服務端發來的所有回覆都在這個回調類中進行接收處理的。

以下爲該類的幾個常用回調函數:

BluetoothGattCallback() {
    /**
     * 讀特性請求的回覆
     * @param gatt 服務端對象
     * @param characteristic 請求讀的特性
     * @param status 請求結果,爲0表示成功
     */
    override fun onCharacteristicRead(
        gatt: BluetoothGatt?,
        characteristic: BluetoothGattCharacteristic?,
        status: Int
    ) {}

    /**
     * 寫特性請求回覆
     * @param gatt 服務端對象
     * @param characteristic 請求寫的特性
     * @param status 請求結果,爲0表示成功
     */
    override fun onCharacteristicWrite(
        gatt: BluetoothGatt?,
        characteristic: BluetoothGattCharacteristic?,
        status: Int
    ) {}

    /**
     * discoverService()這個方法的回調
     * @param gatt 服務端對象
     * @param status 操作結果,爲0表示成功
     */
    override fun onServicesDiscovered(gatt: BluetoothGatt?, status: Int) {}

    /**
     * 與服務端的連接狀態發生變化
     * @param gatt 服務端對象
     * @param status 這個沒啥用
     * @param newSate 當前連接狀態。BluetoothProfile#STATE_DISCONNECTED或者				 * BluetoothProfile#STATE_CONNECTED
     */
    override fun onConnectionStateChange(gatt: BluetoothGatt?, status: Int, newState: Int) {}
    
    /**
     * 特性內容發生改變,由服務端進行通知,直接讀取參數中的特性value即可
     * @param gatt 服務端對象
     * @param characteristic 內容發生改變的特性
     */
    override fun onCharacteristicChanged(
                gatt: BluetoothGatt?,
                characteristic: BluetoothGattCharacteristic?
     ) {}
}

關於autoConnect,如果設置爲true的話,那麼連接速度會大大的降低。但是可以自動連接到設備,這樣可以不用自己去管理當連接突然中斷後又要自己重新掃描去連接。

回調方法中的BluetoothGatt是與服務端交互的關鍵類。

查詢服務

調用BluetoothGatt.discoverServices()

雖然還有個方法BlueGatt.getServices()但是請注意!!!getServices()需要在調用discoverServices()之後,在BluetoothGattCallback.onServicesDiscovered()中確定status爲0,才能正確的獲取最新最全的服務端支持的服務。

另外,如果服務端的服務是在客戶端連接後才新增,那麼客戶端需要重新連接一次之後才能獲取到最新的服務。否則是沒辦法看到的。

發送讀特性請求

調用BluetoothGatt.readCharacteristic()

/**
 * @param characteristic 希望讀取的特性
 * @return true表示請求發送成功
 */
public boolean readCharacteristic(BluetoothGattCharacteristic characteristic) {
     // 需要特性擁有屬性PROPERTY_READ
     if ((characteristic.getProperties() & BluetoothGattCharacteristic.PROPERTY_READ) == 0) return false;
     ....
 }

服務端會在收到請求後進行回覆,如果服務端沒有進行回覆,那麼一段時間(我也不知道是多久)後客戶端就會斷開和服務端的連接。

發送寫特性請求

調用BluetoothGatt.writeCharacteristic()

/** 
 * @param characteristic 希望寫入的特性,該特性的value就是希望寫入的內容
 * @return true表示請求發送成功
 */
public boolean writeCharacteristic(BluetoothGattCharacteristic characteristic) {
     // 要求特性有PROPERTY_WRITE屬性或PROPERTY_WRITE_NO_RESPONSE屬性
    if ((characteristic.getProperties() & BluetoothGattCharacteristic.PROPERTY_WRITE) == 0 && (characteristic.getProperties() & BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE) == 0) {
        return false;
    }
 }

如果特性的屬性是PROPERTY_WRITE,那麼同樣需要服務端在收到請求後進行回覆,否則一段時間後客戶端就會斷開與服務端的連接。

註冊特性通知

調用BluetoothGatt.setCharacteristicNotification(),並在第二個參數傳入true。根據源碼,發現即使特性沒有PROPERTY_NOTIFY屬性,依然可以註冊,並且服務端的通知也能傳過來。so,PROPERTY_NOTIFY有啥用?

關閉客戶端

調用BluetoothGatt.close()

結束語

可以看到,原生的藍牙使用起來還是比較繁瑣的,在日常開發中肯定要進行一些基礎的封裝。我也根據我的使用需求,進行了一下簡單的封裝,可以看看下面這篇文章。
Android】藍牙快速開發工具包-入門級

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