【Android】藍牙快速開發工具包-入門級

開頭語

方便快速開發藍牙,封裝常用的操作。

需要以下三個權限:

  1. android.permission.BLUETOOTH
  2. android.permission.BLUETOOTH_ADMIN
  3. android.permission.ACCESS_FINE_LOCATION 定位權限是由於6.0之後掃描設備需要

經典藍牙

服務端

開啓服務端

首先,實例化一個BluetoothClassicalServer。該對象的構造函數需要兩個必要參數:

  1. serverName,服務端名稱,可隨意。
  2. uuid,用於標識服務端,客戶端需要使用相同的uuid纔可以連接上來。
class BluetoothClassicalServer(
    val serverName: String,
    val uuid: UUID
) : CoroutineScope {}

然後,設置監聽器,用於監聽設備連接狀態改變。

server.listener = object:ClassicalServerListener(){
    override fun onDeviceConnected(client: BluetoothClassicalServer.Client) {}
    override fun onFail(errorMsg: String) {}
}
/*服務端監聽器,監聽設備連接以及服務端狀態*/
open class ClassicalServerListener {
    /**
     * 客戶端設備已連接,可能會多次被調用
     * @param client 客戶端
     */
    open fun onDeviceConnected(client: BluetoothClassicalServer.Client) {}

    /**
     * 服務端發生異常
     * @param errorMsg 錯誤信息
     */
    open fun onFail(errorMsg: String) {}
}

最後,調用start()方法開始監聽客戶端連接。

start()方法使用了一個在IO線程的協程去等待客戶端的連接。如果有設備連接上來,那麼會切換到主線程回調ClassicalServerListener

/**
 * 開啓服務端
 * @param timeout 超時時間,-1則表示無限
 */
fun start(timeout: Int = -1) {
    if (isRunning) return

    serverSocket = adapter.listenUsingInsecureRfcommWithServiceRecord(serverName, uuid)

    // 開啓一個IO線程的協程,去監聽客戶端設備連接
    launch(Dispatchers.IO) {
        try {
            isRunning = true
            // serverSocket.accept()是阻塞方法,會一直阻塞直到有客戶端連接上來或超時
            val clientSocket = serverSocket!!.accept(timeout)
            addClient(clientSocket)
            start()// 繼續監聽連接
            Log.d("d","監聽服務端")
        } catch (e: Exception) {
            // 這裏的異常可能是超時、也可能是手動關閉服務端導致的
            if (isRunning) {
                // 超時等系統異常
                notifyListenerError("發生異常:${e.message}")
            }
            isRunning = false
            serverSocket?.close()
        }
    }
}
監聽客戶端消息

當客戶端連接成功後,爲該客戶端設置一個消息監聽器,監聽來自客戶端的消息。

client.messageListener = object : BluetoothMessageListener() {
    override fun onFail(errorMsg: String) {}
    override fun onReceiveMsg(msg: ByteArray) {}
}
/**
 * Create by AndroidStudio
 * Author: pd
 * Time: 2020/4/5 09:18
 * 消息監聽器,監聽來自客戶端的消息以及客戶端狀態
 */
open class BluetoothMessageListener {
    /**
     * 接收到客戶端傳來的消息
     * @param msg 
     */
    open fun onReceiveMsg(msg:ByteArray){}

    /**
     * 客戶端發生異常
     * @param errorMsg 錯誤信息
     */
    open fun onFail(errorMsg:String){}
}
向客戶端發送消息

調用client.send()。該方法接收一個byte數組。

/**
 * 向該客戶端發送消息
 * @param msg 要發送的消息
 */
fun send(msg: ByteArray) {
    if (!socket.isConnected) {
        notifyListenerError("連接已斷開")
    } else {
        launch(Dispatchers.IO) {// 切換到IO線程
            try {
                ous.write(msg)
            } catch (e: Exception) {
                // 可能在發送信息的時候,與客戶端斷開連接
                notifyListenerError("發生異常:${e.message}")
                disConnect()
            }
        }
    }
}
關閉服務端

