学更好的别人,
做更好的自己。
——《微卡智享》
本文长度为7870字,预计阅读12分钟
前言
接《Android BlueToothBLE入门(一)——低功耗蓝牙介绍》上篇,这篇文章主要就是来做Demo实现Android两台设备的数据通讯。
实现效果
Android BLE Demo简介
微卡智享
01
目录及使用的组件
整个Demo的目录上图中已经做了说明,其中最核心的是BlueToothBLEUtil类,这是把这个Demo中用到的BLE蓝牙方法都放到这里了,因为中心设备(Client)和外围设备(Server)统一用的这个程序,所以这个类里面中心设备和外围设备用到的都做了一个封装,当时还有不少要加的,后面会再补充。
Demo使用的MVI架构(Jeppack Compose还不会,所以用的viewBinding),像RecyclerView的适配器这块还是使用的BaseQuickAdapter,现在4.0在测试过程中了,所以我直接用的4.0beta版,蓝牙权限的申请采用了easypermissions,确实比自己写方便了许多。
build.gradle相关依赖项
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
代码中使用BLE蓝牙相关Api时,Android Studio会经常提示要先判断是否有蓝牙权限,所以这里也是把蓝牙是否做过初始化,和判断是否有相关的蓝牙权限写了一个函数调用
蓝牙权限
检测是否有相关权限
调用蓝牙API时先检测是否有对应的权限
像扫描设备,连接设备时需要知道返回的结果,用到了回调,那类中直接就是传入相磁的CallBack回调函数,在UI界面写回调函数即可。如下面这个扫描蓝牙设备函数
参数为ScanCallback
ScanFragment中定义ScanCallback,实现onScanResult中发送意图
点击扫描设备直接调用类中函数并传入回调函数
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要自己定义,用法和原来有点不太一样
还有原来我用BaseQuickAdapter中直接用的二级列表,当时也是会有问题,具体问题可以看《Android BaseQuickAdapter3.0.4版本二级列表的使用及遇到的问题》,正好这次服务的列表刷新中又需要实现二级列表,现在我是改为自定义添加了,同样绑定了viewBinding。
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 }}
这样一个基本的蓝牙Ble通讯就完成了。
后续问题
上面的视频中通讯传输是没问题,但是如果发送大点的数据,就不行了,蓝牙BLE发送数据默认单次最大传输20个byte,如果是一般的协议命令,如:开关灯、前进左右等等,是没有问题的,如果是需要发送如:图片、BIN文档、音乐等大数据量的文件,则需要做数据的处理。
基本说考虑到蓝牙发送大数据量时应该通过两个途径结合实现:
-
申请修改MTU值,MTU: 最大传输单元(MAXIMUM TRANSMISSION UNIT)
-
分包数据发送
简单的通讯Demo实现后,接下来就准备开始研究分包通讯的问题了。
源码地址
https://github.com/Vaccae/AndroidBLEDemo.git
点击原文链接可以看到“码云”的源码地址
完
往期精彩回顾
Android BlueToothBLE入门(一)——低功耗蓝牙介绍