1、开篇

本文将主要讲述Android应用开发中对BLE API的使用。Android 4.3(API 18)开始支持蓝牙4.0,但此时Android手机只能作为中心设备或者说主设备,不能作为从设备。Android 5.0(API 21)以后,Android开始支持从设备模式。Android 4.3和5.0以后的API会有一些差别,本文实例会使用5.0以后的API。本文会分别讲解主设备和从设备两种模式下的开发流程。

2、从设备模式

先从从设备模式开始,从设备的工作是发送广播,等待主设备发起连接,双方通过约定好的具有特定的UUID的Service、Characteristic和Descriptor进行通信,从设备为服务端,主设备为客户端。

2.1. manifest配置

在开发之前,我们需要在Manifest里面添加如下权限和功能声明:

<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<!-- Android 6.0以上需要定位,9.0及以下可以只需要ACCESS_COARSE_LOCATION,但是Android 10及以上需要ACCESS_FINE_LOCATION -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

<!-- 声明需要蓝牙BLE功能的手机才适用本App -->
<uses-feature
    android:name="android.hardware.bluetooth_le"
    android:required="true" />

2.2 开启Gatt Server

既然作为服务端,那么肯定是需要为客户端提供服务了。一个Service我们可以理解为某一个类型的数据服务,比如手环里的健康管理数据等。一个Service可以包含0个或者若干个其他Service,前者称为priamry service,后者称为Secondary service。一个Service应该包含1个以上的Characteristic,一个Characteristic可以包含0个或者若干个Descriptor。开启Gatt Server后应至少添加一个Service。

// 相关ID的定义
private val SERVICEID = ParcelUuid.fromString("0000110B-0000-1000-8000-00805F9B34FB")
private val CHARID = ParcelUuid.fromString("00008888-0000-1000-8000-00805F9B34FB")
private val DESCID = ParcelUuid.fromString("0000666-0000-1000-8000-00805F9B34FB")

// 获取系统蓝牙服务
val manager: BluetoothManager = getSystemService(BLUETOOTH_SERVICE) as BluetoothManager

// 开启Gatt server
val gattServer = manager.openGattServer(this, serverCallback)

// 初始化可读可写的Descriptor
val gattDescriptor = BluetoothGattDescriptor(
    DESCID.uuid,
    BluetoothGattCharacteristic.PERMISSION_READ or BluetoothGattCharacteristic.PERMISSION_WRITE
)
gattDescriptor.value = byteArrayOf(1)

// 初始化可读可写的Characteristic
val characteristic = BluetoothGattCharacteristic(
    CHARID.uuid,
    BluetoothGattCharacteristic.PROPERTY_READ or BluetoothGattCharacteristic.PROPERTY_WRITE,
    BluetoothGattCharacteristic.PERMISSION_READ or BluetoothGattCharacteristic.PERMISSION_WRITE
)
characteristic.writeType
characteristic.addDescriptor(gattDescriptor)
characteristic.value = byteArrayOf( 3, 4)

// 初始化Service
val gattService = BluetoothGattService(
    SERVICEID.uuid,
    BluetoothGattService.SERVICE_TYPE_PRIMARY
)
gattService.addCharacteristic(characteristic)

// 把service添加到Gatt server,对外提供服务
gattServer.addService(gattService)

Characteristic和Descriptor都是可以指定读写权限的。
另外,openGattServer方法需要传入一个BluetoothGattServerCallback类参数,也就是上面的serverCallback,在这个Callback中,我们会收到客户端的请求的回调,并需要根据请求作出响应:

