Android蓝牙(二) - BLE蓝牙

773 阅读4分钟

Android蓝牙(一) - 经典蓝牙Android蓝牙(一)—— 经典蓝牙,是一种无线技术标准,可实现设备间短距离数据 - 掘金 (juejin.cn)

Android蓝牙(二) - BLE蓝牙Android蓝牙(二) - BLE蓝牙——蓝牙得至少是低功耗蓝牙版本,然后安卓 - 掘金 (juejin.cn)

继传统蓝牙后,写一篇低功耗蓝牙(BLE)。

经典蓝牙: 蓝牙3.0版本以下的蓝牙。
低功耗蓝牙: 蓝牙4.0(及以上版本),如果使用智能手机作为测试平台,其 硬件 条件是,蓝牙得至少是低功耗蓝牙版本,然后安卓系统的话,至少得是 Android 4.3 以上系统才行,因为Google在 Android 4.3 以上才做了BLE主设备的支持,如果想将智能 手机 作为BLE从设备,则必须在 Android 5.0 以上才行。

debd96b0951a0e2042d2ded3238f31b8.png

  1. 权限申请
<!-- 如果明确不需要蓝牙推断位置的话,可以通过标记 usesPermissionFlags=“neverForLocation” --> 
<uses-permission android:name="android.permission.BLUETOOTH_SCAN"
  android:usesPermissionFlags="neverForLocation"
  tools:targetApi="s" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT"/>

<!-- Android 11 及以下版本 -->
<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" android:maxSdkVersion="30"/>

<!-- Android 9 及以下版本 -->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" android:maxSdkVersion="28"/>

2.本地适配器

BluetoothAdapter mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();

//第二种方式
BluetoothManager manager = (BluetoothManager) Context.getSystemService(Context.BLUETOOTH_SERVICE);
BluetoothAdapter mBluetoothAdapter = manager.getAdapter();

3.打开蓝牙

     * 蓝牙是否打开
     */
    private fun isOpenBluetooth(): Boolean {
        if (bluetoothAdapter!!.isEnabled) {
            return true
        } else {
            val enableBtIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
            startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT)
        }
        return false
    }

4.扫描蓝牙

    /**
     * 扫描回调
     */
    private val scanCallback =object : ScanCallback() {
        @SuppressLint("MissingPermission")
        override fun onScanResult(callbackType: Int, result: ScanResult?) {
            super.onScanResult(callbackType, result)
            result?.device?.let {
                Log.i(TAG, "扫描到的设备mac = ${it.address}" )
                if (!deviceList.contains(it)) {
                    deviceList.add(it)
                    adapter!!.notifyDataSetChanged()
                }
            }
        }

        override fun onScanFailed(errorCode: Int) {
            super.onScanFailed(errorCode)
            Log.i(TAG, "扫描失败 = $errorCode" )
        }

        override fun onBatchScanResults(results: MutableList<ScanResult>?) {
            super.onBatchScanResults(results)
            Log.i(TAG, "扫描失败 = ${results.toString()}" )
        }
    }


    /**
     * 开始扫描,会不断扫描,返回null
     */
    @SuppressLint("MissingPermission")
    private fun scanBluetooth() {
        /// 设置扫描策略
        val settingBuilder : ScanSettings.Builder =
            ScanSettings.Builder()
                .setScanMode(ScanSettings.SCAN_MODE_BALANCED)
                .setReportDelay(3000)
//        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
//            settingBuilder.setLegacy(true)
//                .setMatchMode(ScanSettings.MATCH_MODE_STICKY)
//                .setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES)
//                .setNumOfMatches(ScanSettings.MATCH_NUM_MAX_ADVERTISEMENT)
//        }
//        val scanSettings = settingBuilder.build()
//        // 设置过滤条件
//        val scanFilterList = arrayListOf<ScanFilter>()
//        val scanFilter = ScanFilter.Builder()
//            .setServiceUuid(ParcelUuid(UUID.fromString("73538150-66e0-4533-8d0d-c18ee70f39f8")))// 替换程自己的UUID
//            .build()
//        scanFilterList.add(scanFilter)
        bluetoothAdapter!!.bluetoothLeScanner.startScan(scanCallback)
    }

