Android BLE开发指南三:中央设备端开发详解

概述

Android 系统从4.3开始支持BLE,但当时只支持手机作为中心设备,后来从5.0开始,手机亦可作为外围设备。这里我们讲解手机作为中心设备是如何扫描和连接外围设备的,这是我们BLE开发中最常用到的。
在 Android 系统中,SDK 提供了 BluetoothAdapter 类对蓝牙进行操作,该类提供了开启和关闭蓝牙,开始和停止扫描设备等等功能。还有另外一个关键的类是 BluetoothGatt ,从名字就可以看出它对应BLE中的GATT,GATT在前面博文中我们有讲过,这是与设备进行连接和通信的一个核心类。BluetoothGatt 的结构图如下:
BluetoothGatt
BluetoothGatt 包含一个或多个服务 BluetoothGattService,每个 BluetoothGattService 服务都有唯一标识的 UUID,可以通过 UUID 获取服务,也可以获取 BluetoothGatt 中所有的服务列表。

同时每个 BluetoothGattService 也包含了一个或多个特征 BluetoothGattCharacteristic,每个特征也是通过 UUID 唯一标识,它有一个 value 字段,是一个 byte 数组,这个数组就是按照协议定义的数据,我们需要对该数据进行解析。另外每个 BluetoothGattCharacteristic 还有一个属性,一般有写类型的(PROPERTY_WRITE),订阅类型的(PROPERTY_NOTIFY),写类型是中心设备发送数据给外设用到的,订阅类型是中心设备接收外设发送的数据用到的。

每个 BluetoothGattCharacteristic 也会包含一个或多个描述 BluetoothGattDescriptor 。每个描述也是通过 UUID 唯一标识,同样也有一个 value 字段的 byte 数组。

在看具体代码的实现前,我们先说下大概步骤:

  1. 检查使用蓝牙相关权限;
  2. 扫描设备,判断是否是我们感兴趣的设备。如果是广播模式,只用不停的扫描,不需用建立连接,到这里只要解析广播里面的厂商自定义数据就完了。如果需要建立连接继续下一步;
  3. 停止扫描,与设备建立 GATT 连接,
  4. 连接成功后启动发现服务;
  5. 遍历设备的服务列表,通过服务的UUID判断是否有我们感兴趣的服务;
  6. 获取服务的读特征,开启订阅,接受设备发送过来的数据;
  7. 获取服务的写特征,通过该特征发送数据给设备端。

1、声明权限

Android 中使用蓝牙必须先在 AndroidManifest 中声明权限:

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

上面两个权限是蓝牙必须要的,下面的定位权限是Android 6.0中增加的,也是必须的,不然会搜索不到设备,且定位权限需要动态申请。

另外,如果你期望只有支持 BLE 的设备才能安装你的应用,也可以在清单文件中申明:

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

2、判断状态

1、首先要检查用户是否给予应用定位权限,如果没有便需要申请该权限;

2、给予定位权限后,还需要检查手机的定位服务是否开启,因为有定位权限,但是手机没有开始定位服务,也会导致无法搜索到设备。这里大家可能会奇怪使用蓝牙为什么会需要定位,其实蓝牙确实是可以做定位功能的,比如 BLE mesh ,就可以做到室内定位。如果没有开启,需要跳转到定位服务设置页面;

3、检查蓝牙是否开启,如果没有开启,提示用户开启蓝牙;

4、注册蓝牙开启和关闭的广播接收器。蓝牙开启和关闭时,系统会发出相应的广播,收到该广播时,我们需要做相应的操作。

下面是我封装的 BleBaseActivity 的代码,里面处理这些状态相关的逻辑:

abstract class BleBaseActivity : AppCompatActivity() {
    
    

    private val permissionRequestCode = 1530
    private val permissionSettingCode = 1532
    private val locationSettingCode = 1531

    private var bluetoothReceiver: BroadcastReceiver? = null

