Android蓝牙低功耗(BLE)快速开发框架

GIthub传送带点这里

  • minSdk 24
  • targetSdk 33

基于Kotlin、协程

基于sdk 33,最新API

详细的完整的容错机制

基于多个蓝牙库的设计思想

强大的Notify\Indicate\Read\Write任务队列

20230613110126.png
20230613110146.png
20230614090104.png

demo体验

demo下载

详细用法参考demo

详细用法参考demo

详细用法参考demo

用法

    allprojects {
        repositories {
            ...
            maven { url 'https://jitpack.io' }
        }
    }

    dependencies {
        implementation 'com.github.buhuiming:BleCore:latest version'
    }

1、添加权限

//动态申请
val LOCATION_PERMISSION = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
        arrayOf(
            Manifest.permission.ACCESS_FINE_LOCATION,
            Manifest.permission.ACCESS_COARSE_LOCATION,
        )
    } else {
        arrayOf(
            Manifest.permission.ACCESS_FINE_LOCATION,
            Manifest.permission.ACCESS_COARSE_LOCATION,
            Manifest.permission.BLUETOOTH_SCAN,
            Manifest.permission.BLUETOOTH_ADVERTISE,
            Manifest.permission.BLUETOOTH_CONNECT,
        )
    }
  • 注意:
  • 有些设备GPS是关闭状态的话,申请定位权限之后,GPS是依然关闭状态,这里要根据GPS是否打开来跳转页面
  • BleUtil.isGpsOpen(context) 判断GPS是否打开
  • 跳转到系统GPS设置页面,GPS设置是全局的独立的,是否打开跟权限申请无关
    startActivity(Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS))
  • 跳转到系统蓝牙设置页面
    startActivity(Intent(Settings.ACTION_BLUETOOTH_SETTINGS))

1、初始化

val options =
        BleOptions.builder()
            .setScanServiceUuid("0000ff80-0000-1000-8000-00805f9b34fb", "0000ff90-0000-1000-8000-00805f9b34fb")
            .setScanDeviceName("midea", "BYD BLE3")
            .setScanDeviceAddress("70:86:CE:88:7A:AF", "5B:AE:65:88:59:5E", "B8:8C:29:8B:BE:07")
            .isContainScanDeviceName(true)
            .setAutoConnect(false)
            .setEnableLog(true)
            .setScanMillisTimeOut(12000)
            //这个机制是:不会因为扫描的次数导致上一次扫描到的数据被清空,也就是onScanStart和onScanComplete
            //都只会回调一次,而且扫描到的数据是所有扫描次数的总和
            .setScanRetryCountAndInterval(2, 1000)
            .setConnectMillisTimeOut(10000)
            .setConnectRetryCountAndInterval(2, 5000)
            .setOperateMillisTimeOut(6000)
            .setOperateInterval(80)
            .setMaxConnectNum(5)
            .setMtu(500)
            .setTaskQueueType(BleTaskQueueType.Operate)
            .build()
BleManager.get().init(application, options)

//或者使用默认配置
BleManager.get().init(application)

setTaskQueueType方法,有3个选项分别是:

  • BleTaskQueueType.Default 一个设备的Notify\Indicate\Read\Write\mtu操作所对应的任务共享同一个任务 队列(共享队列)(不区分特征值),rssi在rssi队列
  • BleTaskQueueType.Operate 一个设备每个操作独立一个任务队列(不区分特征值) Notify在Notify队列中,Indicate在Indicate队列中,Read在Read队列中, Write在Write队列中,mtu在共享队列,rssi在rssi队列中, 不同操作任务之间相互不影响,相同操作任务之间先进先出按序执行 例如特征值1的写操作和特征值2的写操作,在同一个任务队列当中;特征值1的写操作和特征值1的读操作, 在两个不同的任务队列当中,特征值1的读操作和特征值2的写操作,在两个不同的任务队列当中。
  • BleTaskQueueType.Independent 一个设备每个特征值下的每个操作独立一个任务队列(区分特征值) Notify\Indicate\Read\Write所对应的任务分别放入到独立的任务队列中, mtu在共享队列,rssi在rssi队列中, 且按特征值区分,不同操作任务之间相互不影响,相同操作任务之间相互不影响 例如特征值1的写操作和特征值2的写操作,在两个不同的任务队列当中;特征值1的写操作和特征值1的读操作, 在两个不同的任务队列当中,特征值1的读操作和特征值2的写操作,在两个不同的任务队列当中。

注意:BleTaskQueueType.Operate、BleTaskQueueType.Independent这两种模式下

  • 1、在Notify\Indicate\Read\Write 未完成的情况下,不要执行设置Mtu,否则会导致前者操作失败
  • 2、同时执行Notify\Indicate\Read\Write其中两个以上操作,会可能报设备忙碌失败