private val serverCallback = object : BluetoothGattServerCallback() {

        private var state = BluetoothProfile.STATE_DISCONNECTED

    /**
     * 连接状态发生改变回调。一共有四种状态:连接中、已连接、断开连接中、断开连接。
     * @param device:连接的设备
     * @param status:蓝牙状态码
     * @param newState:改变后新的连接状态
     */
    override fun onConnectionStateChange(device: BluetoothDevice?, status: Int, newState: Int) {
        val oldState = state
        state = newState
        if(status == BluetoothGatt.GATT_SUCCESS && newState == BluetoothProfile.STATE_CONNECTED) {
            this@MainActivity.device = device
        }
        Log.i(TAG, "onConnectionStateChange: $oldState => $newState, status: $status")
    }

    /**
     * 添加服务回调
     */
    override fun onServiceAdded(status: Int, service: BluetoothGattService?) {
        Log.i(TAG, "onServiceAdded: ${service?.uuid}, stateus: $status")
    }

    /**
     * 收到读特征值的请求,在此方法中需要通过BluetoothGattServer#sendResponse方法做出响应
     * @param device:客户端设备
     * @param requestId:本次请求的id
     * @param characteristic:请求读取的特征值
     */
    override fun onCharacteristicReadRequest(
        device: BluetoothDevice,
        requestId: Int,
        offset: Int,
        characteristic: BluetoothGattCharacteristic
    ) {
        Log.i(TAG, "onCharacteristicReadRequest, requestId: $requestId, offset: $offset")
        gattServer?.sendResponse(
            device,
            requestId,
            BluetoothGatt.GATT_SUCCESS,
            offset,
            characteristic.value
        )
    }

    /**
     * 客户端请求写入特征值,如果客户端需要回应,在此方法中需要通过BluetoothGattServer#sendResponse方法做出响应
     * @param device:客户端设备
     * @param requestId:本次请求的id
     * @param characteristic:请求写入的特征值
     */
    override fun onCharacteristicWriteRequest(
        device: BluetoothDevice?, requestId: Int,
        characteristic: BluetoothGattCharacteristic,
        preparedWrite: Boolean,
        responseNeeded: Boolean,
        offset: Int,
        value: ByteArray
    ) {
        characteristic.value = value
        Log.i(TAG, "onCharacteristicWriteRequest, requestId: $requestId, offset: $offset, preparedWrite: $preparedWrite, responseNeeded: $responseNeeded, value: ${bytesToString(value)}")
        if(responseNeeded) {
            gattServer?.sendResponse(
                device,
                requestId,
                BluetoothGatt.GATT_SUCCESS,
                offset,
                characteristic.value
            )
        }
    }

    /**
     * 收到读取描述符的请求,在此方法中需要通过BluetoothGattServer#sendResponse方法做出响应
     * @param device:客户端设备
     * @param requestId:本次请求的id
     * @param descriptor:请求读取的描述符
     */
    override fun onDescriptorReadRequest(
        device: BluetoothDevice,
        requestId: Int,
        offset: Int,
        descriptor: BluetoothGattDescriptor
    ) {
        Log.i(TAG, "onDescriptorReadRequest, requestId: $requestId, offset: $offset")
        gattServer?.sendResponse(
            device,
            requestId,
            BluetoothGatt.GATT_SUCCESS,
            offset,
            descriptor.value
        )
    }

    /**
     * 客户端请求写入描述符,如果客户端需要回应,在此方法中需要通过BluetoothGattServer#sendResponse方法做出响应
     * @param device:客户端设备
     * @param requestId:本次请求的id
     * @param descriptor:请求写入的描述符
     */
    override fun onDescriptorWriteRequest(
        device: BluetoothDevice?,
        requestId: Int,
        descriptor: BluetoothGattDescriptor,
        preparedWrite: Boolean,
        responseNeeded: Boolean,
        offset: Int,
        value: ByteArray
    ) {
        descriptor.value = value
        Log.i(TAG, "onCharacteristicReadRequest, requestId: $requestId, offset: $offset, preparedWrite: $preparedWrite, responseNeeded: $responseNeeded")
        if(responseNeeded) {
            gattServer?.sendResponse(
                device,
                requestId,
                BluetoothGatt.GATT_SUCCESS,
                offset,
                descriptor.value
            )
        }
    }

    /**
     * MTU发生变化的回调
     * @param mtu:变化后MTU的大小
     */
    override fun onMtuChanged(device: BluetoothDevice?, mtu: Int) {
        Log.d(TAG, "onMtuChanged: $mtu")
    }

    /**
     * 给客户端发送完通知的回调
     */
    override fun onNotificationSent(device: BluetoothDevice?, status: Int) {
        Log.d(TAG, "onNotificationSent: $status")
    }
}

2.3 发送广播

要使别的设备可以扫描到我们的设备,那么我们必须发送蓝牙广播才行。