    override fun onCreate(savedInstanceState: Bundle?) {
    
    
        super.onCreate(savedInstanceState)
        BluetoothAdapter.getDefaultAdapter() ?: return//为 null 表示设备不支持蓝牙, return
        checkStatus()
        bluetoothReceiver = object : BroadcastReceiver() {
    
    
            override fun onReceive(context: Context, intent: Intent) {
    
    
                when (intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, -1)) {
    
    
                    BluetoothAdapter.STATE_ON -> {
    
    //蓝牙已经打开
                        onBluetoothOpen()
                        checkStatus()
                    }
                    BluetoothAdapter.STATE_TURNING_OFF -> {
    
     //蓝牙正在关闭
                        onBluetoothClose()
                    }
                }
            }
        }
        //注册蓝牙状态改变广播接收器
        val intentFilter = IntentFilter()
        intentFilter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED)
        registerReceiver(bluetoothReceiver, intentFilter)
    }

    private fun checkStatus() {
    
    
        //检查定位权限 --> 检查定位服务 --> 检查蓝牙开启状态
        if (checkLocalPermission()) {
    
    //定位权限已授权
            if (locationIsEnable()) {
    
    //定位服务已开启
                if (bluetoothIsEnabled()) {
    
    //蓝牙已开启
                    //所有状态都已经OK
                    onBleEverythingOk()
                } else {
    
     // 蓝牙未开启 ,提示开启
                    openBluetooth()
                }
            } else {
    
     // 没有开启定位服务 ,提示去打开
                goToLocationSetting()
            }
        } else {
    
     // 没有授权 请求定位权限
            ActivityCompat.requestPermissions(
                this,
                arrayOf(Manifest.permission.ACCESS_FINE_LOCATION),
                permissionRequestCode
            )
        }
    }

    private fun openBluetooth() {
    
    
        val enableBleIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
        startActivity(enableBleIntent)
    }

    private fun locationIsEnable(): Boolean {
    
    
        //Android6.0以下不需要开启GPS服务即可搜索蓝牙
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) return true
        val locationManager = getSystemService(Context.LOCATION_SERVICE) as LocationManager
        return locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER) ||
                locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)
    }

    private fun checkLocalPermission(): Boolean =
        ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) ==
                PackageManager.PERMISSION_GRANTED

    private fun bluetoothIsEnabled(): Boolean = BluetoothAdapter.getDefaultAdapter().isEnabled

    private fun goToPermissionSetting() {
    
    
        AlertDialog.Builder(this)
            .setTitle("Tip")
            .setMessage("蓝牙需要定位权限,是否去打开定位权限")
            .setPositiveButton("是", object : DialogInterface.OnClickListener {
    
    
                override fun onClick(dialog: DialogInterface, which: Int) {
    
    
                    val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
                    intent.data = Uri.parse("package:$packageName")
                    if (packageManager.resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY) != null) {
    
    
                        //使用 startActivityForResult 是为了能在用户返回后,在onActivityResult检查用户设置的结果
                        startActivityForResult(intent, permissionSettingCode)
                    }
                }
            }).setNegativeButton("否", object : DialogInterface.OnClickListener {
    
    
                override fun onClick(dialog: DialogInterface?, which: Int) {
    
    
                    //用户拒绝打开权限设置页面
                    onLocationPermissionDenied()
                }
            }).show()
    }

    private fun goToLocationSetting() {
    
    
        AlertDialog.Builder(this)
            .setTitle("Tip")
            .setMessage("使用蓝牙需要开启定位服务,是否去打开定位服务")
            .setPositiveButton("是", object : DialogInterface.OnClickListener {
    
    
                override fun onClick(dialog: DialogInterface, which: Int) {
    
    
                    val intent = Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS)
                    if (packageManager.resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY) != null) {
    
    
                        //使用 startActivityForResult 是为了能在用户返回后,在onActivityResult检查用户设置的结果
                        startActivityForResult(intent, locationSettingCode)
                    }
                }
            }).setNegativeButton("否", object : DialogInterface.OnClickListener {
    
    
                override fun onClick(dialog: DialogInterface?, which: Int) {
    
    
                    //用户拒绝打开定位服务设置页面
                    onLocationServiceDenied()
                }
            }).show()
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    
    
        super.onActivityResult(requestCode, resultCode, data)
        when (requestCode) {
    
    
            permissionSettingCode -> {
    
    
                if (checkLocalPermission()) {
    
    //用户在权限设置页面给予了权限
                    if (locationIsEnable()) {
    
    //定位服务已打开
                        if (bluetoothIsEnabled()) {
    
    //蓝牙已打开
                            //所有状态都已经OK
                            onBleEverythingOk()
                        } else {
    
    //提示开启蓝牙
                            openBluetooth()
                        }
                    } else {
    
    //定位服务未开启,提示用户开启定位服务
                        goToLocationSetting()
                    }
                } else {
    
    
                    //用户从权限设置页面回来,还是没有开启给予定位权限
                    onLocationPermissionDenied()
                }
            }
            locationSettingCode -> {
    
    
                if (locationIsEnable()) {
    
    //用户在定位服务设置页面开启了定位服务
                    if (bluetoothIsEnabled()) {
    
    //蓝牙已开启
                        //可以开始搜索了
                        onBleEverythingOk()
                    } else {
    
    //蓝牙未开启,提示用户开启蓝牙
                        openBluetooth()
                    }
                } else {
    
    
                    //用户从定位服务设置页面回来,还是没有开启定位服务
                    onLocationServiceDenied()
                }
            }
        }
    }

    override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
    
    
        if (requestCode == permissionRequestCode && grantResults.isNotEmpty()) {
    
    
            if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
    
    //定位权限已授权
                if (locationIsEnable()) {
    
    //定位服务已开启
                    if (bluetoothIsEnabled()) {
    
    //蓝牙已开启
                        //所有状态都已经OK
                        onBleEverythingOk()
                    } else {
    
    //蓝牙未开启,提示用户开启蓝牙
                        openBluetooth()
                    }
                } else {
    
    //提示用户去开启定位服务
                    goToLocationSetting()
                }
            } else {
    
    //用户拒绝了授权,提示用户去设置页面开启权限
                goToPermissionSetting()
            }
        }
    }

    override fun onDestroy() {
    
    
        super.onDestroy()
        //注销广播接收器
        bluetoothReceiver?.let {
    
    
            unregisterReceiver(it)
        }
    }

    abstract fun onBluetoothOpen()//蓝牙开启了
    abstract fun onBluetoothClosing()//蓝牙正在关闭
    abstract fun onBleEverythingOk()//所有状态都已经OK,可以开始搜索设备了
    abstract fun onLocationServiceDenied()//用户拒绝打开定位服务
    abstract fun onLocationPermissionDenied()//用户拒绝给予定位权限
}

