開頭語
方便快速開發藍牙,封裝常用的操作。
需要以下三個權限:
android.permission.BLUETOOTH
android.permission.BLUETOOTH_ADMIN
android.permission.ACCESS_FINE_LOCATION
定位權限是由於6.0之後掃描設備需要
經典藍牙
服務端
開啓服務端
首先,實例化一個BluetoothClassicalServer
。該對象的構造函數需要兩個必要參數:
- serverName,服務端名稱,可隨意。
- 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