5.停止扫描

/**
 * 停止扫描
 */
@SuppressLint("MissingPermission")
private fun stopScan() {
    bluetoothAdapter!!.bluetoothLeScanner.stopScan(scanCallback)
}

6.连接蓝牙

/**
 * 连接回调
 */
private val gattCallback = object : BluetoothGattCallback() {
    @SuppressLint("MissingPermission")
    override fun onConnectionStateChange(gatt: BluetoothGatt?, status: Int, newState: Int) {
        super.onConnectionStateChange(gatt, status, newState)
        if (newState == BluetoothProfile.STATE_CONNECTED) {
            //已连接
            Log.i(TAG, "已连接")
            //发现服务
            bluetoothGatt?.discoverServices()
        } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
            //已断开连接
            Log.i(TAG, "已断开连接")
        }
    }

    @SuppressLint("MissingPermission")
    override fun onServicesDiscovered(gatt: BluetoothGatt?, status: Int) {
        // 调用 mBleGatt?.discoverServices() 时触发该回调
        if (status != BluetoothGatt.GATT_SUCCESS) {
            //失败
            return
        }
        //获取指定GATT服务,UUID 由远程设备提供
        val bleGattService = bluetoothGatt?.getService(UUID.fromString("8888888"))
        //获取指定GATT特征,UUID 由远程设备提供
        val bleGattCharacteristic = bleGattService?.getCharacteristic(UUID.fromString("777777"))
        //启用特征通知,如果远程设备修改了特征,则会触发 onCharacteristicChange() 回调
        bluetoothGatt?.setCharacteristicNotification(bleGattCharacteristic, true)
        //启用客户端特征配置【固定写法】
        val bleGattDescriptor =
            bleGattCharacteristic?.getDescriptor(UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"))
        bleGattDescriptor?.value = BluetoothGattDescriptor.ENABLE_INDICATION_VALUE
        bluetoothGatt?.writeDescriptor(bleGattDescriptor)
    }

    //启用客户端特征配置结果回调
    override fun onDescriptorWrite(gatt: BluetoothGatt?, descriptor: BluetoothGattDescriptor?, status: Int) {
        if (status == BluetoothGatt.GATT_SUCCESS ){
            //此时蓝牙设备连接才算真正连接成功,即具备读写数据的能力
            Log.i(TAG, "writeDescriptor 成功")
        }
    }

    //App修改特征回调,即 App 给设备发送数据结果回调
    override fun onCharacteristicWrite(gatt: BluetoothGatt?, characteristic: BluetoothGattCharacteristic?, status: Int
    ) {
        if (status == BluetoothGatt.GATT_SUCCESS){
            //数据写入成功
            Log.i(TAG, "writeCharacteristic 成功")
        }else{
            //数据写入失败
            Log.i(TAG, "writeCharacteristic 失败")
        }

}

    //远程设备修改特征描述回调,即设备给 App 发送数据
    override fun onCharacteristicChanged(gatt: BluetoothGatt?, characteristic: BluetoothGattCharacteristic?
    ) {
        //调用 characteristic?.value 获取远程设备发送过来的数据
    }
}

@SuppressLint("MissingPermission")
private fun connectGatt(device: BluetoothDevice) {
    device.connectGatt(mContext,false,gattCallback)
}

7.读写蓝牙

mBleGattCharacteristic?.value = data 
mBleGatt?.writeCharacteristic(mBleGattCharacteristic)

8.代码案例

package com.fei.action.bluetooth.ble