調用server.stop()。該方法只是關閉服務端監聽,即不再允許有新的客戶端連接到服務端。但是,已經連接的客戶端依然可以進行通信。

/**
 * 關閉服務端,但是已經連接的客戶端依然允許通信
 */
fun stop() {
    isRunning = false
    serverSocket?.close()
}
斷開與客戶端的連接

調用client.disConnect()

/**
 * 關閉與客戶端的連接
 */
fun disConnect() {
    socket.close()
    messageListener = null
    // 從服務端設備列表中移除
    server.removeClient(this)
}

客戶端

掃描服務端

調用BtManager.scanDevice。該方法接收3個參數。

經典藍牙的掃描需要在Application.onCreate()中調用BtManager.init()。因爲涉及到廣播註冊,因此使用Application以防止忘記解註冊導致內存泄漏。

/**
 * 掃描設備
 * @param listener 掃描監聽
 * @param timeout 掃描超時時間,最低默認爲10秒
 * @param type 掃描類型
 * @see BluetoothType
 */
fun scanDevice(
    listener: ScanListener? = null,
    timeout: Int = 10,
    type: BluetoothType = BluetoothType.CLASSICAL
) {}
連接服務端

首先,實例化BluetoothClassicalClient()。接收兩個參數。

class BluetoothClassicalClient(
    // 服務端設備
    val serverDevice: BluetoothDevice,
    
    // 服務端uuid,需要和服務端開啓時使用的uuid一致
    private val uuid: UUID
) : CoroutineScope {}

然後,設置客戶端監聽器,監聽和服務端的連接狀態。

client!!.clientListener = object : ClassicalClientListener() {
    // 與服務端連接成功
    override fun onConnected() {}

    // 客戶端出現異常
    override fun onFail(errorMsg: String) {}
}

最後,調用client.connectServer()。正式發起與服務端的連接。

/**
 * 開始連接服務端
 */
fun connectServer() {
    launch(Dispatchers.IO) {
        val socket = serverDevice.createInsecureRfcommSocketToServiceRecord(uuid)
        try {
            socket.connect()// 阻塞直到連接成功或者出現異常
            notifyListenerConnected()
            serverSocket = socket
            // 連接成功後開啓消息輪詢
            startListenMsg()
        } catch (e: Exception) {
            // 可能在連接的時候,服務端關閉了
            notifyListenerError("發生異常:${e.message}")
            socket.close()
        }
    }
}
監聽服務端消息

當與服務端連接成功後,設置消息監聽器。

// 連接成功後設置消息監聽器
client!!.messageListener = object : BluetoothMessageListener(){
    // 與服務端連接出現異常
    override fun onFail(errorMsg: String) {}

    // 接收到服務端的消息
    override fun onReceiveMsg(msg: ByteArray) {}
}
向服務端發送消息

調用client.send()。接收一個byte數組。

/**
 * 向服務端發送消息
 * @param data
 */
fun send(data: ByteArray) {
    launch(Dispatchers.IO) {// 切換到IO線程
        if (serverSocket != null) {
            if (serverSocket!!.isConnected) {
                try {
                    serverSocket!!.outputStream.write(data)
                } catch (e: Exception) {
                    notifyMessageFail("發生異常:${e.message}")
                    serverSocket!!.close()
                }
            }
        }
    }
}
斷開與服務端的連接

調用client.disConnect()

/**
 * 關閉與服務端的連接
 */
fun disConnect() {
    serverSocket?.close()
    messageListener = null
}

低功耗藍牙BLE

服務端

開啓服務端

首先,實例化BluetoothLeServer

class BluetoothLeServer(
    private val context: Context
) : CoroutineScope {}

然後,設置監聽器,該監聽器可監聽客戶端連接狀態、客戶端請求等。