扫描设备

接下来只要继承 BleBaseActivity ,实现相关方法即可,

class BleMainActivity : BleBaseActivity() {
    
    

    private var scanCallback: ScanCallback? = null
    private var leScanCallback: BluetoothAdapter.LeScanCallback? = null

    private var isScanning = false//是否是扫描状态

    //我们需要与之交互的设备的服务UUID
    private val myDeviceUUID = ParcelUuid.fromString("0000180D-0000-1000-8000-00805F9B34FB")

    override fun onBluetoothOpen() {
    
    
    }

    override fun onBluetoothClosing() {
    
    
        isScanning = false
    }

    override fun onBleEverythingOk() {
    
    
        //开始扫描
        val bluetoothAdapter = BluetoothAdapter.getDefaultAdapter()
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
    
    //5.0 及以上的扫描方法
            scanCallback = object : ScanCallback() {
    
    
                override fun onScanFailed(errorCode: Int) {
    
    
                    isScanning = false
                }

                override fun onScanResult(callbackType: Int, result: ScanResult) {
    
    
                    val rssi = result.rssi//信号值,单位dBm,为负数,越接近0信号越强
                    val address = result.device.address//设备MAC地址
                    val name = result.device.name//设备名字
                    //设备广播的数据,有可能是null
                    val scanRecord = result.scanRecord ?: return
                    //设备的服务 UUID 列表,有可能是null
                    val uuidList = scanRecord.serviceUuids ?: return
                    //设备的厂商数据,可能是null,或者是空的,即设备没有定义厂商数据
                    //厂商数据放在一个键值对集合中,key是厂商的ID, value是ID对应的自定义数据
                    val manufacturerData: SparseArray<ByteArray> = scanRecord.manufacturerSpecificData ?: return
                    // 这个方法中得到的设备是搜到的所有设备,需要做过滤,通过服务UUID或厂商ID过滤
                    // 如果搜索到的设备包含我们的UUID,表示是我们的设备,如果不包含,表示是其他的不相关设备
                    // 如果我们的设备有定义的厂商数据,可以判断下manufacturerData是不是空的,
                    // 不是空的就接着比较:manufacturerData中的ID与我们设备的厂商数据中的厂商ID是否一样,
                    // 一样的话,那这个设备就一定是我们要的设备了
                    if (uuidList.contains(myDeviceUUID) && manufacturerData.size() > 0) {
    
    
                        //TODO
                    }
                }

                override fun onBatchScanResults(results: MutableList<ScanResult>?) {
    
    
                }
            }
            bluetoothAdapter.bluetoothLeScanner.startScan(scanCallback)
        } else {
    
    //5.0 以下的扫描方法
            leScanCallback = object : BluetoothAdapter.LeScanCallback {
    
    
                override fun onLeScan(device: BluetoothDevice, rssi: Int, scanRecord: ByteArray) {
    
    
                    val adData = ParseBluetoothAdData.parse(scanRecord)//自己解析广播数据
                    val address = device.address//设备MAC地址
                    val name = device.name//设备名字
                    //设备的服务 UUID 列表,有可能是空的
                    val uuidList = adData.UUIDs
                    //设备的厂商数据,前两个字节表示厂商ID,后面的是的数据,可能是null,或者是空的,即设备没有定义厂商数据
                    val manufacturerBytes = adData.manufacturerByte ?: return
                    // 这个方法中得到的设备是搜到的所有设备,需要做过滤,通过服务UUID或厂商ID过滤
                    // 如果搜索到的设备包含我们的UUID,表示是我们的设备,如果不包含,表示是其他的不相关设备
                    // 如果我们的设备有定义的厂商数据,可以判断下manufacturerData是不是空的,
                    // 不是空的就接着比较:manufacturerData中的ID与我们设备的厂商数据中的厂商ID是否一样,
                    // 一样的话,那这个设备就一定是我们要的设备了
                    if (uuidList.contains(myDeviceUUID.uuid) && manufacturerBytes.isNotEmpty()) {
    
    
                        //TODO
                    }
                }
            }
            bluetoothAdapter.startLeScan(leScanCallback)
        }
        isScanning = true
    }

    override fun onLocationServiceDenied() {
    
    
    }

    override fun onLocationPermissionDenied() {
    
    
    }

    private fun stopScan() {
    
    
        if (!isScanning) return//没有在扫描,不用停止
        isScanning = false
        val bluetoothAdapter = BluetoothAdapter.getDefaultAdapter()
        if (!bluetoothAdapter.isEnabled) return//蓝牙已经关闭了,还去停止扫描干嘛
        //停止扫描
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
    
    
            scanCallback?.let {
    
    
                bluetoothAdapter.bluetoothLeScanner.stopScan(it)
            }
        } else {
    
    
            leScanCallback?.let {
    
    
                bluetoothAdapter.stopLeScan(it)
            }
        }
    }

    override fun onDestroy() {
    
    
        super.onDestroy()
		stopScan()//不要忘了在页面关闭的是否停止扫描
    }
}