import android.Manifest
import android.annotation.SuppressLint
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothDevice
import android.bluetooth.BluetoothGatt
import android.bluetooth.BluetoothGattCallback
import android.bluetooth.BluetoothGattCharacteristic
import android.bluetooth.BluetoothGattDescriptor
import android.bluetooth.BluetoothManager
import android.bluetooth.BluetoothProfile
import android.bluetooth.le.ScanCallback
import android.bluetooth.le.ScanResult
import android.bluetooth.le.ScanSettings
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import android.util.Log
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.core.app.ActivityCompat
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.common.base.BaseActivity
import com.common.viewmodel.EmptyViewModel
import com.fei.firstproject.databinding.ActivitySimpleBlueToothBinding
import java.util.UUID

/**
 *    author : huangjf
 *    date   : 2024/9/5 11:26
 *    desc   :
 */
class BleBluetoothActivity:BaseActivity<EmptyViewModel,ActivitySimpleBlueToothBinding>() {

    private val REQUEST_ENABLE_BT = 0x11
    private var bluetoothManager: BluetoothManager? = null
    private var bluetoothAdapter: BluetoothAdapter? = null
    private var isBtEnable = false //是否可以开启蓝牙
    private val deviceList = ArrayList<BluetoothDevice>(10)
    private var adapter: SimpleBluetoothAdapter? = null
    private var bluetoothGatt: BluetoothGatt? = null

    companion object {
        const val TAG = "BleBluetoothActivity"
    }

    override fun createObserver() {
        mBinding.btnScan.setOnClickListener {
            if (isBtEnable) {
                scanBluetooth()
            }
        }
        mBinding.btnStopScan.setOnClickListener {
            if(isBtEnable) {
                stopScan()
            }
        }
    }

    override fun initViewAndData(savedInstanceState: Bundle?) {
        if (isSupportBluetooth() && isOpenBluetooth()) {
            isBtEnable = true
            initRecyclerView()
        }
    }

