Android BLE Development Guide III: Detailed Explanation of Central Device-side Development

overview

The Android system has supported BLE since 4.3, but at that time it only supported the mobile phone as the central device. Later, starting from 5.0, the mobile phone can also be used as the peripheral device. Here we explain how the mobile phone as the central device scans and connects peripheral devices, which is the most commonly used in our BLE development.
In the Android system, the SDK provides the BluetoothAdapter class to operate Bluetooth, which provides functions such as turning on and off Bluetooth, starting and stopping scanning devices, and so on. Another key class is BluetoothGatt. From the name, it can be seen that it corresponds to GATT in BLE. GATT has been mentioned in the previous blog post . This is a core class for connecting and communicating with devices. The structure diagram of BluetoothGatt is as follows:
BluetoothGatt
BluetoothGatt contains one or more services BluetoothGattService, each BluetoothGattService service has a uniquely identified UUID, you can get the service through UUID, and you can also get the list of all services in BluetoothGatt.

At the same time, each BluetoothGattService also contains one or more characteristics BluetoothGattCharacteristic, each characteristic is also uniquely identified by UUID, it has a value field, which is a byte array, this array is the data defined according to the protocol, we need to analyze the data . In addition, each BluetoothGattCharacteristic also has a property, which generally has a write type (PROPERTY_WRITE) and a subscription type (PROPERTY_NOTIFY). The write type is used by the central device to send data to the peripheral device, and the subscription type is used by the central device to receive data sent by the peripheral device. used.

Each BluetoothGattCharacteristic will also contain one or more BluetoothGattDescriptors describing it. Each description is also uniquely identified by UUID, and also has a byte array of value field.

Before looking at the implementation of the specific code, let's talk about the general steps:

  1. Check the use of Bluetooth-related permissions;
  2. Scan the device to determine whether it is the device we are interested in. If it is in the broadcast mode, it only needs to scan continuously without establishing a connection, and here it is only necessary to analyze the manufacturer's custom data in the broadcast and it is over. If a connection needs to be established, continue to the next step;
  3. Stop scanning, establish a GATT connection with the device,
  4. Start the discovery service after the connection is successful;
  5. Traverse the service list of the device, and judge whether there is a service we are interested in through the UUID of the service;
  6. Obtain the read feature of the service, enable the subscription, and accept the data sent by the device;
  7. Obtain the write feature of the service, and send data to the device through this feature.

1. Declare permissions

To use Bluetooth in Android, you must first declare permissions in 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"/>

The above two permissions are necessary for Bluetooth, and the following location permission is added in Android 6.0, which is also necessary, otherwise the device will not be found, and the location permission needs to be dynamically applied.

In addition, if you expect that only devices that support BLE can install your application, you can also declare in the manifest file:

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

2. Judgment status

1. First, check whether the user has given the application location permission, if not, you need to apply for the permission;

2. After the location permission is given, it is also necessary to check whether the location service of the mobile phone is turned on, because the location permission is provided, but the location service is not started on the mobile phone, which will also cause the device to be unable to be searched. Here you may wonder why you need positioning when using Bluetooth. In fact, Bluetooth can indeed perform positioning functions, such as BLE mesh, which can achieve indoor positioning. If it is not enabled, you need to jump to the location service setting page;

3. Check whether the Bluetooth is turned on, if not, prompt the user to turn on the Bluetooth;

4. Register the broadcast receiver for bluetooth on and off. When Bluetooth is turned on and off, the system will send out a corresponding broadcast. When receiving the broadcast, we need to do corresponding operations.

The following is the code of my encapsulated BleBaseActivity, which handles the logic related to these states:

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()//用户拒绝给予定位权限
}

scanning device

Next, just inherit BleBaseActivity and implement related methods.

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()//不要忘了在页面关闭的是否停止扫描
    }
}

On the 5.0 system, the SDK provides a new scanning method. The scanned device broadcast data does not need to be parsed by ourselves. The system has already parsed it, and the callback method of the scanned device is in the main thread. However, the old callback method is in the child thread, which needs attention. If you need to refresh the UI in the callback, you need to switch to the main thread.

The scanning methods provided by the other two different system versions can both pass in a UUID list for filtering. Only when the service UUID of the scanned device is included in the UUID list, the scanning callback method is called. The new scan method on 5.0 can also set the scan mode, such as SCAN_MODE_LOW_LATENCY and SCAN_MODE_LOW_POWER and so on.

//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)

After scanning to our device, if the device is only broadcasting data and not based on connection, then parse out the manufacturer-defined data in TODO, and display or save the data according to the needs, and it is over here. How to parse these byte data will be introduced in a later article.

If the device needs to be connected and interacted with, then connect the device at TODO and stop scanning the device.

connect device

 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)
            }
        }
    }

When connecting a device, use the connectGatt method of the bluetoothDevice object to establish a connection, or use the bluetoothAdapter. false , indicating that automatic connection is not required, and the third parameter needs to be passed in the BluetoothGattCallback object, which is the core of interacting with the device. In addition, it should be noted that every time a connection is established with a device, it needs to be scanned first, and then the connection is made after the device is scanned. Can't remember the mac address of the device, and connect directly without scanning.

In the BluetoothGattCallback object, there are many callback methods, all of which are callbacks when interacting with the device. There are several important methods:

  • onConnectionStateChange This method will be called back when the connection to the device is successful, disconnected, or a connection error (133, etc.) occurs. Note that the connection here is successful, but it is only connected, and communication cannot be performed (similar to finding someone, but also need to confirm the identity) can give you the information), you need to discover the service discoverServices;
  • onServicesDiscovered Callback for services discovered. When the service we need is obtained, we can start to subscribe to the message and obtain the writing feature, and now we have truly established two-way communication with the device.
  • onCharacteristicChanged accepts the data sent by the device, which is the original binary data. For example, a temperature detector will return real-time temperature, unit and other information. Of course, this information needs to be analyzed according to the protocol.

send data

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

Peripheral devices usually send raw binary data and only accept binary data. Therefore, when writing data to the device, it is necessary to parse the data into a bytes array according to the protocol before sending it.

Disconnect

    bluetoothGatt.disconnect()
    bluetoothGatt.close()

Usually when the bluetoothGatt.disconnect() method is called, the onConnectionStateChange will be called back, and the bluetoothGatt.close() method will be called in the onConnectionStateChange. But sometimes the onConnectionStateChange method may not be called back, and then bluetoothGatt.close() may not be executed, which may cause an error during the next connection (133 will appear after multiple disconnections), so the two methods can also be called together when disconnecting.

Guess you like

Origin blog.csdn.net/ganduwei/article/details/95237006