文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

Android BlueToothBLE入门(二)——设备的连接和通讯(附Demo源码地址)

2023-09-17 10:34

关注

学更好的别人,

做更好的自己。

——《微卡智享》

f98a7c1995354157b92adeec3f3f0d95.jpeg

本文长度为7870,预计阅读12分钟

前言

接《Android BlueToothBLE入门(一)——低功耗蓝牙介绍》上篇,这篇文章主要就是来做Demo实现Android两台设备的数据通讯。

265be150c55a258e1f6706ac4b187866.png

实现效果

a9419e281e36da4547eac0c7a83bfeab.jpeg

Android BLE Demo简介

256e157d1778e648caefe10bac58b2a3.png

微卡智享

01

目录及使用的组件

5b702abca0dd6934c392134519936e8c.png

整个Demo的目录上图中已经做了说明,其中最核心的是BlueToothBLEUtil类,这是把这个Demo中用到的BLE蓝牙方法都放到这里了,因为中心设备(Client)和外围设备(Server)统一用的这个程序,所以这个类里面中心设备和外围设备用到的都做了一个封装,当时还有不少要加的,后面会再补充。

Demo使用的MVI架构(Jeppack Compose还不会,所以用的viewBinding),像RecyclerView的适配器这块还是使用的BaseQuickAdapter,现在4.0在测试过程中了,所以我直接用的4.0beta版,蓝牙权限的申请采用了easypermissions,确实比自己写方便了许多。

build.gradle相关依赖项

09dcaad06b31cb7f24cc55c73f24c631.png