建议:以上模式主要也是针对操作之间的问题,强烈建议不要同时执行2个及以上操作,模式BleTaskQueueType.Default就是为 了让设备所有操作同一时间只执行一个,Rssi不受影响

2、扫描

注意:扫描之前先检查权限、检查GPS开关、检查蓝牙开关
扫描及过滤过程是在工作线程中进行,所以不会影响主线程的UI操作,最终每一个回调结果都会回到主线程。
开启扫描:
BleManager.get().startScan {
    onScanStart {

    }
    onLeScan { bleDevice, currentScanCount ->
        //可以根据currentScanCount是否已有清空列表数据
    }
    onLeScanDuplicateRemoval { bleDevice, currentScanCount ->
        //与onLeScan区别之处在于:同一个设备只会出现一次
    }
    onScanComplete { bleDeviceList, bleDeviceDuplicateRemovalList ->
        //扫描到的数据是所有扫描次数的总和
    }
    onScanFail {
        val msg: String = when (it) {
            is BleScanFailType.UnSupportBle -> "BleScanFailType.UnSupportBle: 设备不支持蓝牙"
            is BleScanFailType.NoBlePermissionType -> "BleScanFailType.NoBlePermissionType: 权限不足,请检查"
            is BleScanFailType.GPSDisable -> "BleScanFailType.BleDisable: 设备未打开GPS定位"
            is BleScanFailType.BleDisable -> "BleScanFailType.BleDisable: 蓝牙未打开"
            is BleScanFailType.AlReadyScanning -> "BleScanFailType.AlReadyScanning: 正在扫描"
            is BleScanFailType.ScanError -> {
                "BleScanFailType.ScanError: ${it.throwable?.message}"
            }
        }
        BleLogger.e(msg)
        Toast.makeText(application, msg, Toast.LENGTH_SHORT).show()
    }
}

3、停止扫描

BleManager.get().stopScan()

4、是否扫描中

BleManager.get().isScanning()

5、连接

BleManager.get().connect(device)
BleManager.get().connect(deviceAddress)
  • 在某些型号手机上,connectGatt必须在主线程才能有效,所以把连接过程放在主线程,回调也在主线程。
  • 为保证重连成功率,建议断开后间隔一段时间之后进行重连。

6、断开连接

BleManager.get().disConnect(device)
BleManager.get().disConnect(deviceAddress)
  • 断开后,并不会马上更新状态,所以马上连接会直接返回已连接,而且扫描不出来,要等待一定时间才可以

7、是否已连接

BleManager.get().isConnected(device)

8、扫描并连接,如果扫描到多个设备,则会连接第一个

BleManager.get().startScanAndConnect(bleScanCallback: BleScanCallback,
                                     bleConnectCallback: BleConnectCallback)
  • 扫描到首个符合扫描规则的设备后,便停止扫描,然后连接该设备。

9、获取设备的BluetoothGatt对象

BleManager.get().getBluetoothGatt(device)

10、设置Notify

BleManager.get().notify(bleDevice: BleDevice,
                              serviceUUID: String,
                              notifyUUID: String,
                              useCharacteristicDescriptor: Boolean = false,
                              bleIndicateCallback: BleIndicateCallback)

BleDescriptorGetType设计原则

  • 正常情况下,每个特征值下至少有一个默认描述符,并且遵循蓝牙联盟定义的UUID规则,如 [com.bhm.ble.data.Constants.UUID_CLIENT_CHARACTERISTIC_CONFIG_DESCRIPTOR]便是蓝牙联盟定义的 客户端特性配置的描述符UUID,这样做是方便BLE终端在接入不同类型设备时,能够获取到正确的配置。 比如有一个APP,需要 接入A商家的智能手表和B商家的智能手表来监听用户的心跳,而如果A商家的智能手表或者B商家的智能手表 不遵循蓝牙联盟定义关于 心跳相关的UUID,则对APP来说就要分别去获取A商家的智能手表或者B商家的智能手表对应特征值的描述 符UUID,显然是不合理的。当然这个是需要硬件设备支持的,也就是说硬件设备可以自定义UUID,但需要遵循规则。
  • 在开发过程中,我们会遇到不同硬件设备定义UUID的情况,有的硬件设备通过特征值的UUID来获取描述符(用来writeDescriptor, 打开或关闭notify、indicate),而非是通过系统提供接受通知自带的UUID获取描述符。此外特征值有多个描述符时,获取其中 一个描述符来写入数据,可能会导致onCharacteristicChanged函数没有回调,我不确定是否是硬件设备需要支持修改的问题。 因此[AllDescriptor]方式则是简单粗暴的将特征值下所有的描述符都写入数据,以保证onCharacteristicChanged函数回调, 这个方法经过了一系列设备的验证可行,但不保证是完全有效的。

11、取消Notify