// 广播设置
private val settings = AdvertiseSettings.Builder()
    //设置广播频率,LOW_LATENCY:100ms左右,BALANCED:250~300ms左右,LOW_POWER:1000ms左右
    .setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_LOW_LATENCY)
    // 是否可以连接,false就是不可连接只广播
    .setConnectable(true)
    // 设置广播信号强度
    .setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_HIGH)
    // 设置超时时间(毫秒),最大不能超过180000,0为不限制
    .setTimeout(100)
    .build()

// 广播数据
private val data = AdvertiseData.Builder()
    // 16 bit Service UUID
    .addServiceUuid(SERVICEID)
    // 设置厂商自定义数据
    //.addManufacturerData(0x202, byteArrayOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 0xA, 0xB, 0xC, 0xD, 0xF, 0x10))
    // 是否在广播中包含设备名称
    .setIncludeDeviceName(true)
    // 是否包含广播信号强度
    //.setIncludeTxPowerLevel(true)
    .build()

// 扫描响应数据
private val response = AdvertiseData.Builder()
    // 设置厂商自定义数据
    .addManufacturerData(0x202, byteArrayOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 0xA, 0xB, 0xC, 0xD))
    // 是否在广播中包含设备名称
    .setIncludeDeviceName(true)
    // 是否包含广播信号强度
    //.setIncludeTxPowerLevel(true)
    .build()


// 开启广播
advertiser.startAdvertising(settings, data, response, advCallback)

其中response参数可以为null。

2.4 响应客户端请求

事实上2.2节已经展示了响应请求的代码,这里提出来再说一下。

gattServer?.sendResponse(
    device,
    requestId,
    BluetoothGatt.GATT_SUCCESS,
    offset,
    descriptor.value
)

在收到客户端请求的相关回调之后,根据是否需要响应以及实际业务情况发送响应到客户端。

2.5 给客户端发送通知

根据本机运行状态或者接收到外部数据之后,特征值可能会发生变化,这时候我们可能需要主动通知客户端。

val random = Random()
val values = ByteArray(2)
random.nextBytes(values)
characteristic!!.value = values

// 发送通知的关键代码
gattServer?.notifyCharacteristicChanged(it, characteristic, false)

这里使用了随机数据模拟变化。

3、主设备模式

主设备模式开发流程大致有扫描设备、连接设备、发现服务和数据通信几个步骤。在开发之前,我们也需要在manifest中配置权限和功能声明。具体可参考上面从机模式,这里不赘述。

3.1 扫描设备

// 扫描过滤
private val filters = listOf(
    ScanFilter.Builder()
        // 厂商数据过滤
        //.setManufacturerData(0, byteArrayOf(0))
        // 设备名称过滤
        .setDeviceName("Peripheral")
        // Service UUID过滤
        .setServiceUuid(ParcelUuid(Constants.SERVICEID) )
        .build(),
)

// 扫描设置
private val settings = ScanSettings.Builder()
    .setReportDelay(0)
    // 扫描频率,对应广播频率
    .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
    .build()

// 扫描回调
private val callback = object : ScanCallback() {

    override fun onScanResult(callbackType: Int, result: ScanResult) {

        val device = result.device ?: return

        Log.d(TAG, "Found device ${result.device.name}: ${result.device.address}")

        if(!devices.contains(device)) {
            devices.add(device)
            deviceAdapter.notifyItemInserted(devices.size - 1)
        }

    }

    override fun onScanFailed(errorCode: Int) {
        super.onScanFailed(errorCode)

        Log.e(TAG, "Scan failed: $errorCode")
        Toast.makeText(this@ScanActivity, "Scan failed: $errorCode", Toast.LENGTH_SHORT).show()
    }
}

private fun scan() {
    val adapter = BluetoothAdapter.getDefaultAdapter()
    if(adapter == null || !adapter.isEnabled) {
        Toast.makeText(this, "Open BT function first", Toast.LENGTH_SHORT).show()
        finish()
        return
    }

    // 开启扫描的关键代码
    adapter.bluetoothLeScanner.startScan(filters, settings, callback)
}

startScan还有一个只传递callback的重载方法,使用默认的扫描设置且不过滤设备。

3.2 连接设备

/**
 * 连接设备
 */