在5.0系统上,SDK 提供了新的扫描方法,扫描到的设备广播数据不需要我们自己解析,系统已经解析好了,并且扫描到设备的回调方法都是在主线程。而旧的回调方法在子线程里面,这点需要注意,如果需要在回调这里刷新UI,就需要切换到主线程。

另外两个不同系统版本提供的扫描方法,都可以传入一个UUID列表进行过滤,只有扫描到的设备的服务UUID包含在该UUID列表时,才调用扫描回调方法。5.0 上新的扫描方法还可以设置扫描模式,比如 SCAN_MODE_LOW_LATENCY 和 SCAN_MODE_LOW_POWER 等。

//5.0 及以上
val scanSettings = ScanSettings.Builder()
    .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)//低延迟,扫描间隔很短,不停的扫描,更容易扫描到设备,但是更耗电一些,建议APP在前台是才使用这种模式
    //.setScanMode(ScanSettings.SCAN_MODE_LOW_POWER)//省电的模式,扫描间隔会长一点,扫描到设备花的时间会长一些
    .build()
val scanFilter = ScanFilter.Builder()
    .setServiceUuid(myDeviceUUID)
    .build()
bluetoothAdapter.bluetoothLeScanner.startScan(listOf(scanFilter), scanSettings, scanCallback)