BleManager.get().stopNotify(bleDevice: BleDevice,
                              serviceUUID: String,
                              notifyUUID: String,
                              useCharacteristicDescriptor: Boolean = false)

12、设置Indicate

BleManager.get().indicate(bleDevice: BleDevice,
                              serviceUUID: String,
                              indicateUUID: String,
                              useCharacteristicDescriptor: Boolean = false,
                              bleIndicateCallback: BleIndicateCallback)

13、取消Indicate

BleManager.get().stopIndicate(bleDevice: BleDevice,
                              serviceUUID: String,
                              indicateUUID: String,
                              useCharacteristicDescriptor: Boolean = false)

14、读取信号值

BleManager.get().readRssi(bleDevice: BleDevice, bleRssiCallback: BleRssiCallback)
  • 获取设备的信号强度,需要在设备连接之后进行。
  • 某些设备可能无法读取Rssi,不会回调onRssiSuccess(),而会因为超时而回调onRssiFail()。

15、设置Mtu值

BleManager.get().setMtu(bleDevice: BleDevice, bleMtuChangedCallback: BleMtuChangedCallback) 
  • 设置MTU,需要在设备连接之后进行操作。
  • 默认每一个BLE设备都必须支持的MTU为23。
  • MTU为23,表示最多可以发送20个字节的数据。
  • 该方法的参数mtu,最小设置为23,最大设置为512。
  • 并不是每台设备都支持拓展MTU,需要通讯双方都支持才行,也就是说,需要设备硬件也支持拓展MTU该方法才会起效果。
    调用该方法后,可以通过onMtuChanged(int mtu)查看最终设置完后,设备的最大传输单元被拓展到多少。如果设备不支持,
    可能无论设置多少,最终的mtu还是23。
  • 建议在indicate、notify、read、write未完成的情况下,不要执行设置Mtu,否则会导致前者操作失败

16、设置连接的优先级

BleManager.get().setConnectionPriority(connectionPriority: Int)
  • 设置连接的优先级,一般用于高速传输大量数据的时候可以进行设置。

17、读特征值数据

BleManager.get().readData(bleDevice: BleDevice,
                          serviceUUID: String,
                          readUUID: String,
                          bleIndicateCallback: BleReadCallback)

18、写数据

 BleManager.get().writeData(bleDevice: BleDevice,
                            serviceUUID: String,
                            writeUUID: String,
                            data: ByteArray,
                            bleWriteCallback: BleWriteCallback)
 BleManager.get().writeData(bleDevice: BleDevice,
                            serviceUUID: String,
                            writeUUID: String,
                            data: SparseArray,
                            bleWriteCallback: BleWriteCallback)
  • 因为分包后每一个包,可能是包含完整的协议,所以分包由业务层处理,组件只会根据包的长度和mtu值对比后是否拦截

  • 特殊情况下:indicate\mtu\notify\read\rssi 这些操作,同一个特征值在不同地方调用(不同callback),最后面的操作
    对应的回调才会触发,其他地方先前的操作对应的回调不会触发
    解决方案:业务层每个特征值对应的操作维护一个单例的callback对象(假如为SingleCallback),在不同地方调用再传递callback
    (放入到SingleCallback中的集合CallbackList),SingleCallback 回调时循环CallbackList中的callback,这样就达到了
    同一个特征值在不同地方调用,都能收到回调

  • indicate\mtu\notify\read\rssi这些操作 ,同一个特征值在不同地方调用,后面的操作会取消前面未完成的操作;write操作比较
    特殊,每个写操作都会有回调,且write操作之间不会被取消。具体详情看taskId

19、断开某个设备的连接 释放资源

BleManager.get().close(bleDevice: BleDevice)

20、断开所有连接 释放资源

BleManager.get().closeAll()

21、一些移除监听的函数

BleManager.get().removeBleScanCallback()
BleManager.get().removeBleConnectCallback(bleDevice: BleDevice)
BleManager.get().removeBleIndicateCallback(bleDevice: BleDevice, indicateUUID: String)
BleManager.get().removeBleNotifyCallback(bleDevice: BleDevice, notifyUUID: String)
BleManager.get().removeBleRssiCallback(bleDevice: BleDevice)
BleManager.get().removeBleMtuChangedCallback(bleDevice: BleDevice)
BleManager.get().removeBleReadCallback(bleDevice: BleDevice, readUUID: String)
BleManager.get().removeBleWriteCallback(bleDevice: BleDevice, writeUUID: String)

存在问题

  • 1、关闭系统蓝牙,没有触发onConnectionStateChange
    解决方案:1、操作前判断蓝牙状态,2、蓝牙广播
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,214评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,307评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,543评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,221评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,224评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,007评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,313评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,956评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,441评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,925评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,018评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,685评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,234评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,240评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,464评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,467评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,762评论 2 345

推荐阅读更多精彩内容