server.listener = object : BleServerListener() {
    /**
     * 客戶端設備連接狀態改變
     * @param device 客戶端設備
     * @param status 連接狀態
     * @param isConnected true已連接,false未連接
    */
    override fun onDeviceConnectionChange(
        device: BluetoothDevice?,
        status: Int,
        isConnected: Boolean
    ) {
    }

    /**
     * 讀特性請求
     * @param request 請求體
    */
    override fun onCharacteristicReadRequest(request: CharacteristicRequest) {
    }

    /**
     * 寫特性請求
     * @param request 請求體
     */
    override fun onCharacteristicWriteRequest(request: CharacteristicWriteRequest) {
    }

    /**
     * 新增服務回調
     * @param service 新增的服務
     * @param isSuccess 是否新增成功
    */
    override fun onServiceAdded(service: BluetoothGattService?, isSuccess: Boolean) {
}
}

最後,真正的開啓服務端。server.start()

/**
 * 開啓BLE服務端
 * @return true表示服務端成功開啓/false表示開啓失敗
 */
fun start(): Boolean {
    gattServer = bluetoothManager.openGattServer(context, gattCallback)
    gattServer?.clearServices()
    return gattServer != null
}
開啓廣播

調用server.startAdv()。接收3個必要參數以及1個可空參數。

/**
 * 開啓廣播才能被BLE掃描模式搜索到該設備
 * @param advertiseSettings 廣播設置
 * @param advertiseData 廣播內容
 * @param scanResponse 廣播被掃描後回覆的內容
 * @param callback 回調
 */
fun startAdv(
    advertiseSettings: AdvertiseSettings,
    advertiseData: AdvertiseData,
    scanResponse: AdvertiseData? = null,
    callback: AdvertiseCallback
) {
    bluetoothAdapter.bluetoothLeAdvertiser.startAdvertising(
        advertiseSettings,
        advertiseData,
        callback
    )
    isAdving = true
}
新增服務

調用server.addService()

/**
 * 下一次addService必須要等上一次addService回調了onServiceAdded()之後才能再調用
 * @param uuid 服務uuid
 */
fun addService(uuid: UUID) {
    val service = BluetoothGattService(uuid,BluetoothGattService.SERVICE_TYPE_PRIMARY)
    addService(service)
}
新增特性

首先,調用server.buildCharacteristic()構造一個特性。該方法接收一個必要參數以及4個可空參數。

/**
 * 創建特性
 * @param characteristicUUID 特性UUID
 * @param readable 特性是否可讀,默認否
 * @param writable 特性是否可寫,默認否
 * @param writeNoResponse 特性是否支持不用回覆的寫入,默認否
 * @param notify 特性是否可通知,默認爲否
 */
fun buildCharacteristic(
    characteristicUUID: UUID,
    readable: Boolean = false,
    writable: Boolean = false,
    writeNoResponse: Boolean = false,
    notify: Boolean = false
): BluetoothGattCharacteristic {

    var permission = 0x00
    if (readable) permission = permission or BluetoothGattCharacteristic.PERMISSION_READ
    if (writable) permission = permission or BluetoothGattCharacteristic.PERMISSION_WRITE

    var property = 0x00
    if (readable) property = property or BluetoothGattCharacteristic.PROPERTY_READ
    if (writable) property = property or BluetoothGattCharacteristic.PROPERTY_WRITE
    if (writeNoResponse) property =
        property or BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE
    if (notify) property = property or BluetoothGattCharacteristic.PROPERTY_NOTIFY

    return BluetoothGattCharacteristic(characteristicUUID, property, permission)
}

然後,調用server.addCharacteristicToService(),方法接收兩個必要參數。

/**
 * 向已存在的服務添加特性
 * @param serviceUUID 服務的UUID
 * @param characteristic 要添加的特性
 * @return true表示添加成功,false表示失敗
 */
fun addCharacteristicToService(
    serviceUUID: UUID,
    characteristic: BluetoothGattCharacteristic
): Boolean {
    val service = gattServer?.getService(serviceUUID) ?: return false
    gattServer?.removeService(service)
    service.addCharacteristic(characteristic)
    gattServer?.addService(service)
    return true
}
回覆客戶端請求

首先,實例化一個回覆類BleServerResponse

class BleServerResponse(
    val request: CharacteristicRequest,// 要回復的請求體
    val status: Int,// 狀態碼,0表示該請求成功
    val data: ByteArray,// 回覆的內容
    val offset: Int = 0// 內容偏移量
)

