赞
踩
学更好的别人,
做更好的自己。
——《微卡智享》
本文长度为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.repository
-
-
- import android.Manifest
- import android.app.Application
- 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.BluetoothGattServer
- import android.bluetooth.BluetoothGattServerCallback
- import android.bluetooth.BluetoothGattService
- import android.bluetooth.BluetoothManager
- import android.bluetooth.le.AdvertiseCallback
- import android.bluetooth.le.AdvertiseData
- import android.bluetooth.le.AdvertiseSettings
- import android.bluetooth.le.BluetoothLeAdvertiser
- import android.bluetooth.le.ScanCallback
- import android.bluetooth.le.ScanFilter
- import android.bluetooth.le.ScanSettings
- import android.content.Context
- import android.content.pm.PackageManager
- import android.os.Build
- import android.os.ParcelUuid
- import android.util.Log
- import android.widget.Toast
- import androidx.core.app.ActivityCompat
- import kotlinx.coroutines.CoroutineScope
- import kotlinx.coroutines.Dispatchers
- import kotlinx.coroutines.delay
- import kotlinx.coroutines.launch
- import pub.devrel.easypermissions.AfterPermissionGranted
- import pub.devrel.easypermissions.EasyPermissions
- import vac.test.bluetoothbledemo.BaseApp
- import vac.test.bluetoothbledemo.EncodeUtil
- import vac.test.bluetoothbledemo.bytesToHexString
- import vac.test.bluetoothbledemo.ui.MainActivity
- import java.io.IOException
- import java.util.UUID
-
-
- object 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
- /*
- 蓝牙技术联盟SIG定义UUID共用了一个基本的UUID:0x0000xxxx-0000-1000-8000-00805F9B34FB。
- 总共128位,为了进一步简化基本UUID,每一个蓝牙技术联盟定义的属性有一个唯一的16位UUID,
- 以代替上面的基本UUID的‘x’部分。使用16位的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 服务和特征
- * 广播是广播,只有添加Gatt服务和特征后,连接才有服务和特征用于数据交换
- */
- //获取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)) {
- //扫描设置
- /**
- * 三种模式
- * - SCAN_MODE_LOW_POWER : 低功耗模式,默认此模式,如果应用不在前台,则强制此模式
- * - SCAN_MODE_BALANCED :平衡模式,一定频率下返回结果
- * - SCAN_MODE_LOW_LATENCY 高功耗模式,建议应用在前台才使用此模式
- */
- val builder = ScanSettings.Builder()
- .setScanMode(ScanSettings.SCAN_MODE_LOW_POWER)
-
-
- /**
- * 三种回调模式
- * - CALLBACK_TYPE_ALL_MATCHED : 寻找符合过滤条件的广播,如果没有,则返回全部广播
- * - CALLBACK_TYPE_FIRST_MATCH : 仅筛选匹配第一个广播包出发结果回调的
- * - CALLBACK_TYPE_MATCH_LOST : 这个看英文文档吧,不满足第一个条件的时候,不好解释
- */
- 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<ScanFilter> {
- val scanFilterList = ArrayList<ScanFilter>()
- 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<BluetoothGattService> {
- 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
4.0版本的BaseQuickAdapter,里面的ViewHolder要自己定义,用法和原来有点不太一样
还有原来我用BaseQuickAdapter中直接用的二级列表,当时也是会有问题,具体问题可以看《Android BaseQuickAdapter3.0.4版本二级列表的使用及遇到的问题》,正好这次服务的列表刷新中又需要实现二级列表,现在我是改为自定义添加了,同样绑定了viewBinding。
04
Fragment中使用ViewBinding注意事项
在Fragment中使用viewBinding,为了防止内存泄漏,Google有标准的写法,不过每个Fragment都这样写比较麻烦,所以这里定义了一个BaseFragment,用于处理viewBinding内存泄露问题。
- abstract class BaseFragment<T : ViewBinding> : 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入门(一)——低功耗蓝牙介绍
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。