//5.0 以下	
bluetoothAdapter.startLeScan(arrayOf(myDeviceUUID.uuid), leScanCallback)

扫描到我们的设备后,如果该设备只是广播数据,不是基于连接的,那就在 TODO 的地方解析出厂商自定义的数据,根据需求显示或保存数据等操作即可,到这里就结束了。关于如何解析这些 byte 数据,在后面的文章会介绍。

如果设备是需要连接,进行交互的,那接下来,就在 TODO 的地方连接该设备,并停止扫描设备。

连接设备

 private fun connDevice(bluetoothDevice: BluetoothDevice) {
    
    
 		//false:不需要自动连接
        bluetoothDevice.connectGatt(applicationContext, false, bluetoothGattCallback)
    }

 private fun connDevice(address: String) {
    
    
        val bluetoothAdapter = BluetoothAdapter.getDefaultAdapter()
        val bluetoothDevice = bluetoothAdapter.getRemoteDevice(address)
        //false:不需要自动连接
        bluetoothDevice.connectGatt(applicationContext, false, bluetoothGattCallback)
    }

 private val bluetoothGattCallback: BluetoothGattCallback by lazy {
    
    

        object : BluetoothGattCallback() {
    
    

            //连接状态改变的回调
            override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
    
    
                when (newState) {
    
    
                    BluetoothProfile.STATE_CONNECTED -> {
    
    
                        //连接成功,开启发现服务
                        gatt.discoverServices()
                    }
                    BluetoothProfile.STATE_DISCONNECTED -> {
    
    
                        //连接断开,关闭GATT资源
                        gatt.close()
                    }
                }
            }

            //发现服务的回调
            override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
    
    
                if (status == BluetoothGatt.GATT_SUCCESS) {
    
    
                    //获取感兴趣的服务
                    val gattService = gatt.getService(UUID.fromString("")) ?: return//填写你们定义的服务UUID
                    //获取该服务的读特征,用于订阅设备发送的数据
                    val notifyCharacteristic =
                        gattService.getCharacteristic(UUID.fromString("")) ?: return//填写你们定义的读特征UUID

                    //订阅 notify 这是模板代码,通常都是固定的------>start
                    gatt.setCharacteristicNotification(notifyCharacteristic, true)
                    val descriptor =
                        notifyCharacteristic.getDescriptor(UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"))//这个UUID是固定的
                    descriptor.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
                    gatt.writeDescriptor(descriptor)
                    //订阅 notify ------>end

                    //获取写特征值,用于发送数据
                    val writeCharacteristic =
                        gattService.getCharacteristic(UUID.fromString("")) ?: return//填写你们定义的写特征UUID
                }
            }

            //接收到设备发送的数据的回调
            override fun onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) {
    
    
                //设备发送的数据, 是byte数组,按照协议解析即可
                val bytes = characteristic.value
                //TODO
            }

            //向设备发送数据时会回调该方法,每调用一次gatt.writeCharacteristic()就回调一次
            override fun onCharacteristicWrite(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, status: Int) {
    
    
                super.onCharacteristicWrite(gatt, characteristic, status)
            }

            //调用gatt.readCharacteristic 后回调读到的数据
            override fun onCharacteristicRead(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, status: Int) {
    
    
                super.onCharacteristicRead(gatt, characteristic, status)
            }

            //每调用一次gatt.writeDescriptor就回调一次
            override fun onDescriptorWrite(gatt: BluetoothGatt, descriptor: BluetoothGattDescriptor, status: Int) {
    
    
                super.onDescriptorWrite(gatt, descriptor, status)
            }

            //调用gatt.readDescriptor 后回调读到的数据
            override fun onDescriptorRead(gatt: BluetoothGatt, descriptor: BluetoothGattDescriptor, status: Int) {
    
    
                super.onDescriptorRead(gatt, descriptor, status)
            }
        }
    }

