目前蓝牙连接可分为两种模式,即:传统蓝牙模式和低功耗模式。区别在于,传统蓝牙模式对资源消耗比较大,适用于大流量数据传输要求场景;低功耗模式则比较节能,满足对于能耗方面要求比较高的设备。
无论哪种模式,都要经过如下几个步骤来实现设备间的无线蓝牙传输:
-
蓝牙相关权限申请
-
设备蓝牙功能是否可用
-
发现设备
-
连接设备
-
数据传输
-
销毁连接
1. BLE低功耗蓝牙模式–GATT服务
顾名思义,低功耗蓝牙旨在降低蓝牙功耗,为功率要求比较严格的BLE设备提供蓝牙连接支持,适用于设备间少量数据传输。如:近程传感器、心率检测仪、健康设备等。
低功耗蓝牙连接基本步骤和传统蓝牙一样,只不过某些步骤方式稍显不同;
1.1 蓝牙权限
参照 2.1 蓝牙权限
1.2 蓝牙功能可用性
参照 2.2 蓝牙功能可用性
1.3 发现设备
我们使用BluetoothLeScanner类提供的startScan方法执行蓝牙扫描动作,通过ScanFilter设置特定过滤条件获取指定的蓝牙设备。
扫描非常耗电,因此应遵循以下准则:
-
找到所需设备后,立即停止扫描。
-
绝对不进行循环扫描,并设置扫描时间限制。之前可用的设备可能已超出范围,继续扫描会耗尽电池电量。
@SuppressLint("MissingPermission")
private fun scanLeDevice(enable: Boolean) {
if(mScanning) {
Toast.makeText(applicationContext, "当前正在扫描,请稍后重试", Toast.LENGTH_SHORT).show()
return
}
when (enable) {
true -> {
Log.i(tag, "[scanLeDevice]: 开始蓝牙扫描")
// Stops scanning after a pre-defined scan period.
handler.postDelayed({
Log.i(tag, "[scanLeDevice]: 一定时间后自动结束蓝牙扫描")
mScanning = false
bluetoothLeScanner?.stopScan(leScanCallback)
}, SCAN_PERIOD)
mScanning = true
bluetoothLeScanner?.startScan(listOf(getScanFilter()), getScanSettings(), leScanCallback)
}
else -> {
Log.i(tag, "[scanLeDevice]: 手动结束蓝牙扫描")
mScanning = false
bluetoothLeScanner?.stopScan(leScanCallback)
}
}
}
//扫描结果回调
private val leScanCallback = object : ScanCallback() {
override fun onScanFailed(errorCode: Int) {
super.onScanFailed(errorCode)
Log.e(tag, "[onScanFailed]: 蓝牙设备扫描失败, errorCode: $errorCode")
}
@SuppressLint("MissingPermission")
override fun onScanResult(callbackType: Int, result: ScanResult?) {
super.onScanResult(callbackType, result)
val device = result?.device
if (device != null && device.name?.isNotBlank() == true) {
//将符合条件的设备添加到集合列表里
val item = scanStateList.find { it.device.address == device.address }
if (item != null) {
item.rssi = result.rssi
} else {
scanStateList.add(ScanStateResult(device, STATE_NONE, result.rssi))
}
} else {
Log.d(tag, "[onScanResult]:ignore scan result: $callbackType, $result")
}
Log.i(tag, "[onScanResult]: callbackType: $callbackType, device name: ${result?.device?.name}, address: ${result?.device?.address}, rssi: ${result?.rssi}")
}
@SuppressLint("MissingPermission")
override fun onBatchScanResults(results: MutableList<ScanResult>?) {
super.onBatchScanResults(results)
results?.forEach {
Log.i(tag, "[onBatchScanResults]: device name: ${it.device?.name}, address: ${it.device?.address}")
}
}
}
//配置扫描筛选条件
private fun getScanFilter(): ScanFilter{
return ScanFilter.Builder().
setServiceUuid(ParcelUuid.fromString(SERVICE_UUID)).
build()
}
/**
* 扫描配置
* 如:模式配置等
*/
private fun getScanSettings(): ScanSettings {
return ScanSettings.Builder().
setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY).
build()
}
1.4 连接设备
与 BLE 设备交互的第一步便是连接到 GATT 服务器。更具体地说,是连接到设备上的 GATT 服务器。使用BluetoothDevice类的connectGatt方法连接设备GATT服务器。
bluetoothGatt = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
it?.device?.connectGatt(this, false, getGattCallback(), BluetoothDevice.TRANSPORT_LE)
} else {
it?.device?.connectGatt(this, false, getGattCallback())
}
engine = TransportEngine(bluetoothGatt)
连接过程中,在相应的客户端GATT结果回调中,需要根据设备的配置文件信息,执行响应的连接认证数据传输,才能确保连接成功,过程大体如下:
连接设备 → 发现并连接服务 → 执行连接认证 → 连接成功
/**
* Gatt服务连接结果回调
*/
fun BLEActivity.getGattCallback(): BluetoothGattCallback {
return object : BluetoothGattCallback() {
/**
* 连接状态回调
*/
@SuppressLint("MissingPermission")
override fun onConnectionStateChange(
gatt: BluetoothGatt,
status: Int,
newState: Int
) {
val intentAction: String
when (newState) {
BluetoothProfile.STATE_CONNECTED -> {
intentAction = ACTION_GATT_CONNECTED
connectionState = STATE_CONNECTED
//发送连接状态广播
broadcastUpdate(intentAction)
//发现服务
Log.i(TAG, "Connected to GATT server.")
Log.i(TAG, "Attempting to start service discovery: " +
gatt.discoverServices())
}
BluetoothProfile.STATE_DISCONNECTED -> {
intentAction = ACTION_GATT_DISCONNECTED
connectionState = STATE_DISCONNECTED
Log.i(TAG, "Disconnected from GATT server.")
//发送连接状态广播
broadcastUpdate(intentAction)
}
}
}
// New services discovered
@SuppressLint("MissingPermission")
override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
when (status) {
BluetoothGatt.GATT_SUCCESS -> {
broadcastUpdate(ACTION_GATT_SERVICES_DISCOVERED)
val gattService = gatt.getService(UUID.fromString(SERVICE_UUID))
Log.i(TAG, "Discovered service: " + (gattService != null))
if (gattService != null) {
for (characteristic in gattService.characteristics) {
Log.i(TAG, "Characteristic uuid: " + characteristic.uuid)
if (characteristic.uuid == WRITE_UUID) {
//获取数据写入特征,用于后续数据发送
engine?.setWriteCharacteristic(characteristic)
} else if (characteristic.uuid == NOTIFY_UUID) {
//设置设备可接受数据
val success =
gatt.setCharacteristicNotification(characteristic, true)
if (success) {
//向远程设备写入特征描述
for (dp in characteristic.descriptors) {
if (dp != null) {
if (characteristic.properties and BluetoothGattCharacteristic.PROPERTY_NOTIFY != 0) {
dp.value =
BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
} else if (characteristic.properties and BluetoothGattCharacteristic.PROPERTY_INDICATE != 0) {
dp.value =
BluetoothGattDescriptor.ENABLE_INDICATION_VALUE
}
gatt.writeDescriptor(dp)
}
}
}
}
}
connectionState = STATE_CONNECTED
} else {
Log.e(TAG, "device not support service")
clearGatt()
connectionState = STATE_DISCONNECTED
}
}
else -> {
Log.e(TAG, "onServicesDiscovered received: $status")
clearGatt()
connectionState = STATE_DISCONNECTED
}
}
}
// Result of a characteristic read operation
override fun onCharacteristicRead(
gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic,
status: Int
) {
when (status) {
BluetoothGatt.GATT_SUCCESS -> {
Log.i(TAG, "[onCharacteristicRead]")
// broadcastUpdate(ACTION_DATA_AVAILABLE, characteristic)
}
}
}
private fun broadcastUpdate(action: String) {
val intent = Intent(action)
sendBroadcast(intent)
}
private fun broadcastUpdate(action: String, characteristic: BluetoothGattCharacteristic) {
val intent = Intent(action)
// This is special handling for the Heart Rate Measurement profile. Data
// parsing is carried out as per profile specifications.
when (characteristic.uuid) {
UUID.fromString(SERVICE_UUID) -> {
val flag = characteristic.properties
val format = when (flag and 0x01) {
0x01 -> {
Log.i(TAG, "Heart rate format UINT16.")
BluetoothGattCharacteristic.FORMAT_UINT16
}
else -> {
Log.i(TAG, "Heart rate format UINT8.")
BluetoothGattCharacteristic.FORMAT_UINT8
}
}
val heartRate = characteristic.getIntValue(format, 1)
Log.i(TAG, String.format("Received heart rate: %d", heartRate))
intent.putExtra(EXTRA_DATA, (heartRate).toString())
}
else -> {
// For all other profiles, writes the data formatted in HEX.
val data: ByteArray? = characteristic.value
if (data?.isNotEmpty() == true) {
val hexString: String = data.joinToString(separator = " ") {
String.format("%02X", it)
}
intent.putExtra(EXTRA_DATA, "$data\n$hexString")
}
}
}
sendBroadcast(intent)
}
override fun onCharacteristicWrite(
gatt: BluetoothGatt?,
characteristic: BluetoothGattCharacteristic,
status: Int
) {
super.onCharacteristicWrite(gatt, characteristic, status)
Log.i(TAG, "onCharacteristicWrite: " + characteristic.uuid.toString() + ", " + status)
}
/**
* 接收数据
*/
override fun onCharacteristicChanged(
gatt: BluetoothGatt?,
characteristic: BluetoothGattCharacteristic
) {
Log.i(TAG, "onCharacteristicChanged: $characteristic")
val value = characteristic.value
engine?.onReceiveData(value)
}
/**
* 向远程设备写入特征描述结果回调
*/
override fun onDescriptorWrite(
gatt: BluetoothGatt?,
descriptor: BluetoothGattDescriptor?,
status: Int
) {
Log.i(TAG, "onDescriptorWrite: $status")
val address: Array<String>? = currentDevice?.address?.split(":")?.toTypedArray()
val code = address?.size?.let { ByteArray(it) }
var i = address?.size?.minus(1)
var t = 0
if (i != null) {
while (i >= 0) {
val v = ByteUtil.stringToHex(address!![i])
code?.set(t++, (v xor 0x0A5.toByte()))
i--
}
}
//执行认证
engine!!.verify(code)
}
}
}
1.5 数据传输与接收
1.5.1 接收数据
BLE设备会在特征发生改变时收到通知;
首先需要设置接收通知:
lateinit var bluetoothGatt: BluetoothGatt
lateinit var characteristic: BluetoothGattCharacteristic
var enabled: Boolean = true
...
bluetoothGatt.setCharacteristicNotification(characteristic, enabled)
val uuid: UUID = UUID.fromString(SampleGattAttributes.CLIENT_CHARACTERISTIC_CONFIG)
val descriptor = characteristic.getDescriptor(uuid).apply {
value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
}
bluetoothGatt.writeDescriptor(descriptor)
然后就可以接收数据通知:
/**
* 接收数据
*/
override fun onCharacteristicChanged(
gatt: BluetoothGatt?,
characteristic: BluetoothGattCharacteristic
) {
Log.i(TAG, "onCharacteristicChanged: $characteristic")
val value = characteristic.value
engine?.onReceiveData(value)
}
1.5.2 写入数据
private final BluetoothGatt gatt;
private BluetoothGattCharacteristic writeCharacteristic;
/**
* 发送数据
* @param data
* @return
*/
@SuppressLint("MissingPermission")
public boolean write(byte[] data) {
writeCharacteristic.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT);
writeCharacteristic.setValue(data);
return gatt.writeCharacteristic(writeCharacteristic);
}
1.6 销毁连接
完成使用后,释放资源;
/**
*清除Gatt
*/
private void clearGatt() {
Log.d(TAG, "clearGatt " + gatt);
if (gatt != null) {
gatt.disconnect();
gatt.close();
gatt = null;
}
}
2. 传统蓝牙模式
2.1 蓝牙权限
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
BLUETOOTH、BLUETOOTH_ADMIN:允许用户发现和连接设备
ACCESS_FINE_LOCATION、ACCESS_COARSE_LOCATION:获取位置信息,对于适配Android9(API28)或者更低版本的应用,声明ACCESS_COARSE_LOCATION权限,否则声明ACCESS_FINE_LOCATION权限。
其中ACCESS_FINE_LOCATION权限属于敏感权限,需要代码中手动申请:
//所需权限
private val permissions = arrayOf(Manifest.permission.ACCESS_FINE_LOCATION)
/**
*申请权限
*/
private fun permission(permissions: Array<String>) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
requestPermissions(permissions, PERMISSION_REQUEST_CODE)
}
}
2.2 蓝牙功能可用性
2.2.1 设置蓝牙
private var bluetoothAdapter: BluetoothAdapter? = null
//初始化蓝牙
bluetoothAdapter = (getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager).adapter
if (bluetoothAdapter == null) {
// Device doesn't support Bluetooth
Toast.makeText(applicationContext, "设备不支持蓝牙功能", Toast.LENGTH_SHORT).show()
}
2.2.2 检测蓝牙功能是否开启
//检查蓝牙是否开启,未开启需申请打开系统的蓝牙开启Activity
//新版registerForActivityResult方法使用如下,老版已废弃
if (bluetoothAdapter?.isEnabled == false) {
val launcher = registerForActivityResult(object : ActivityResultContract<Bundle, Int>(){
override fun createIntent(context: Context, input: Bundle?): Intent {
//弹窗提示框,开启蓝牙
val enableBtIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
if (input != null) enableBtIntent.putExtras(input)
return enableBtIntent
}
override fun parseResult(resultCode: Int, intent: Intent?): Int {
return resultCode
}
}
) {
Log.i(tag, "resultCode: $it")
}
launcher.launch(null)
}
2.3 发现设备
使用BluetoothAdapter的startDiscovery方法执行发现过程,该方法是一个异步方法,返回布尔值表示发现进程是否开启。
发现进程是一个非常耗资源的操作,本轮扫描结束后应该及时销毁
这里需要注意一个重要问题:在取消发现进程前,必须先解除广播接收器,才能有效取消该进程;否则会影响重新启动发现进程
//注册扫描接收器
registerBleReceiver()
//开始扫描蓝牙
bluetoothAdapter?.startDiscovery()
...
/**
* 蓝牙扫描结果接收器
*/
private val receiver = object : BroadcastReceiver() {
@SuppressLint("MissingPermission")
override fun onReceive(context: Context?, intent: Intent?) {
when(intent?.action) {
BluetoothDevice.ACTION_FOUND -> {
intent.getParcelableExtra<BluetoothDevice>(BluetoothDevice.EXTRA_DEVICE).let {
val name = it?.name
val address = it?.address
Log.i(tag, "[broadcast onReceive]: device name: $name")
Log.i(tag, "[broadcast onReceive]: device address: $address")
}
}
BluetoothAdapter.ACTION_DISCOVERY_STARTED -> Log.i(tag, "[broadcast onReceive]: 开始扫描蓝牙")
BluetoothAdapter.ACTION_DISCOVERY_FINISHED-> {
//扫描结束及时取消发现进程
val result = cancelBleDiscovery()
Log.i(tag, "[broadcast onReceive]: 蓝牙扫描结束: $result")
}
}
}
}
/**
* 取消蓝牙扫描进程
* 必须先解除广播接收器,才能有效取消该进程,
* 以便于重新扫描
*/
@SuppressLint("MissingPermission")
private fun cancelBleDiscovery(): Boolean? {
//解除广播接收器
unregisterReceiver(receiver)
//关闭蓝牙扫描进程
return bluetoothAdapter?.cancelDiscovery()
}
2.4 连接设备、传输数据
官方提供了一些通用的蓝牙配置文件,适用于设备间蓝牙无线连接通信接口规范,比如:免提、耳机、A2DP、健康设备等蓝牙无线连接,但是这里要注意,健康设备协议HDP、MCAP在新版api里面已经不被支持,建议使用GATT等低功耗模式实现蓝牙无线连接。
2.4.1 作为蓝牙服务器运行, 用于监听连接请求,响应连接
/**
* 作为蓝牙服务器端运行
* 用于监听连接请求,响应连接
*/
@SuppressLint("MissingPermission")
private inner class AcceptThread : Thread() {
private val TAG = "AcceptThread"
private val mmServerSocket: BluetoothServerSocket? by lazy(LazyThreadSafetyMode.NONE) {
bluetoothAdapter?.listenUsingInsecureRfcommWithServiceRecord(NAME, MY_UUID)
}
override fun run() {
// Keep listening until exception occurs or a socket is returned.
var shouldLoop = true
while (shouldLoop) {
val socket: BluetoothSocket? = try {
mmServerSocket?.accept()
} catch (e: IOException) {
Log.e(TAG, "Socket's accept() method failed", e)
shouldLoop = false
null
}
socket?.also {
//新线程从远端设备读取数据和写入数据
MyBluetoothService(Handler(Looper.getMainLooper()), it)
mmServerSocket?.close()
shouldLoop = false
}
}
}
// Closes the connect socket and causes the thread to finish.
fun cancel() {
try {
mmServerSocket?.close()
} catch (e: IOException) {
Log.e(TAG, "Could not close the connect socket", e)
}
}
}
2.4.2 客户端发起连接请求
/**
* 发起蓝牙连接的客户端示例
*/
@SuppressLint("MissingPermission")
private inner class ConnectThread(device: BluetoothDevice) : Thread() {
private val TAG = "ConnectThread"
private val mmSocket: BluetoothSocket? by lazy(LazyThreadSafetyMode.NONE) {
device.createRfcommSocketToServiceRecord(MY_UUID)
}
public override fun run() {
// Cancel discovery because it otherwise slows down the connection.
bluetoothAdapter?.cancelDiscovery()
mmSocket?.use { socket ->
// Connect to the remote device through the socket. This call blocks
// until it succeeds or throws an exception.
socket.connect()
// The connection attempt succeeded. Perform work associated with
// the connection in a separate thread.
//新线程从远端设备读取数据和写入数据
MyBluetoothService(Handler(Looper.getMainLooper()), socket)
}
}
// Closes the client socket and causes the thread to finish.
fun cancel() {
try {
mmSocket?.close()
} catch (e: IOException) {
Log.e(TAG, "Could not close the client socket", e)
}
}
}
2.4.3 传输数据
/**
* 从连接设备读取数据和向连接设备写入数据示例
*/
class MyBluetoothService(
// handler that gets info from Bluetooth service
private val handler: Handler,
private val mmSocket: BluetoothSocket
) {
private val TAG = "MyBluetoothService"
// Defines several constants used when transmitting messages between the
// service and the UI.
private val MESSAGE_READ: Int = 0
private val MESSAGE_WRITE: Int = 1
private val MESSAGE_TOAST: Int = 2
// ... (Add other message types here as needed.)
private inner class ConnectedThread(private val mmSocket: BluetoothSocket) : Thread() {
private val mmInStream: InputStream = mmSocket.inputStream
private val mmOutStream: OutputStream = mmSocket.outputStream
private val mmBuffer: ByteArray = ByteArray(1024) // mmBuffer store for the stream
override fun run() {
var numBytes: Int // bytes returned from read()
// Keep listening to the InputStream until an exception occurs.
while (true) {
// Read from the InputStream.
numBytes = try {
mmInStream.read(mmBuffer)
} catch (e: IOException) {
Log.d(TAG, "Input stream was disconnected", e)
break
}
// Send the obtained bytes to the UI activity.
val readMsg = handler.obtainMessage(
MESSAGE_READ, numBytes, -1,
mmBuffer)
readMsg.sendToTarget()
}
}
// Call this from the main activity to send data to the remote device.
fun write(bytes: ByteArray) {
try {
mmOutStream.write(bytes)
} catch (e: IOException) {
Log.e(TAG, "Error occurred when sending data", e)
// Send a failure message back to the activity.
val writeErrorMsg = handler.obtainMessage(MESSAGE_TOAST)
val bundle = Bundle().apply {
putString("toast", "Couldn't send data to the other device")
}
writeErrorMsg.data = bundle
handler.sendMessage(writeErrorMsg)
return
}
// Share the sent message with the UI activity.
val writtenMsg = handler.obtainMessage(
MESSAGE_WRITE, -1, -1, mmBuffer)
writtenMsg.sendToTarget()
}
// Call this method from the main activity to shut down the connection.
fun cancel() {
try {
mmSocket.close()
} catch (e: IOException) {
Log.e(TAG, "Could not close the connect socket", e)
}
}
}
}
2.4.4 使用配置文件连接设备
如耳机、A2DP设备可使用API支持的配置文件连接设备,涉及相关类:BluetoothHeadset、BluetoothA2dp类等,基本步骤如下:
1. 获取默认适配器
2. 设置 BluetoothProfile.ServiceListener。此侦听器会在 BluetoothProfile 客户端连接到服务或断开服务连接时向其发送通知。
3. 使用 getProfileProxy() 与配置文件所关联的配置文件代理对象建立连接。在以下示例中,配置文件代理对象是一个 BluetoothHeadset 实例。
4. 在 onServiceConnected() 中,获取配置文件代理对象的句柄。
5. 获得配置文件代理对象后,您可以用其监视连接状态,并执行与该配置文件相关的其他操作。
var bluetoothHeadset: BluetoothHeadset? = null
// Get the default adapter
val bluetoothAdapter: BluetoothAdapter? = BluetoothAdapter.getDefaultAdapter()
private val profileListener = object : BluetoothProfile.ServiceListener {
override fun onServiceConnected(profile: Int, proxy: BluetoothProfile) {
if (profile == BluetoothProfile.HEADSET) {
bluetoothHeadset = proxy as BluetoothHeadset
}
}
override fun onServiceDisconnected(profile: Int) {
if (profile == BluetoothProfile.HEADSET) {
bluetoothHeadset = null
}
}
}
// Establish connection to the proxy.
bluetoothAdapter?.getProfileProxy(context, profileListener, BluetoothProfile.HEADSET)
// ... call functions on bluetoothHeadset
// Close proxy connection after use.
bluetoothAdapter?.closeProfileProxy(BluetoothProfile.HEADSET, bluetoothHeadset)
2.5 销毁蓝牙
始终记得在退出使用的时候,关闭蓝牙连接
/**关闭连接*/
fun cancel() {
try {
socket.close()
} catch (e: IOException) {
Log.e(TAG, "Could not close the connect socket", e)
}
}
/**
*加入使用配置文件连接设备,调用如下方法关闭连接,
*这里以耳机为例
*/
bluetoothAdapter?.closeProfileProxy(BluetoothProfile.HEADSET, bluetoothHeadset)