然後,調用server.sendResponse()。方法接收一個必要參數。

/**
 * 回覆請求
 * @param response 回覆對象
 * @return true表示回覆成功,false表示回覆失敗
 */
fun sendResponse(response: BleServerResponse): Boolean {
    val result = gattServer?.sendResponse(
        response.request.device,
        response.request.requestId, response.status, response.offset, response.data
    ) ?: false
    // 如果回覆成功的話,那麼清除這次請求
    if (result) lastRequest = null
    return result
}
主動通知客戶端

調用server.notifyCharacteristic(),方法接收2個必要參數和1個可空參數。

/**
 * 服務端主動通知客戶端特性內容有變化
 * @param characteristic 內容變化的特性
 * @param device 客戶端設備
 * @param confirm 默認爲false
 */
fun notifyCharacteristic(
    characteristic: BluetoothGattCharacteristic,
    device: BluetoothDevice,
    confirm: Boolean = false
): Boolean {
    return gattServer?.notifyCharacteristicChanged(device, characteristic, confirm) ?: false
}
關閉廣播

調用server.stopAdv()。方法接收1個必要參數。

/**
 * 關閉廣播,可能導致設備斷開連接
 * @param callback 開啓廣播的時候設置的回調
 */
fun stopAdv(callback: AdvertiseCallback) {
    bluetoothAdapter.bluetoothLeAdvertiser.stopAdvertising(callback)
    isAdving = false
}
關閉服務端

調用server.stop()

/**
 * 關閉BLE服務端
 */
fun stop() {
    gattServer?.close()
    gattServer = null
}

客戶端

掃描服務端

調用BtManager.scanDevice()。如果只有BLE掃描的話,可以不用在Application.onCreate()中調用BtManager.init()

/**
 * 掃描設備
 * @param listener 掃描監聽
 * @param timeout 掃描超時時間,最低默認爲10秒
 * @param type 掃描類型,不傳則默認爲經典藍牙
 * @see BluetoothType
 */
fun scanDevice(
    listener: ScanListener? = null,
    timeout: Int = 10,
    type: BluetoothType = BluetoothType.CLASSICAL
) {}
連接服務端

首先,實例化BluetoothLeClient

class BluetoothLeClient(
    val serverDevice: BluetoothDevice,// 服務端設備
    private val context: Context// 上下文
) : CoroutineScope {}

然後,調用client.connectServer()發起連接。

/**
 * 連接服務端
 * @param gattCallback 回調監聽
 * @return false表示連接失敗,可能當前設備不支持BLE,不是服務端不支持
 */
fun connectServer(gattCallback: BluetoothGattCallback? = null): Boolean {
    if (gattCallback != null) callback = gattCallback
    server = serverDevice.connectGatt(context, false, callbackPoxy)
    server?.discoverServices()
    if (server != null) isConnected = true
    return server != null
}
查詢服務

調用server.checkService()。查詢結果在開啓服務端時設置的回調監聽onServicesDiscovered()中接收。

/**
 * 查詢服務端支持的服務列表,結果在回調onServicesDiscovered
 */
fun checkService() {
    if (server == null) {
        callback.onServicesDiscovered(server, -1)
    } else {
        server?.discoverServices()
    }
}

callback.onServicesDiscovered()中的status = 0時。調用gatt.getService()即可獲取到服務端支持的服務列表。

val callback = object : BluetoothGattCallback() {
    override fun onServicesDiscovered(gatt: BluetoothGatt?, status: Int) {
        // 這裏是非主線程啊!!!
        if (status == 0) {
            val list = gatt!!.services
        }
    }
}
發送讀特性請求

調用client.readCharacteristic()

/**
 * 發送讀特性請求
 * @param characteristic 要讀取的特性
 * @return true表示發送請求成功,false表示發送請求失敗
 */
fun readCharacteristic(characteristic: BluetoothGattCharacteristic): Boolean {
    return server?.readCharacteristic(characteristic) ?: false
}
發送寫特性請求

調用client.writeCharactetistic()