连接设备时,使用 bluetoothDevice 对象的 connectGatt 方法即可建立连接,或者使用 bluetoothAdapter.getRemoteDevice 方法传入扫描到的 mac 地址获取 bluetoothDevice 对象再进行连接,connectGatt 发放需要传入三个参数,第二个最好传 false ,表示不需要自动连接,第三个参数需要传入 BluetoothGattCallback 对象,该对象是与设备交互的核心。另外,需要注意,每一次和设备建立连接,都需要经过先扫描,扫描到设备后才进行连接。不能记住设备的mac地址,不经过扫描而直接连接。

BluetoothGattCallback 对象中,有很多回调方法,都是在与设备进行交互时的回调。其中有几个很重要的方法:

  • onConnectionStateChange 当与设备连接成功、断开或者连接发生错误(133等)都会回调用该方法,注意这里的连接成功,只是连上了,还不能进行通信(类似只是找到人了,还需要确认身份才可以将情报给你),需要去发现服务 discoverServices ;
  • onServicesDiscovered 发现了服务的回调。当获取到我们需要的服务时,就可以开始订阅消息,和获取写的特征,现在即真正和设备建立了双向通信。
  • onCharacteristicChanged 接受设备发送过来的数据,是原始的二进制数据。比如一个温度探测仪,这里就会返回实时的温度,单位等信息,当然这些信息需要按照协议解析出来。

发送数据

    fun sendData(bytes: ByteArray) {
    
    
        if (writeCharacteristic != null) {
    
    
            writeCharacteristic.setValue(bytes)
            writeCharacteristic.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE)
            bluetoothGatt.writeCharacteristic(writeCharacteristic)
        }
    }

外围设备通常发送的数据是原始二进制数据,也只接受二进制数据,所以往设备写入数据时,也需要按照协议,将数据解析为一个 bytes 数组里面,才可以发送。

断开连接

    bluetoothGatt.disconnect()
    bluetoothGatt.close()

通常当调用 bluetoothGatt.disconnect() 方法后,会回调 onConnectionStateChange ,在 onConnectionStateChange 里面再去调用 bluetoothGatt.close() 方法。但有时可能不会回调 onConnectionStateChange 方法,继而没有执行 bluetoothGatt.close() ,继而可能导致下次连接时出现错误(多次断连会出现133),所以断开连接时也可以两个方法一起调用。

猜你喜欢

转载自blog.csdn.net/ganduwei/article/details/95237006