dependencies {    'androidx.lifecycle:lifecycle-livedata-ktx:2.4.1'    'androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.1'    'androidx.lifecycle:lifecycle-runtime-ktx:2.4.1'    //使用协程    "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4"    "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4"    "io.github.cymchad:BaseRecyclerViewAdapterHelper:4.0.0-beta04"    // 使用 Android X 的应用添加该依赖    'pub.devrel:easypermissions:3.0.0'}

02

蓝牙核心类BlueToothBLEUtil

外围设备和中心设备通讯,我们就用自己定义的服务即可,所以类中我们已经定义好常量来实现。

上一篇介绍过蓝牙技术联盟SIG定义UUID共用了一个基本的UUID:0x0000xxxx-0000-1000-8000-00805F9B34FB。总共128位,为了进一步简化基本UUID,每一个蓝牙技术联盟定义的属性有一个唯一的16位UUID,以代替上面的基本UUID的‘x’部分。使用16位的UUID便于记忆和操作。

所以类中我们定义的服务UUID只是中间xxxx四位即可,写了一个函数来直接生成对应的UUID

5bbfc6913293fc3e1a4747e010ab39b5.png

1489f97e1ade6e92d179a7221d9734e5.png

代码中使用BLE蓝牙相关Api时,Android Studio会经常提示要先判断是否有蓝牙权限,所以这里也是把蓝牙是否做过初始化,和判断是否有相关的蓝牙权限写了一个函数调用

1d1ee7d358d67e675fcc1edbe7d1e755.png

蓝牙权限

9befaca7fc6d9872dbdac39d75a078d2.png

检测是否有相关权限

231d91049d1188143156cd4a89b930cc.png

调用蓝牙API时先检测是否有对应的权限

像扫描设备,连接设备时需要知道返回的结果,用到了回调,那类中直接就是传入相磁的CallBack回调函数,在UI界面写回调函数即可。如下面这个扫描蓝牙设备函数

f61a3c4d0586d22469834111bb540efb.png

参数为ScanCallback

3c54c36d841f4df2168248b6466b4924.png

ScanFragment中定义ScanCallback,实现onScanResult中发送意图

5c532a8aa4f6ef4c26470a42a0757f13.png

点击扫描设备直接调用类中函数并传入回调函数

BlueToothBLEUtil源码

package vac.test.bluetoothbledemo.repositoryimport android.Manifestimport android.app.Applicationimport android.bluetooth.BluetoothAdapterimport android.bluetooth.BluetoothDeviceimport android.bluetooth.BluetoothGattimport android.bluetooth.BluetoothGattCallbackimport android.bluetooth.BluetoothGattCharacteristicimport android.bluetooth.BluetoothGattDescriptorimport android.bluetooth.BluetoothGattServerimport android.bluetooth.BluetoothGattServerCallbackimport android.bluetooth.BluetoothGattServiceimport android.bluetooth.BluetoothManagerimport android.bluetooth.le.AdvertiseCallbackimport android.bluetooth.le.AdvertiseDataimport android.bluetooth.le.AdvertiseSettingsimport android.bluetooth.le.BluetoothLeAdvertiserimport android.bluetooth.le.ScanCallbackimport android.bluetooth.le.ScanFilterimport android.bluetooth.le.ScanSettingsimport android.content.Contextimport android.content.pm.PackageManagerimport android.os.Buildimport android.os.ParcelUuidimport android.util.Logimport android.widget.Toastimport androidx.core.app.ActivityCompatimport kotlinx.coroutines.CoroutineScopeimport kotlinx.coroutines.Dispatchersimport kotlinx.coroutines.delayimport kotlinx.coroutines.launchimport pub.devrel.easypermissions.AfterPermissionGrantedimport pub.devrel.easypermissions.EasyPermissionsimport vac.test.bluetoothbledemo.BaseAppimport vac.test.bluetoothbledemo.EncodeUtilimport vac.test.bluetoothbledemo.bytesToHexStringimport vac.test.bluetoothbledemo.ui.MainActivityimport java.io.IOExceptionimport java.util.UUIDobject BlueToothBLEUtil {    //服务 UUID    const val BLESERVER = "2603"    //特征 UUID    const val BLECHARACTERISTIC = "ca01"    //描述 UUID    const val BLEDESCRIPTOR = "da01"    //蓝牙相关权限    const val REQUEST_CODE_PERMISSIONS = 10    val REQUIRED_BLEPERMISSIONS = arrayOf(        Manifest.permission.BLUETOOTH, Manifest.permission.BLUETOOTH_ADMIN,        Manifest.permission.BLUETOOTH_CONNECT, Manifest.permission.BLUETOOTH_SCAN,        Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.BLUETOOTH_ADVERTISE    )    private var mBluetoothManager: BluetoothManager? = null    private var mBluetoothAdapter: BluetoothAdapter? = null    private var mBluetoothGattService: BluetoothGattService? = null    private var mBluetoothGattServer: BluetoothGattServer? = null    private var mBluetoothDevice: BluetoothDevice? = null    private var mBluetoothGatt: BluetoothGatt? = null    //BLE广播操作类    private var mBluetoothLeAdvertiser: BluetoothLeAdvertiser? = null    //是否初始化    var hasInit = false    lateinit var mApplication: Application    //检测蓝牙权限    fun checkBlueToothPermission(permissions: String = ""): Boolean {        if (!hasInit) throw IOException("未初始化蓝牙BlueTooth!")        if (permissions == "") return true        return ActivityCompat.checkSelfPermission(            mApplication.applicationContext,            permissions        ) == PackageManager.PERMISSION_GRANTED    }    fun init(application: Application): Boolean {        if (hasInit) return true        mApplication = application        //初始化ble设配器        mBluetoothManager =            mApplication.applicationContext.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager        //初始化适配器        mBluetoothAdapter = mBluetoothManager!!.adapter        hasInit = true        return hasInit    }    fun destory() {        mBluetoothGatt = null        mBluetoothDevice = null        mBluetoothGattService = null        mBluetoothAdapter = null        hasInit = false    }    //获取UUID        fun getUUID(baseuuid: String): UUID {        return UUID.fromString("0000${baseuuid}-0000-1000-8000-00805f9b34fb")    }    //广播时间(设置为0则持续广播)    val Time = 0    //是否在扫描中    private var mScanning: Boolean = false    //获取BluetoothManager    fun getBluetoothManager(): BluetoothManager? {        return if (checkBlueToothPermission()) {            mBluetoothManager        } else {            null        }    }    //获取BluetoothAdapter    fun getBluetoothAdapter(): BluetoothAdapter? {        return if (checkBlueToothPermission()) {            mBluetoothAdapter        } else {            null        }    }    //region 服务端外围设备相关函数        //获取Gatt服务    fun getGattService(): BluetoothGattService {        //初始化Service        //创建服务,并初始化服务的UUID和服务类型。        //BluetoothGattService.SERVICE_TYPE_PRIMARY 为主要服务类型        val mGattService = BluetoothGattService(            getUUID(BLESERVER),            BluetoothGattService.SERVICE_TYPE_PRIMARY        )        //初始化特征(添加读写权限)        //在服务端配置特征时,设置BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE,        //那么onCharacteristicWriteRequest()回调时,不需要GattServer进行response才能进行响应。        val mGattCharacteristic = BluetoothGattCharacteristic(            getUUID(BLECHARACTERISTIC),            BluetoothGattCharacteristic.PROPERTY_WRITE or                    BluetoothGattCharacteristic.PROPERTY_NOTIFY or                    BluetoothGattCharacteristic.PROPERTY_READ,            (BluetoothGattCharacteristic.PERMISSION_WRITE or BluetoothGattCharacteristic.PERMISSION_READ)        )        //初始化描述        val mGattDescriptor = BluetoothGattDescriptor(            getUUID(BLEDESCRIPTOR),            BluetoothGattDescriptor.PERMISSION_READ or BluetoothGattDescriptor.PERMISSION_WRITE        )        //Service添加特征值        mGattService.addCharacteristic(mGattCharacteristic)        //特征值添加描述        mGattCharacteristic.addDescriptor(mGattDescriptor)        return mGattService    }    //添加服务    fun addGattServer(mGattServerCallback: BluetoothGattServerCallback) {        if (checkBlueToothPermission(Manifest.permission.BLUETOOTH_CONNECT)) {            mBluetoothGattService = getGattService()            mBluetoothGattServer =                mBluetoothManager!!.openGattServer(                    mApplication.applicationContext, mGattServerCallback                )            mBluetoothGattServer!!.addService(mBluetoothGattService)        }    }    //开启广播    //官网建议获取mBluetoothLeAdvertiser时,先做mBluetoothAdapter.isMultipleAdvertisementSupported判断,    // 但部分华为手机支持Ble广播却还是返回false,所以最后以mBluetoothLeAdvertiser是否不为空且蓝牙打开为准    fun startAdvertising(phonename: String, mAdvertiseCallback: AdvertiseCallback): Boolean {        if (checkBlueToothPermission(Manifest.permission.BLUETOOTH_CONNECT)) {            mBluetoothAdapter!!.name = phonename            mBluetoothLeAdvertiser = mBluetoothAdapter!!.bluetoothLeAdvertiser            //蓝牙关闭或者不支持            return if (mBluetoothLeAdvertiser != null && mBluetoothAdapter!!.isEnabled) {                Log.d(                    "pkg", "mBluetoothLeAdvertiser != null = ${mBluetoothLeAdvertiser != null} " +"mBluetoothAdapter.isMultipleAdvertisementSupported = ${mBluetoothAdapter!!.isMultipleAdvertisementSupported}"                )                //开始广播(不附带扫描响应报文)                mBluetoothLeAdvertiser?.startAdvertising(                    getAdvertiseSettings(),                    getAdvertiseData(), mAdvertiseCallback                )                true            } else {                false            }        } else {            return false        }    }    //关闭蓝牙广播    fun stopAdvertising(mAdvertiseCallback: AdvertiseCallback): Boolean {        if (checkBlueToothPermission(Manifest.permission.BLUETOOTH_ADVERTISE)) {            mBluetoothLeAdvertiser?.let { advertiser ->                advertiser.stopAdvertising(mAdvertiseCallback)            }            return true        } else {            return false        }    }    //endregion    fun scanBlueToothDevice(scancallback: ScanCallback) {        if (mScanning) return        if (checkBlueToothPermission(Manifest.permission.BLUETOOTH_SCAN)) {            //扫描设置                        val builder = ScanSettings.Builder()                .setScanMode(ScanSettings.SCAN_MODE_LOW_POWER)                        builder.setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES)            //判断手机蓝牙芯片是否支持皮批处理扫描            if (mBluetoothAdapter!!.isOffloadedFilteringSupported) {                builder.setReportDelay(0L)            }            mScanning = true            //3秒后关闭            CoroutineScope(Dispatchers.IO).launch {                delay(6000)                stopScanBlueToothDevice(scancallback)                Log.i("bluetooth", "关闭扫描")            }            //过滤掉不是自己程序发送的广播            val filter = getScanFilter()            mBluetoothAdapter!!.bluetoothLeScanner?.startScan(filter, builder.build(), scancallback)            //过滤特定的 UUID 设备            //bluetoothAdapter?.bluetoothLeScanner?.startScan()        }    }    fun stopScanBlueToothDevice(scancallback: ScanCallback) {        //连接时要先关闭扫描        if (mScanning) {            if (checkBlueToothPermission(Manifest.permission.BLUETOOTH_SCAN)) {                mBluetoothAdapter!!.bluetoothLeScanner?.stopScan(scancallback)                mScanning = false            }        }    }    //初始化广播设置    fun getAdvertiseSettings(): AdvertiseSettings {        //初始化广播设置        return AdvertiseSettings.Builder()            //设置广播模式,以控制广播的功率和延迟。ADVERTISE_MODE_LOW_LATENCY为高功率,低延迟            .setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_LOW_POWER)            //设置蓝牙广播发射功率级别            .setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_LOW)            //广播时限。最多180000毫秒。值为0将禁用时间限制。(不设置则为无限广播时长)            .setTimeout(Time)            //设置广告类型是可连接还是不可连接。            .setConnectable(true)            .build()    }    //设置广播报文    fun getAdvertiseData(): AdvertiseData {        return AdvertiseData.Builder()            //设置广播包中是否包含设备名称。            .setIncludeDeviceName(true)            //设置广播包中是否包含发射功率            .setIncludeTxPowerLevel(true)            //设置UUID            .addServiceUuid(ParcelUuid(getUUID(BLESERVER)))            .build()    }    //设置扫描过滤    fun getScanFilter(): ArrayList {        val scanFilterList = ArrayList()        val builder = ScanFilter.Builder()        builder.setServiceUuid(ParcelUuid(getUUID(BLESERVER)))        scanFilterList.add(builder.build())        return scanFilterList    }    //获取原生蓝牙对象    fun getBlueToothDevice(macAddress: String): BluetoothDevice? {        return if (checkBlueToothPermission()) {            mBluetoothDevice = mBluetoothAdapter!!.getRemoteDevice(macAddress)            if (mBluetoothDevice == null) throw IOException("获取不到BluetoothDevice")            mBluetoothDevice!!        } else {            null        }    }    //申请通讯字节长度    fun requestMTP(size: Int = 512): Boolean {        return if (checkBlueToothPermission()) {            mBluetoothGatt?.let {                it.requestMtu(size)            } ?: false        } else {            false        }    }    //连接蓝牙Gatt    fun connect(macAddress: String, callback: BluetoothGattCallback): BluetoothGatt? {        return if (checkBlueToothPermission(Manifest.permission.BLUETOOTH_CONNECT)) {            if (mBluetoothDevice == null)                getBlueToothDevice(macAddress)            mBluetoothGatt =                mBluetoothDevice!!.connectGatt(mApplication.applicationContext, false, callback)            mBluetoothGatt        } else {            null        }    }    //断开蓝牙Gatt    fun disConnect() {        if (checkBlueToothPermission(Manifest.permission.BLUETOOTH_CONNECT)) {            mBluetoothGatt?.let {                it.disconnect()                //调用close()后,连接时传入callback会被置空,无法得到断开连接时onConnectionStateChange()回调                it.close()            }        }    }    //获取蓝牙GattService    fun getBlueToothGattService(gatt: BluetoothGatt): List {        return gatt.services    }    //发送Characteristic    fun writeCharacteristic(        srvuuid: String,        charuuid: String,        byteArray: ByteArray    ) {        if (checkBlueToothPermission(Manifest.permission.BLUETOOTH_CONNECT)) {            mBluetoothGatt?.let {                val characteristic =                    it.getService(getUUID(srvuuid)).getCharacteristic(getUUID(charuuid))                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {                    it.writeCharacteristic(                        characteristic,                        byteArray,                        BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE                    )                } else {                    characteristic.setValue(byteArray)                    it.writeCharacteristic(characteristic)                }            } ?: {                throw IOException("mBluetoothGatt为空")            }        }    }    //发送Characteristic    fun writeCharacteristic(        characteristic: BluetoothGattCharacteristic,        byteArray: ByteArray    ) {        if (checkBlueToothPermission(Manifest.permission.BLUETOOTH_CONNECT)) {//        var hexstr = byteArrsyToHexString(byteArray)//        var transbytes = hexstr!!.toByteArray()            mBluetoothGatt?.let {                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {                    it.writeCharacteristic(                        characteristic,                        byteArray,                        BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE                    )                } else {                    characteristic.setValue(byteArray)                    it.writeCharacteristic(characteristic)                }            } ?: {                throw IOException("mBluetoothGatt为空")            }        }    }    fun readCharacteristic(characteristic: BluetoothGattCharacteristic): ByteArray? {        return if (checkBlueToothPermission(Manifest.permission.BLUETOOTH_CONNECT)) {            var byteArray: ByteArray? = null            mBluetoothGatt?.let {                it.readCharacteristic(characteristic)                byteArray = characteristic.value            }            byteArray        } else {            null        }    }    //发送返回值sendResponse    fun sendResponse(        device: BluetoothDevice, requestId: Int, offset: Int, value: ByteArray    ) {        if (checkBlueToothPermission(Manifest.permission.BLUETOOTH_CONNECT)) {            mBluetoothGattServer!!.sendResponse(                device, requestId, BluetoothGatt.GATT_SUCCESS,                offset, value            )        }    }    fun setCharacteristicNotify(characteristic: BluetoothGattCharacteristic, bool: Boolean) {        if (checkBlueToothPermission(Manifest.permission.BLUETOOTH_CONNECT)) {            mBluetoothGatt?.let {                it.setCharacteristicNotification(characteristic, bool)            }        }    }    fun notifyCharacteristicChanged(        device: BluetoothDevice,        characteristic: BluetoothGattCharacteristic,        byteArray: ByteArray    ) {        if (checkBlueToothPermission(Manifest.permission.BLUETOOTH_CONNECT)) {            //回复客户端,让客户端读取该特征新赋予的值,获取由服务端发送的数据            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {                mBluetoothGattServer!!.notifyCharacteristicChanged(                    device,                    characteristic,                    false,                    byteArray                )            } else {                characteristic.value = byteArray                mBluetoothGattServer!!.notifyCharacteristicChanged(device, characteristic, false)            }        }    }}

03

适配器BaseQuickAdapter

0版本的BaseQuickAdapter,里面的ViewHolder要自己定义,用法和原来有点不太一样

6ccfbcf7337f99f3db16804e18e242f6.png

41bc85769100e1b49234d69a87e2caa2.png

还有原来我用BaseQuickAdapter中直接用的二级列表,当时也是会有问题,具体问题可以看《Android BaseQuickAdapter3.0.4版本二级列表的使用及遇到的问题》,正好这次服务的列表刷新中又需要实现二级列表,现在我是改为自定义添加了,同样绑定了viewBinding。

40fdcf34d78bbe061f6cd00cb4f5b79e.png

68ca2469271136e75b01b846e913a722.png

04

Fragment中使用ViewBinding注意事项

在Fragment中使用viewBinding,为了防止内存泄漏,Google有标准的写法,不过每个Fragment都这样写比较麻烦,所以这里定义了一个BaseFragment,用于处理viewBinding内存泄露问题。

abstract class BaseFragment : Fragment() {    private var _binding: T? = null    protected val binding: T get() = _binding!!    abstract val bindingInflater: (LayoutInflater, ViewGroup?, Bundle?) -> T    override fun onCreateView(        inflater: LayoutInflater,        container: ViewGroup?,        savedInstanceState: Bundle?    ): View {        _binding = bindingInflater.invoke(inflater, container, savedInstanceState)        return binding.root    }    override fun onDestroyView() {        super.onDestroyView()        _binding = null    }}

aaff3b28a61a7b999318d1a01ec83946.png

这样一个基本的蓝牙Ble通讯就完成了。

后续问题

上面的视频中通讯传输是没问题,但是如果发送大点的数据,就不行了,蓝牙BLE发送数据默认单次最大传输20个byte,如果是一般的协议命令,如:开关灯、前进左右等等,是没有问题的,如果是需要发送如:图片、BIN文档、音乐等大数据量的文件,则需要做数据的处理。

基本说考虑到蓝牙发送大数据量时应该通过两个途径结合实现:

  1. 申请修改MTU值,MTU: 最大传输单元(MAXIMUM TRANSMISSION UNIT)

  2. 分包数据发送

简单的通讯Demo实现后,接下来就准备开始研究分包通讯的问题了。

源码地址

https://github.com/Vaccae/AndroidBLEDemo.git

点击原文链接可以看到“码云”的源码地址

b555b12c37de5bcf8a19d835eb445af1.png

f17be96a3f171ddee3a8d5048af22020.png

往期精彩回顾

 

51110eba9a28ec644b8f640c1c437822.jpeg

Android BlueToothBLE入门(一)——低功耗蓝牙介绍

 

 

9132f2df2517286d7a36a8a289f0a1c6.jpeg

Android监听消息(二)——电话及短信监听

 

 

557e546de405bbff90b2fe4225d807f9.jpeg

Android监听消息(一)——应用消息捕获

 

来源地址:https://blog.csdn.net/Vaccae/article/details/131179791

阅读原文内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-移动开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