/**
 * 發送寫特性請求
 * @param characteristic 要寫入的特性
 * @param data 要寫入的內容
 * @return true表示發送請求成功,false表示發送請求失敗
 */
fun writeCharacteristic(
    characteristic: BluetoothGattCharacteristic,
    data:ByteArray
): Boolean {
    characteristic.value = data
    return server?.writeCharacteristic(characteristic) ?: false
}
註冊特性通知

當註冊了特性通知之後,服務端才能通過通知主動向客戶端發送消息。否則,即使服務端發送了通知,但是由於客戶端沒有註冊,依然無法收到。

調用client.regCharacteristicNotify()

/**
 * 註冊特性通知
 * @param characteristic 希望接收通知的特性
 * @return true表示註冊成功,false表示註冊失敗
 */
fun regCharacteristicNotify(characteristic: BluetoothGattCharacteristic): Boolean {
    return server?.setCharacteristicNotification(characteristic, true) ?: false
}
斷開與服務端的連接

調用client.disconnectServer()

/**
 * 斷開和服務端的連接
 */
fun disconnectServer() {
    server?.disconnect()
    isConnected = false
}

公共工具

一些經典藍牙和Ble都需要用到的方法,統一放在公共工具類BtManager

經典藍牙掃描初始化

由於經典藍牙的掃描結果是通過廣播的形式傳遞過來的。因此需要註冊一下廣播。

/**
 * 因爲需要註冊廣播,所以在application中初始化一下
 * 如果不需要經典藍牙的話不調用也不影響
 */
fun init(application: Application) {
    this.application = application
    val filter = IntentFilter()
    filter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED)
    filter.addAction(BluetoothDevice.ACTION_FOUND)
    this.application!!.registerReceiver(receiver, filter)
}

掃描設備

掃描經典藍牙設備時,一定要先確定是否調用了BtManager.init()

如果無法掃描到任何設備,請確認App擁有定位權限。

/**
 * 掃描設備
 * @param listener 掃描監聽
 * @param timeout 掃描超時時間,最低默認爲10秒,最高爲60秒
 * @param type 掃描類型
 * @see BluetoothType
 */
fun scanDevice(
    listener: ScanListener? = null,
    timeout: Int = 10,
    type: BluetoothType = BluetoothType.CLASSICAL
) {
    stopJob?.cancel()
    // 超時時間
    val realTimeout = when {
        timeout < 10 -> 10 * 1000
        timeout > 60 -> 60 * 1000
        else -> timeout * 1000
    }
    
    // 超時後將結束掃描
    stopJob = launch(Dispatchers.Default) {
        delay(realTimeout.toLong())
        stopScan()
        stopJob = null
    }
    stopScan()
    deviceList.clear()
    isScanning = true
    this.scanListener = listener
    when (type) {
        BluetoothType.CLASSICAL -> scanClassicalDevice()
        BluetoothType.LOW_ENG -> scanBleDevice()
    }
}

結束掃描設備

/**
 * 結束設備掃描
 */
fun stopScan() {
    blAdapter.cancelDiscovery()
    blAdapter.bluetoothLeScanner.stopScan(bleCallBack)
    isScanning = false
    scanListener = null
    stopJob?.cancel()
    stopJob = null
}

查看當前手機已配對的設備

/**
 * 查找當前已綁定的設備,經典藍牙
 */
fun getBondedDevice(): ArrayList<BluetoothDevice> {
    val list = ArrayList<BluetoothDevice>()
    list.addAll(blAdapter.bondedDevices)
    return list
}

判斷當前設備藍牙是否已打開

/**
 * 藍牙是否打開
 */
fun isOn(): Boolean {
    return blAdapter.isEnabled
}

獲取當前設備藍牙名稱

/**
 * 獲取藍牙名稱
 */
fun getName(): String {
    return blAdapter.name
}

判斷當前設備是否支持藍牙

/**
 * 當前設備是否支持藍牙
 */
fun isSupport(): Boolean {
    return blAdapter != null
}

源碼連接

所有源碼已打包上傳至github。https://github.com/d745282469/BlueToothTool

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