    /**
     * 蓝牙是否打开
     */
    private fun isOpenBluetooth(): Boolean {
        if (ActivityCompat.checkSelfPermission(
                this,
                Manifest.permission.BLUETOOTH_SCAN
            ) != PackageManager.PERMISSION_GRANTED ||
            ActivityCompat.checkSelfPermission(
                this,
                Manifest.permission.BLUETOOTH_CONNECT
            ) != PackageManager.PERMISSION_GRANTED
        ) {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                requestPermissions(
                    arrayOf(
                        Manifest.permission.BLUETOOTH_SCAN,
                        Manifest.permission.BLUETOOTH_CONNECT
                    ), 0x11
                )
            }
            return false
        }
        if (bluetoothAdapter!!.isEnabled) {
            return true
        } else {
            val enableBtIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
            startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT)
        }
        return false
    }


    /**
     * 是否支持蓝牙
     */
    private fun isSupportBluetooth(): Boolean {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            bluetoothManager = getSystemService(BluetoothManager::class.java)
            bluetoothAdapter = bluetoothManager!!.adapter ?: return false
        }

        return true
    }


    private fun initRecyclerView() {
        adapter = SimpleBluetoothAdapter()
        mBinding.recycler.layoutManager =
            LinearLayoutManager(mContext, RecyclerView.VERTICAL, false)
        mBinding.recycler.adapter = adapter
    }

    /**
     * 连接回调
     */
    private val gattCallback = object : BluetoothGattCallback() {
        @SuppressLint("MissingPermission")
        override fun onConnectionStateChange(gatt: BluetoothGatt?, status: Int, newState: Int) {
            super.onConnectionStateChange(gatt, status, newState)
            if (newState == BluetoothProfile.STATE_CONNECTED) {
                //已连接
                Log.i(TAG, "已连接")
                //发现服务
                bluetoothGatt?.discoverServices()
            } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
                //已断开连接
                Log.i(TAG, "已断开连接")
            }
        }

        @SuppressLint("MissingPermission")
        override fun onServicesDiscovered(gatt: BluetoothGatt?, status: Int) {
            // 调用 mBleGatt?.discoverServices() 时触发该回调
            if (status != BluetoothGatt.GATT_SUCCESS) {
                //失败
                return
            }
            //获取指定GATT服务,UUID 由远程设备提供
            val bleGattService = bluetoothGatt?.getService(UUID.fromString("8888888"))
            //获取指定GATT特征,UUID 由远程设备提供
            val bleGattCharacteristic = bleGattService?.getCharacteristic(UUID.fromString("777777"))
            //启用特征通知,如果远程设备修改了特征,则会触发 onCharacteristicChange() 回调
            bluetoothGatt?.setCharacteristicNotification(bleGattCharacteristic, true)
            //启用客户端特征配置【固定写法】
            val bleGattDescriptor =
                bleGattCharacteristic?.getDescriptor(UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"))
            bleGattDescriptor?.value = BluetoothGattDescriptor.ENABLE_INDICATION_VALUE
            bluetoothGatt?.writeDescriptor(bleGattDescriptor)
        }

        //启用客户端特征配置结果回调
        override fun onDescriptorWrite(gatt: BluetoothGatt?, descriptor: BluetoothGattDescriptor?, status: Int) {
            if (status == BluetoothGatt.GATT_SUCCESS ){
                //此时蓝牙设备连接才算真正连接成功,即具备读写数据的能力
                Log.i(TAG, "writeDescriptor 成功")
            }
        }

        //App修改特征回调,即 App 给设备发送数据结果回调
        override fun onCharacteristicWrite(gatt: BluetoothGatt?, characteristic: BluetoothGattCharacteristic?, status: Int
        ) {
            if (status == BluetoothGatt.GATT_SUCCESS){
                //数据写入成功
                Log.i(TAG, "writeCharacteristic 成功")
            }else{
                //数据写入失败
                Log.i(TAG, "writeCharacteristic 失败")
            }

    }

        //远程设备修改特征描述回调,即设备给 App 发送数据
        override fun onCharacteristicChanged(gatt: BluetoothGatt?, characteristic: BluetoothGattCharacteristic?
        ) {
            //调用 characteristic?.value 获取远程设备发送过来的数据
        }
    }

    /**
     * 扫描回调
     */
    private val scanCallback =object : ScanCallback() {
        @SuppressLint("MissingPermission")
        override fun onScanResult(callbackType: Int, result: ScanResult?) {
            super.onScanResult(callbackType, result)
            result?.device?.let {
                Log.i(TAG, "扫描到的设备mac = ${it.address}" )
                if (!deviceList.contains(it)) {
                    deviceList.add(it)
                    adapter!!.notifyDataSetChanged()
                }
            }
        }

        override fun onScanFailed(errorCode: Int) {
            super.onScanFailed(errorCode)
            Log.i(TAG, "扫描失败 = $errorCode" )
        }

        override fun onBatchScanResults(results: MutableList<ScanResult>?) {
            super.onBatchScanResults(results)
            Log.i(TAG, "扫描失败 = ${results.toString()}" )
        }
    }


    /**
     * 开始扫描,会不断扫描,返回null
     */
    @SuppressLint("MissingPermission")
    private fun scanBluetooth() {
        /// 设置扫描策略
        val settingBuilder : ScanSettings.Builder =
            ScanSettings.Builder()
                .setScanMode(ScanSettings.SCAN_MODE_BALANCED)
                .setReportDelay(3000)
//        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
//            settingBuilder.setLegacy(true)
//                .setMatchMode(ScanSettings.MATCH_MODE_STICKY)
//                .setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES)
//                .setNumOfMatches(ScanSettings.MATCH_NUM_MAX_ADVERTISEMENT)
//        }
//        val scanSettings = settingBuilder.build()
//        // 设置过滤条件
//        val scanFilterList = arrayListOf<ScanFilter>()
//        val scanFilter = ScanFilter.Builder()
//            .setServiceUuid(ParcelUuid(UUID.fromString("73538150-66e0-4533-8d0d-c18ee70f39f8")))// 替换程自己的UUID
//            .build()
//        scanFilterList.add(scanFilter)
        bluetoothAdapter!!.bluetoothLeScanner.startScan(scanCallback)
    }

    /**
     * 停止扫描
     */
    @SuppressLint("MissingPermission")
    private fun stopScan() {
        bluetoothAdapter!!.bluetoothLeScanner.stopScan(scanCallback)
    }

    private inner class SimpleBluetoothAdapter :
        RecyclerView.Adapter<SimpleBluetoothAdapter.ViewHolder>() {

        @SuppressLint("MissingPermission")
        inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {

            init {
                itemView.setOnClickListener {
                    val device = deviceList[adapterPosition]
                    connectGatt(device)
                }
            }
        }

        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
            return ViewHolder(
                View.inflate(
                    parent.context,
                    android.R.layout.simple_list_item_1,
                    null
                )
            )
        }

        override fun getItemCount(): Int {
            return deviceList.size
        }

        @SuppressLint("MissingPermission")
        override fun onBindViewHolder(holder: ViewHolder, position: Int) {
            (holder.itemView as TextView).text =
                "名字 = " + deviceList[position].name + "地址 = " + deviceList[position].address + "类型 =  " + deviceList[position].type

        }
    }

    @SuppressLint("MissingPermission")
    private fun connectGatt(device: BluetoothDevice) {
        device.connectGatt(mContext,false,gattCallback)
    }

    @SuppressLint("MissingPermission")
    private fun disconnectGatt() {
        /**
         * 断开连接
         */
        bluetoothGatt?.disconnect()
        bluetoothGatt?.close()

    }

    override fun onDestroy() {
        super.onDestroy()
        stopScan()
        disconnectGatt()
    }
}