private fun connect(device: BluetoothDevice) {
    if(gatt != null) {
        gatt!!.connect()
        return
    }

    // 连接设备的关键代码
    gatt = device.connectGatt(this, false, object : BluetoothGattCallback() {

        /**
         * 连接状态发生改变的回调
         * @param status:蓝牙状态码
         * @param newState:发生变化后新的状态
         */
        override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
            super.onConnectionStateChange(gatt, status, newState)

            if (status != BluetoothGatt.GATT_SUCCESS) {
                this@MainActivity.gatt = null
                return
            }

            if(newState == BluetoothProfile.STATE_CONNECTED) {
                // 发现服务
                gatt.discoverServices()
            }
        }

        /**
         * 发现服务步骤完成的回调
         */
        override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
            Log.d(TAG, "onServicesDiscovered: $status")
            
            // 找我们需要的服务
            val service = gatt.services?.firstOrNull { it.uuid == Constants.SERVICEID }
            if(service == null) {
                toast("Service not found!")
                gatt.close()
                return
            }

            // 找我们需要的特征值
            val characteristic = service.characteristics.firstOrNull { it.uuid == Constants.CHARID }
            if(characteristic == null) {
                toast("Characteristic not found!")
                gatt.close()
                return
            }

            gatt.setCharacteristicNotification(characteristic, true)

            // 找我们需要的描述符
            val descriptor = characteristic.descriptors.firstOrNull { it.uuid == Constants.DESCID }
            if(descriptor == null) {
                toast("Descriptor not found!")
                gatt.close()
                return
            }

            this@MainActivity.characteristic = characteristic
            this@MainActivity.descriptor = descriptor

            Log.i(TAG, "All ready!")
        }

        /**
         * 接收到客户端主动发送特征值变化通知的回调
         */
        override fun onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) {
            super.onCharacteristicChanged(gatt, characteristic)

            Log.d(TAG, "onCharacteristicChanged: ${bytesToString(characteristic.value)}")
        }

        /**
            * 读取到特征值的回调
            */
        override fun onCharacteristicRead(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, status: Int) {
            Log.d(TAG, "onCharacteristicRead: ${bytesToString(characteristic.value)}, status: $status")
        }

        /**
            * 写入到特征值的回调
            */
        override fun onCharacteristicWrite(
            gatt: BluetoothGatt?,
            characteristic: BluetoothGattCharacteristic?,
            status: Int
        ) {
            Log.d(TAG, "onCharacteristicWrite: $status")
        }

        /**
         * 写入到描述符的回调
         */
        override fun onDescriptorWrite(
            gatt: BluetoothGatt?,
            descriptor: BluetoothGattDescriptor?,
            status: Int
        ) {
            Log.d(TAG, "onDescriptorWrite: $status")
        }

        /**
         * 读取到描述符的回调
         */
        override fun onDescriptorRead(
            gatt: BluetoothGatt?,
            descriptor: BluetoothGattDescriptor,
            status: Int
        ) {
            Log.d(TAG, "onDescriptorRead: ${bytesToString(descriptor.value)} $status")

        }

    })
}

3.3 发现服务

上面callback中其实已经包含发现服务的代码了,这里提出来,啰嗦一下

// 发现服务
gatt.discoverServices()

/**
 * 发现服务步骤完成的回调
 */
override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) { 
    // 这里获取约定的服务、特征值和描述符
    ...
}

3.3 读写

/**
 * 读取特征值
 */
fun readChar(v: View) {
    val c = characteristic ?: return
    gatt?.readCharacteristic(c)
}

/**
 * 写特征值
 */
fun writeChar(v: View) {
    val c = characteristic ?: return
    val value = ByteArray(6)
    c.value = value
    random.nextBytes(value)
    gatt?.writeCharacteristic(c)
}

/**
 * 写入描述符
 */
fun writeDesc(v: View) {
    val d = descriptor ?: return
    val value = ByteArray(6)
    random.nextBytes(value)
    d.value = value
    gatt?.writeDescriptor(d)
}

/**
 * 读取描述符
 */
fun readDesc(v: View) {
    val d = descriptor ?: return
    gatt?.readDescriptor(d)
}

这里都是写入随机值,实际开发中根据业务需求写即可。

3.4 接收客户端通知

object : BluetoothGattCallback() {
    ...  

    /**
    * 接收到客户端主动发送特征值变化通知的回调
    */
    override fun onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) {
        // 根据业务需求进行实际操作
        ...
        Log.d(TAG, "onCharacteristicChanged: ${bytesToString(characteristic.value)}")
    }

    ...
}

4、示例代码

Github: https://github.com/sahooz/BledDemo