Android 蓝牙开发(一) – 传统蓝牙聊天室
Android 蓝牙开发(三) – 低功耗蓝牙开发
项目工程BluetoothDemo

前面已经学习了经典蓝牙开发,学习了蓝牙的配对连接和通信,又通过 配置 A2DP 文件,实现手机和蓝牙音响的连接和播放语音。

这篇,我们来学习蓝牙开发的最后一章,低功耗蓝牙 BLE,也就是我们常说的蓝牙 4.0 。
今天要完成的效果如下:

中心设备 外围设备

一. 简介

与传统蓝牙不同,低功耗蓝主要为了降低设备功耗,支持更低功耗(如心率检测仪,健身设备)等设备进行通信。

Android 在 4.3(API 18) 后将低功耗蓝牙内置,并提供对应的 API,以便于应用发现设备,查询服务和传输信息

1.1 相关概念

低功耗蓝牙有两个角色,分别是中心设备和外围设备

  • 外围设备:指功耗更低的设备,会不断的发出广播,直到与中心设备连接
  • 中心设备:可以进行扫描,寻找外设广播,并从广播中拿到数据

一般我们的手机会充当中心设备,去搜索周围外设的广播,比如健康设备等,然后健康设备就是外围设备,一直发广播,直到中心设备连接上。在Android 5.0 后,手机也可以充当外围设备。

1.2 关键术语

关于 BLE 的关键术语如下:

  • 通用属性配置文件(GATT) : GATT 配置文件是一种通用规范,内容主要针对的是 BLE 通信读写时的简短的数据片段,目前 BLE 的通信均以 GATT 为基础
  • 属性协议(ATT) : ATT 是 GATT 的基础,由它来传输属性和特征服务,这些属性都有一个特定的 UUID来作为唯一标识,为通信的基础。
  • GATT Service : 通常中心设备与外围设备要进行,首先要知道服务的 UUID,并与之建立通信,然后通过特征描述符等进行数据通信,这些等后面我们再来理解

二. 权限配置

首先,你需要使用 BLUETOOTH 的权限,考虑到 LE 信标通常与位置相关联,还须声明 ACCESS_FINE_LOCATION 权限。没有此权限,扫描将无法返回任何结果。

注意:如果您的应用适配 Android 9(API 级别 28)或更低版本,则您可以声明 ACCESS_COARSE_LOCATION 权限而非 ACCESS_FINE_LOCATION 权限。

<uses-permission android:name="android.permission.BLUETOOTH"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/><!-- If your app targets Android 9 or lower, you can declareACCESS_COARSE_LOCATION instead. -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

注意!Android 10 需要你开启gps,否则蓝牙不可用

如果你想要你的设备只支持 BLE ,还可以有以下神明:

<uses-permission android:name="android.permission.BLUETOOTH"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/><!-- If your app targets Android 9 or lower, you can declareACCESS_COARSE_LOCATION instead. -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

如果设置 required=“false”,你也可以在运行时使用 PackageManager.hasSystemFeature() 确定 BLE 的可用性:

private fun PackageManager.missingSystemFeature(name: String): Boolean = !hasSystemFeature(name)
...packageManager.takeIf { it.missingSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE) }?.also {Toast.makeText(this, R.string.ble_not_supported, Toast.LENGTH_SHORT).show()finish()
}

三. 查找 BLE 设备

关于蓝牙的开启,请参考 Android 蓝牙开发(一) – 传统蓝牙聊天室

要查找 BLE 设备,在 5.0 之前,使用 startLeScan() 方法,它会返回当前设备和外设的广播数据。不过在 5.0 之后,使用 startScan() 去扫描,这里为了方便手机充当外围设备,统一使用 5.0 之后的方法。

而扫描是耗时的,我们应该在扫描到想要的设备后就立即停止或者在规定时间内停止,扫描代码如下:

fun scanDev(callback: BleDevListener) {devCallback = callbackif (isScanning) {return}//扫描设置val builder = ScanSettings.Builder()/*** 三种模式* - SCAN_MODE_LOW_POWER : 低功耗模式,默认此模式,如果应用不在前台,则强制此模式* - SCAN_MODE_BALANCED : 平衡模式,一定频率下返回结果* - SCAN_MODE_LOW_LATENCY 高功耗模式,建议应用在前台才使用此模式*/.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)//高功耗,应用在前台if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {/*** 三种回调模式* - CALLBACK_TYPE_ALL_MATCHED : 寻找符合过滤条件的广播,如果没有,则返回全部广播* - CALLBACK_TYPE_FIRST_MATCH : 仅筛选匹配第一个广播包出发结果回调的* - CALLBACK_TYPE_MATCH_LOST : 这个看英文文档吧,不满足第一个条件的时候,不好解释*/builder.setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES)}//判断手机蓝牙芯片是否支持皮批处理扫描if (bluetoothAdapter.isOffloadedFilteringSupported) {builder.setReportDelay(0L)}isScanning = true//扫描是很耗电的,所以,我们不能持续扫描handler.postDelayed({bluetoothAdapter.bluetoothLeScanner?.stopScan(scanListener)isScanning = false;}, 3000)bluetoothAdapter.bluetoothLeScanner?.startScan(null, builder.build(), scanListener)//过滤特定的 UUID 设备//bluetoothAdapter?.bluetoothLeScanner?.startScan()}}

可以看到,在 5.0 之后可以通过 ScanSettings 进行扫描的一些设备,比如设置扫描模式 setScanMode ,在 startScan() 中,也可以过滤自己的 UUID,从而省去一些时间。接着在扫描回调中,把能获取名字的设备通过回调给 recyclerview 去回调。

 private val scanListener = object : ScanCallback() {override fun onScanResult(callbackType: Int, result: ScanResult?) {super.onScanResult(callbackType, result)//不断回调,所以不建议做复杂的动作result ?: returnresult.device.name ?: returnval bean = BleData(result.device, result.scanRecord.toString())devCallback?.let {it(bean)}}

效果如下:

四. 手机充当外围设备(服务端)

上面说到,Android 5.0 之后,手机也能充当外围设备,这里我们也来实践一下;

首先,Android要完成一个外围设备,需要完成以下步骤:

  1. 编写广播设置,比如发送实践,发送功率等
  2. 编写广播数据,这个是需要的,需要设置 service 的uuid,或者显示名字等
  3. 编写扫描广播(可选),这个广播当中心设备扫描时,数据能被接受的广播,通常我们会在这里编写一些厂商数据
  4. 添加 Gatt service ,用来跟中心设备通信

4.1 广播设置

在发送广播之前,我们可以先对广播进行一些配置:

 /*** GAP广播数据最长只能31个字节,包含两中: 广播数据和扫描回复* - 广播数据是必须的,外设需要不断发送广播,让中心设备知道* - 扫描回复是可选的,当中心设备扫描到才会扫描回复* 广播间隔越长,越省电*///广播设置val advSetting = AdvertiseSettings.Builder()//低延时,高功率,不使用后台.setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_LOW_LATENCY)// 高的发送功率.setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_HIGH)// 可连接.setConnectable(true)//广播时限。最多180000毫秒。值为0将禁用时间限制。(不设置则为无限广播时长).setTimeout(0).build()

可以看到,这里设置成可连接广播,且广播模式设置为 SCAN_MODE_LOW_LATENCY 高功耗模式 ,它共有三种模式:

  • SCAN_MODE_LOW_POWER : 低功耗模式,默认此模式,如果应用不在前台,则强制此模式
  • SCAN_MODE_BALANCED : 平衡模式,一定频率下返回结果
  • SCAN_MODE_LOW_LATENCY 高功耗模式,建议应用在前台才使用此模式

发送功率也是可选的:

  • 使用高TX功率级别进行广播:AdvertiseSettings#ADVERTISE_TX_POWER_HIGH
  • 使用低TX功率级别进行广播:AdvertiseSettings#ADVERTISE_TX_POWER_LOW
  • 使用中等TX功率级别进行广播:AdvertiseSettings#ADVERTISE_TX_POWER_MEDIUM
  • 使用最低传输(TX)功率级别进行广播:AdvertiseSettings#ADVERTISE_TX_POWER_ULTRA_LOW

4.2 配置发送广播数据

接着,是广播数据包:

 //设置广播包,这个是必须要设置的val advData = AdvertiseData.Builder().setIncludeDeviceName(true) //显示名字.setIncludeTxPowerLevel(true)//设置功率.addServiceUuid(ParcelUuid(BleBlueImpl.UUID_SERVICE)) //设置 UUID 服务的 uuid.build()

比较好理解,让广播显示手机蓝牙名字,并设置服务的 UUID

4.3 配置扫描广播(可选)

扫描广播是当中心设备在扫描时,能够显示出来的广播,它可以添加一些必要数据,如厂商数据,服务数据等,注意!与上面的广播一样,不能超过31个字节

//测试 31bitval byteData = byteArrayOf(-65, 2, 3, 6, 4, 23, 23, 9, 9,9,1, 2, 3, 6, 4, 23, 23, 9, 9, 8,23,23,23)//扫描广播数据(可不写,客户端扫描才发送)val scanResponse = AdvertiseData.Builder()//设置厂商数据.addManufacturerData(0x19, byteData).build()

最后,使用 startAdvertising() 就可以开始发送广播了:

val bluetoothLeAdvertiser = bluetoothAdapter?.bluetoothLeAdvertiser//开启广播,这个外设就开始发送广播了bluetoothLeAdvertiser?.startAdvertising(advSetting,advData,scanResponse,advertiseCallback)

使用 去监听广播开启成功与否:

    private val advertiseCallback = object : AdvertiseCallback() {override fun onStartSuccess(settingsInEffect: AdvertiseSettings?) {super.onStartSuccess(settingsInEffect)logInfo("服务准备就绪,请搜索广播")}override fun onStartFailure(errorCode: Int) {super.onStartFailure(errorCode)if (errorCode == ADVERTISE_FAILED_DATA_TOO_LARGE) {logInfo("广播数据超过31个字节了 !")} else {logInfo("服务启动失败: $errorCode")}}}

此时,你去搜索,就能搜到你手机的蓝牙名称和对应的广播数据了。

4.4 Gatt Service

但如果外围设备想要与中心设备通信,还需要启动 Gatt service 才行,上面说到,启动Service 时,我们需要配置特征 Characteristic描述符 Descriptor,这里我们来解释以下。

4.3 特征 Characteristic

Characteristic 是Gatt通信最小的逻辑单元,一个 characteristic 包含一个单一 value 变量 和 0-n个用来描述 characteristic 变量的 描述符 Descriptor。与 service 相似,每个 characteristic 用 16bit或者32bit的uuid作为标识,实际的通信中,也是通过 Characteristic 进行读写通信的。

所以为了方便通信,这里我们要添加读写的 Characteristic。

//添加读+通知的 GattCharacteristicval readCharacteristic = BluetoothGattCharacteristic(BleBlueImpl.UUID_READ_NOTIFY,BluetoothGattCharacteristic.PROPERTY_READ or BluetoothGattCharacteristic.PROPERTY_NOTIFY,BluetoothGattCharacteristic.PERMISSION_READ)//添加写的 GattCharacteristicval writeCharacteristic = BluetoothGattCharacteristic(BleBlueImpl.UUID_WRITE,BluetoothGattCharacteristic.PROPERTY_WRITE,BluetoothGattCharacteristic.PERMISSION_WRITE)

描述符 Descriptor

它的定义就是描述 GattCharacteristic 值已定义的属性,比如指定可读的属性,可接受范围等,比如为写的 特征添加描述符:

 //添加 Descriptor 描述符val descriptor =BluetoothGattDescriptor(BleBlueImpl.UUID_DESCRIBE,BluetoothGattDescriptor.PERMISSION_WRITE)//为特征值添加描述writeCharacteristic.addDescriptor(descriptor)

接着,把特征添加到服务中,并使用openGattServer() 去打开 Gatt 服务:

/*** 添加 Gatt service 用来通信*///开启广播service,这样才能通信,包含一个或多个 characteristic ,每个service 都有一个 uuid
val gattService =BluetoothGattService(BleBlueImpl.UUID_SERVICE,BluetoothGattService.SERVICE_TYPE_PRIMARY)
gattService.addCharacteristic(readCharacteristic)
gattService.addCharacteristic(writeCharacteristic)val bluetoothManager = getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
//打开 GATT 服务,方便客户端连接
mBluetoothGattServer = bluetoothManager.openGattServer(this, gattServiceCallbak)
mBluetoothGattServer?.addService(gattService)

代码比较简单,接着就可以使用 gattServiceCallbak 去监听数据成功与读写的数据了:

    private val gattServiceCallbak = object : BluetoothGattServerCallback() {override fun onConnectionStateChange(device: BluetoothDevice?, status: Int, newState: Int) {super.onConnectionStateChange(device, status, newState)device ?: returnLog.d(TAG, "zsr onConnectionStateChange: ")if (status == BluetoothGatt.GATT_SUCCESS && newState == 2) {logInfo("连接到中心设备: ${device?.name}")} else {logInfo("与: ${device?.name} 断开连接失败!")}}...

五. 中心设备连接外设(客户端)

上面已经配置了服务端的代码,接着,通过扫描到的广播,使用 BluetoothDevice 的 connectGatt() 方法,来连接 GATT 服务:

 override fun onItemClick(adapter: BaseQuickAdapter<*, *>, view: View, position: Int) {//连接之前先关闭连接closeConnect()val bleData = mData[position]blueGatt = bleData.dev.connectGatt(this, false, blueGattListener)logInfo("开始与 ${bleData.dev.name} 连接.... $blueGatt")}

此时,如果你的配置没有出错的话,就可以通过 BluetoothGattCallback 回调连接到设备了:

private val blueGattListener = object : BluetoothGattCallback() {override fun onConnectionStateChange(gatt: BluetoothGatt?, status: Int, newState: Int) {super.onConnectionStateChange(gatt, status, newState)val device = gatt?.deviceif (newState == BluetoothProfile.STATE_CONNECTED){isConnected = true//开始发现服务,有个小延时,最后200ms后尝试发现服务handler.postDelayed({gatt?.discoverServices()},300)device?.let{logInfo("与 ${it.name} 连接成功!!!")}}else if (newState == BluetoothProfile.STATE_DISCONNECTED){isConnected = falselogInfo("无法与 ${device?.name} 连接: $status")closeConnect()}}override fun onServicesDiscovered(gatt: BluetoothGatt?, status: Int) {super.onServicesDiscovered(gatt, status)// Log.d(TAG, "zsr onServicesDiscovered: ${gatt?.device?.name}")val service = gatt?.getService(BleBlueImpl.UUID_SERVICE)mBluetoothGatt = gattlogInfo("已连接上 GATT 服务,可以通信! ")}

代码应该好看懂,就是 onConnectionStateChange() 中的 newState 为 BluetoothProfile.STATE_CONNECTED 时,表示已经连接上了,这个时候,尝试去发现这个服务,如果也能回调 onServicesDiscovered() 方法,则证明此时 GATT 服务已经成功建立,可以进行通信了。

5.1 读数据

此时就可以来读取外围设备的数据,这个数据是外围设备给中心设备去读的,所以,外围设备的读回调是这样的:

外围设备的BluetoothGattServerCallback

        override fun onDescriptorReadRequest(device: BluetoothDevice?,requestId: Int,offset: Int,descriptor: BluetoothGattDescriptor?) {super.onDescriptorReadRequest(device, requestId, offset, descriptor)val data = "this is a test"mBluetoothGattServer?.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS,offset, data.toByteArray())logInfo("客户端读取 [descriptor ${descriptor?.uuid}] $data")}

很简单,就是发送一个 "this is a test " 的字符传

中心设备读

 /*** 读数据*/fun readData(view: View) {//找到 gatt 服务val service = getGattService(BleBlueImpl.UUID_SERVICE)if (service != null) {val characteristic =service.getCharacteristic(BleBlueImpl.UUID_READ_NOTIFY) //通过UUID获取可读的CharacteristicmBluetoothGatt?.readCharacteristic(characteristic)}}// 获取Gatt服务private fun getGattService(uuid: UUID): BluetoothGattService? {if (!isConnected) {Toast.makeText(this, "没有连接", Toast.LENGTH_SHORT).show()return null}val service = mBluetoothGatt?.getService(uuid)if (service == null) {Toast.makeText(this, "没有找到服务", Toast.LENGTH_SHORT).show()}return service}

如果找得到 GATT 服务,则通过 getCharacteristic() 拿到 GATT 通信的最小单元 Characteristic,通过 mBluetoothGatt?.readCharacteristic(characteristic) 读取数据,这样就会在 BluetoothGattCallback回调的 onCharacteristicRead 拿到数据:

override fun onCharacteristicRead(gatt: BluetoothGatt?,characteristic: BluetoothGattCharacteristic?,status: Int) {super.onCharacteristicRead(gatt, characteristic, status)characteristic?.let {val data = String(it.value)logInfo("CharacteristicRead 数据: $data")}}

同理写也一样,这样我们的 BLE 低功耗蓝牙就学习结束了

参考:
https://www.jianshu.com/p/d273e46f47b1
https://developer.android.google.cn/guide/topics/connectivity/bluetooth-le

Android 蓝牙开发(三) -- 低功耗蓝牙开发相关推荐

  1. Android 8.0 BLE 低功耗蓝牙开发记录

    Android 8.0 BLE 低功耗蓝牙开发记录(1-3)--------------(权限申请篇未完待续) 目的:开源博客,希望大家一起修改博客错误地方,共同完善并会鸣谢提供意见的朋友.为大家提供 ...

  2. ESP32开发三_蓝牙开发

    蓝牙开发 交流QQ: 1048272975             QQ交流群: 636564526 蓝牙是一种不断创新发展的无线通信技术标准,采用了2.4G ISM频段,在音频传输.数据传输.位置服 ...

  3. c# 低功耗蓝牙_Android ble低功耗蓝牙开发-客户端

    什么是BLE(低功耗蓝牙) BLE(Bluetooth Low Energy,低功耗蓝牙)是对传统蓝牙BR/EDR技术的补充. 尽管BLE和传统蓝牙都称之为蓝牙标准,且共享射频,但是,BLE是一个完全 ...

  4. Qt低功耗蓝牙系列一(什么是低功耗蓝牙开发,低功耗蓝牙的通信机制原理)

    文章目录 前言 Android 蓝牙 BLE 低功耗蓝牙协议栈简介 蓝牙的选用 BLE 低功耗蓝牙模块具体应用场景 蓝牙灯控方案 BLE 蓝牙智能锁方案 蓝牙 MAC 地址扫描打印解决方案 蓝牙 Me ...

  5. windows 蓝牙程序开发 ble低功耗蓝牙

    1.需要使用到的动态库 Bluetooth Microsoft.Windows.SDK.Contracts 2.需要使用到的命名控件 using System.Threading.Tasks; usi ...

  6. Android硬件操作之低功耗蓝牙(一) 硬件篇

    一.综述 我入手的蓝牙模块是HC08模块,属于低功耗蓝牙的一种,厂家已经做好了一些封装,直接使用串口 就能通信.我挺喜欢这种傻瓜式的操作方式的,我和同学一起买了4块蓝牙模块,HC08和HC06个一块. ...

  7. 蓝牙:深入浅出低功耗蓝牙(BLE)协议栈

    深入浅出低功耗蓝牙(BLE)协议栈 BLE协议栈为什么要分层?怎么理解BLE"连接"?如果BLE协议只有ATT层没有GATT层会发生什么? 协议栈框架 一般而言,我们把某个协议的实 ...

  8. BG22蓝牙——第三弹 蓝牙的一些入门知识,整理了大佬们的文章和链接

    低功耗蓝牙和经典蓝牙概述 低功耗蓝牙和经典蓝牙概述 https://www.cnblogs.com/iini/p/8692541.html 为了区分所谓的低功耗蓝牙和经典蓝牙,蓝牙4.0发布之初,SI ...

  9. BT传统蓝牙和BLE低功耗蓝牙的区别

    蓝牙3.0及以下为传统蓝牙.   蓝牙4.0以上标准包含两个蓝牙标准,是一个双模的标准,它包含经典蓝牙部分(Classic Bluetooth)和低功耗蓝牙部分(Bluetooth Low Energ ...

最新文章

  1. 分享Silverlight/WPF/Windows Phone一周学习导读(10月1日-10月15日)
  2. 图解排序算法之3种简单排序(选择,冒泡,直接插入)
  3. ubuntu adb
  4. 积极参加各类竞赛——大学生竞赛
  5. Linux下的查找命令
  6. requirejs(shim)处理加载非AMD规范的js库
  7. SELinux入门:了解和配置SELinux
  8. windows server 2012 --安装远程桌面服务后无法远程的问题
  9. matlab中fft定点运算,可用于嵌入式计算的定点FFT算法 (转载)
  10. 全网最新猎豹网校-快速掌握Python项目实战
  11. 流利阅读 2019.1.21 Top S. Korean animal rights group slammed for destroying dogs
  12. 解决通过硬盘或U盘安装ubuntu server出现无法挂载光盘的问题教程
  13. 带通采样(欠采样)原理以及其在ADC中下变频的应用
  14. UNDO自动管理模式下,如何手工指定回滚段(_smu_debug_mode)
  15. YOLO算法改进之结合GradCAM可视化热力图(附详细教程)
  16. 技术揭秘“QQ空间”自动转发不良信息
  17. 一天一天学做外挂@第三天-实现人物飞天效果[武林外传]
  18. iOS开发·紫色感叹号
  19. squid源码分析4—coss存储机制分析
  20. mems加速度计芯片的参数介绍

热门文章

  1. 可编程渲染管线(Scriptable Render Pipeline, SRP)
  2. 网络直播中延迟产生的原因及解决方案
  3. singan:learning a generative model from a single natural image
  4. 2021年煤气找解析及煤气考试技巧
  5. CPU温度实时预警并将信息发送到钉钉
  6. 在SAFe中如何组织和管理价值流?
  7. 使用ArchiSteamFarm在树莓派挂卡
  8. 从零开始 荔枝派nano TF卡启动
  9. 关于DDD,个人理解
  10. python-最后-高级技巧