9.注意事项

  • 连接过程会有很多中间过程(触发连接 -> 连接回调成功后 -> 发现服务 -> …),当获取为 null 或者返回失败时,要做异常返回,防止进度卡死。
  • 同上,连接中间过程较多,防止远端设备偶现无响应,在连接过程中设置超时机制,超时判定连接失败。
  • 当存在多个 GATT 特征时,可能需要调用多次 setCharacteristicNotification() + writeDescriptor(),注意此操作不能连续调用,正确姿势:gatt1 调用完成,待 onDescriptorWrite() 回调后,gatt2 再调用。
  • 因为远端设备只能处理单条指令,所以需要维护一个优先级队列
  • 蓝牙传输有最大传输单元限制(MTU),默认最大 23 个字节,可用的只有 20 个字节,[ 23 byte(ATT) =1 byte(Opcode) + 2 byte(Handler) + 20 byte(BATT) ],所以在发送指令时要做分包处理。
  • MTU 可通过调用 requestMtu() 调整大小,具体调整多大需和远端设备协定,调用后会回调 gattCallback#onMtuChanged(),注意:发现服务的调用要在该回调中,不能在连接状态回调中。
  • 单一指令发送和回包,需要加超时机制。即调用发送指令时开始超时倒计时,当触发 onCharacteristicChanged() 时并判断为指令回包,则移除倒计时。如果 onCharacteristicWrite() 返回失败或超时未回包,则移除倒计时并返回失败。
  • 单一指令发送并伴随多条回包,需要加 watchDog 机制。即调用发送指令时开始“养狗”,当有远端设备回包时“喂狗”,回包全部完成时“杀狗”,如果 onCharacteristicWrite() 返回失败或到时间没有“喂狗”,则“杀狗”并返回失败。

10.用户体验

  • Android 12 以下版本蓝牙扫描需要开启定位+授权才能使用,所以在扫描前要申请蓝牙&定位权限+判断是否开启蓝牙&定位。
  • 使用过程中,用户可能误操作关闭蓝牙,所以要监听蓝牙开关状态。
  • 蓝牙扫描添加超时机制,超时自动停止扫描。
  • 如果用列表按照信号强度展示扫描结果,建议扫描结束后再让用户选择设备,防止列表频繁跳动,导致用户误选。
  • 关于蓝牙的UI界面或操作,都需要判断当前蓝牙是否已连接。

qrcode_for_gh_cde562595b7f_258.jpg

文章上如有错误,欢迎留言,谢谢大家支持