Android-嵌入式蓝牙 多年蓝牙产品开发学习心路历程分享-A2DP音频流的建立A2DP播放暂停音乐命令交互HFPAVRCPRFCOMMOPPAVDTP
Posted 短距(connectivity)知识分享
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android-嵌入式蓝牙 多年蓝牙产品开发学习心路历程分享-A2DP音频流的建立A2DP播放暂停音乐命令交互HFPAVRCPRFCOMMOPPAVDTP相关的知识,希望对你有一定的参考价值。
序言
当你点进来看到这段话的时候,证明你真心想学好蓝牙,哈哈哈,本来想搜索蓝牙某个协议,却成了一大段废话文,但此时,别退出去,如果你是一个牛逼的蓝牙开发者,只是想了解蓝牙某个协议或者知识点,那么直接点击进入我的主页,查看蓝牙相关文章,绝对能满足你的需要。但是,如果你是一个蓝牙新人、才开始学习蓝牙开发,那么请用几分钟看完这篇文章,一定会激励你的蓝牙学习和给你提供一个正确的学习途径,废话少说,直入正题。
PS:本文章将持续更新,内容很长,请及时收藏。
一、自我介绍
1、工作历程
本人从事android 蓝牙、嵌入式蓝牙开发多年,做过手机、平板、智能手表、智能手环、TWS耳机等产品,基本市面上关于蓝牙的产品都做过。
大学本科毕业,校招进入国内500强公司,分配是手机驱动开发部门,当时对手机开发,一脸懵逼,感觉很高大上,然后就被分配到短距组,当然有的公司叫连接组(connectivity组),一般有蓝牙、wifi、gps、NFC这四个字模块,来公司的第一周,主要是公司文化等等培训,第二周我的主管让我选自己想要做的模块,我思索很久,最后选择了蓝牙,其实现在回想过来,当时选择蓝牙开发目的可能那是我手里面有个蓝牙耳机吧,哈哈哈哈哈哈。
2、学习历程
我本科在一个普通一本工程类大学,学的是机械设计及其制造化专业,学的课程基本都是机械设计、单片机、机械电子等课程,还好大一的时候学习过C语言,可惜当时和大家一样没学懂,哈哈,大学期间还算是个好学习,认真学习,好好吃饭,大二的时候,希望毕业后去一个机械厂里,当一名机械工程师。
大三的时候,我学习了数字电路和模拟电路课程,感觉好神奇的东西,因为学的机械产品都是看得见,摸得着的,一个小小的cpu,里面蕴藏着这么多神奇的东西,可能和大家一样,虽然模拟电子学不懂,但是觉得很神奇。此时我改变了我的就业方向,励志要成为一名硬件工程师,大三第二学期开启了我的疯狂学习之旅,开始疯狂学习数字电路,模拟电路,还买了许多硬件类书籍,学习电路。在网上买了课程,学习画PBC板子等等。
到了大三那个暑假,硬件学的好辛苦啊,这玩意怎么这个难,抽象,此时在网上看到了嵌入式软件开发工程师,这个就业前景好,怎么怎么滴,那好吧,搞软件吧,现在回想过来,其实当时的选择真的是根据个人爱好的,没有考虑的太多。第一次下载了哔哩哔哩APP,里面有还多关于驱动学习的课程,有进程、线程、c语言、驱动开发等等,很多课程,那个暑假我就开始了学习,给大家看看我当时买的书:
当时买的书挺多的,有的现在都丢了,就这样,一遍跟着网上视频学习,一遍看着书学习。
到了大四的十月份,有公司来校招了,决定去试试,但结果一塌糊涂,自己学的完全和面试官问的牛头不对马嘴,面试了两家公司,以失败告终。此时,我没有放弃,决定报培训班学习,在北京报了一家嵌入式培训班,可谓是行业第一的。学习到了大四的来年三月份,不得不说,培训班学的可谓是有条理,质量还是不错的,但是学费了是高了点,2w呢。
接下来就是找工作了,简历上写的是驱动开发工程师,开始面试了三四家小公司,基本没有通过,不得不说,小公司对应届生确实不太友好。。。。因为需求少,资金有限,所有要求高。当然,在面试过程中,也要不断的在网上搜索题目做,尤其是C语言基础题目。四月的时候 ,突然小米公司发来邮件,让我面试,哈哈哈,突然的惊喜,都不记得什么时候投过的简历。此时已经面试过三四次了,终于等来了小米面试,第一轮是电话面试,问了些C语言基础,进程、线程等等。第二轮是视频面试,是在牛客网上的,先让我做了两道C语言编程题目,我基本通过,然后问了我几个问题,无奈这个面试官是小米穿戴部门,主要是做APP层,对驱动不太了解,一直问我上层知识,我回答的是是而非,而我给人家说驱动知识,人家也不感兴趣。结果可想而知,没有通过面试,此时已经五月份了,我失落至极。
当然,经历过小米面试,我信心大增,打开BOSS直聘,我当时是在西安,直接就筛选一万人以上的公司,也就是当年应届生入职的某ODM公司,庆幸的是,面试官刚好问的是驱动知识,三轮面试,A+通过。面试过程就写到了这儿,如果大家有想问的,在评论区留言或者私信我,我都会一一回复的。
二、Android 蓝牙开发心路历程
1、初学Android 蓝牙
到了第一个家公司,开始学习蓝牙,学了一个多月,突然发现和驱动没有太多联系,当然,做蓝牙的工程师,有个别名,叫协议开发工程师,哈哈哈哈,世事无常,大肠包小肠。后来以至于到现在,很少做驱动了。一直搞得是蓝牙协议。
我们公司当时是上层和协议栈是两个团队,也是说JNI往上,由java团队负责处理问题,JNI向下,协议栈和controlller都是由C语言团队来处理的,当然我也就是从协议栈开始入手了。此时,我主要语言学习放到了C语言上,基本放弃了JAVA,此时c语言基础,像static、const、结构体、枚举、联合体、回调函数、函数指针、指针函数、指针等等概念,都要深刻的学习。
本博主所有的文章将全面分析蓝牙知识,按照下面这个图片来展开。
①第一阶段:蓝牙基础内容学习
此时我在网上搜索蓝牙基本概念学习,蓝牙是干嘛的,有哪些协议等等,从蓝牙的概念,到每个协议的具体概念,到framework-hal-btit-bta-btm-hci - l2cap -ll-controller,等等每一层的概念都从网上搜集到资料,进行了本地整理,当然这个阶段,学习至少得两个月,当然还要学习写Anroid架构等等知识。
下面是我工作这么多年,整理的蓝牙基础知识,需要学习的直接下载就好,不用再去网上一个一个搜索,很费事的。
Android 蓝牙基本概念,每个协议介绍,ble介绍,整理了全网所有的蓝牙基本概念,便于新人学习 - 点击下载
②第二阶段:搜索连接配对
相信大家一开始学习蓝牙,就得从打开蓝牙,搜索周围设备,配对这个流程开始学习吧,当然,配对这个流程确实是挺复杂的,那么该如何下手学习呢?从HCI日志下手学习,HCI是host和controller交互的命令-事件流程,可以清楚的看到两者之间做了哪些事情,蓝牙新手必须从HCI学习做起来。下图就是HCI搜索、连接、配对流程图:
哈哈哈哈,是不是感觉这个图片很棒,将蓝牙连接配对流程全部写了出来,那么接下来我就将搜索、连接、配对流程简单的描述下:
首先打开蓝牙开关后,需要搜索周围可发现的蓝牙设备,即inquiry,一般持续扫描12.8秒,这个时间是在协议栈中设置的,可更改,搜索完成后,此时还需要继续发现这些设备的姓名,即diacovery,会将扫描到的设备信息包含地址进行上报。这是可以发起某设备的连接、即建立ACL物理和逻辑链路,建立完成后,发起配对流程,一般配对有四种方式:数值比较、passkey、justwork、oob。蓝牙耳机一般采取的是justwork模式。之后就是生成link key,用来后续数据收发进行加密了。
下面是配对的整理流程,我将core5.2中整理了出来,可以说是全网最详细得了,可以点击查看:
学习完了,配对的基本概念,接下来是否要学习源码了?NO NO NO切记,不要直接看源码,Android源码博大而精深,还是从源码流程图看起,主要是看每个阶段,都在哪层哪个函数中,调用了哪个函数,这个函数是干嘛用的,就可以了,从宏观上,学习Android 蓝牙配对流程。
更多的蓝牙搜索、配对流程图,请点击下面链接查看:
Android 蓝牙配对、协议栈使能、inquiry、discovery、hci发送数据、等等详细源码流程图 - 点击下载
当你通过我画的流程图看懂了整理源码流程,那么接下来就需要详细的看源码每个函数中具体做了哪些事情,这个就比较多了,不是一天两天就能看完的,可以查看我写的源码分析来进行学习:
③第三阶段:A2DP
相信大家学习的第一个蓝牙协议都是A2DP吧,哈哈哈,当然头最疼的也是这个协议吧,A2DP使用场景是最多的,出问题也是最多的,如果你是做Android的,那么最重要的是学习A2DP这个啦,为什么说重要呢?因为A2DP只要给音频流提供一个通道,需要audio那边将数据流发到蓝牙侧,然后蓝牙在转发出去,所以牵扯到audio,这就麻烦了,在具体项目中,很容易和audio的同事扯皮,哈哈哈哈哈。我先把A2DP发送音频流程简单描述下:
音频流传输路径:bt侧每20ms 从audio的buf中拿pcm data,之后进行encoder,然后将encoder的数据入队,之后我们stack AVDTP 会将队列里的数据写进L2CAP的buff里,如果Controller 中NOCP(Number of complete packet)回的慢,L2CAP的buff就无法发送到peer,从而造成bt侧的queue内部overflow,进而BT侧就会清除queue中的数据,从而造成了声音不连续。
下面是我写的A2DP的文章,你可以点击看看:
Android 蓝牙 A2DP基础概念、A2DP音频流的建立及传输流程、A2DP播放暂停音乐命令交互过程分析 - 史上最全分析
学习完了A2DP的基本概念,要深入学习A2DP,先从流图图学习,一定不要从源码函数学习。
下面这张图是音频流从Audio到A2dp的路径:可以说是本人用了九牛二虎之力,从网上查询资料,到抓取手机日志,到反复查看源码总结出来的流程。
上图只是音频流的流向,如果还要深入了解A2DP,还是需要学习A2DP初始化、使能,java-framework的实现流程,这样才能彻底理解,完整的A2DP使能、连接、播放流程图请点击下面链接查看:
Android A2dp 初始化,连接,音乐播放、avrcp连接源码流程,非常详细的从btif-bta-btm-hci 数据流程 - 点击下载
当看完流程图分析了,接下来就是学习源码了,这个其实还是蛮难的,因为每个函数都是很长的,代码量很大,要想理解每句代码的含义,还是需要有别人的指导才能进行,我分析了A2DP源码函数,将每个函数都一一进行了剖析,使Android 蓝牙开发者,更能清楚的理解:
Android 蓝牙A2dp-Avrcp初始化-连接-播放源码分析文档大全,非常详细的从btif-bta-btm-hci 数据流 - 点击下载
④第三阶段:HFP
HFP是手机蓝牙与外设连接的时候,第一个就连接上的协议,优先级是最高的,首先建立了Rfcomm通道,主要用来进行AT命令交互,然后建立SLC服务层,用来交互编码格式等等一些信息,最后是当有电话来临时候,发起eSCO链路的建立。HFP的学习过程中还要进行Telecomm的学习,应该毕竟是和打电话有关的,所以必要情况下还是要学习medom的知识。
和之前一样,先学习Android 蓝牙HFP的源码流程图,从整体上了解HFP协议:
Android 蓝牙HFP 初始化init,协议连接,andio连接,非常详细的从btif-bta-btm-hci 数据流程走 - 点击下载
接下来就是源码的学习了:
Android 蓝牙HFP 初始化init,协议连接,andio连接,非常详细的从btif-bta-btm-hci 数据流程走 - 点击下载
⑤第四阶段:AVRCP
AVRCP协议是Device端用来控制Target端的,比如说播放音乐下一曲、上一曲、暂停、音量加减等等,当A2DP 连接成功后,会连接AVRCP协议的,具体的AVRCP概念可以看我这篇文章:
Android-蓝牙AVRCP 以及 绝对音量(absolute volume)概述-A2dp - 点击下载
其实这个协议稍微比较简单,在实际项目开发过程,设计比较多的问题,就是绝对音量的问题,我们都知道,绝对音量就是使用的是AVRCP协议,有时候某个设备兼容性的问题,我们可能将device的绝对音量功能添加到黑名单中,来避免绝对音量的问题。可以看下我写的这篇文档,主要将如何将设备添加到黑名单中:
⑥第五阶段:OPP
OPP协议在手机端是用来传输文件的,在嵌入式开发中,是没有这个协议的,很多人可能在手机上都已经忽略掉这个功能了,但对于我们蓝牙开发者来说的话,还是要关注下的,因为在手机开发中,会有一个测试项:利用opp协议传输文件,测试功耗,那么我们还是要学习写这个协议的。可以参考我这篇文档,将OPP流程进行全面的分析:
Android 蓝牙opp obex协议连接源码分析(1)-opp连接
⑦第六阶段:PBAP
PBAP协是是都用来同步电话本的,比如我们利用车载设备来拨打电话时候,或者在开发智能手表项目时候,在device端拨打电话时候,就需要利用到这个协议了,来进行电话号码的拉取,
具体的代码流程参考我写的这篇文章:
Android 蓝牙 PBAP obex协议详解 - 史上最全
⑧第七阶段:RFCOMM
RFCOMM协议,这个协议很重要,目前hfp 、opp、spp等等底层都是通过rfcomm来传输数据的,这个协议的重点在于:
1、全双工:也就是master和slave都可以发起连接,并且CID号是固定的,也就是在代码中写死的。
2、rfcomm流控:发送数据肯定要流控,不管项目中还是面试中,都会问道rfcomm流控这个问题,所有要多学习:下面这篇文件将rfcomm流控讲解的非常清楚,可以详细看看:
Android 蓝牙 SPP串口协议 rfcomm 流控以及credit配置
看完rfcomm基本概念后,就要学习源码了,当前本博主很贴心的给大家梳理出来了。请点击下面连接下载查看,因为是word格式,比较多。
Android 蓝牙hfp初始化、rfcomm连接、slc连接、sco连接源代码分析大全,非常详细的从btif-bta-btm
今天就先写到这里了,过几天再更新,有学习蓝牙的朋友可以评论或者私信留言哦,我都会一一回复的哦。
Android 蓝牙开发 -- 低功耗蓝牙开发
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 declare
ACCESS_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 declare
ACCESS_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 = callback
if (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 ?: return
result.device.name ?: return
val bean = BleData(result.device, result.scanRecord.toString())
devCallback?.let
it(bean)
效果如下:
四. 手机充当外围设备(服务端)
上面说到,Android 5.0 之后,手机也能充当外围设备,这里我们也来实践一下;
首先,Android要完成一个外围设备,需要完成以下步骤:
- 编写广播设置,比如发送实践,发送功率等
- 编写广播数据,这个是需要的,需要设置 service 的uuid,或者显示名字等
- 编写扫描广播(可选),这个广播当中心设备扫描时,数据能被接受的广播,通常我们会在这里编写一些厂商数据
- 添加 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个字节。
//测试 31bit
val 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。
//添加读+通知的 GattCharacteristic
val readCharacteristic = BluetoothGattCharacteristic(
BleBlueImpl.UUID_READ_NOTIFY,
BluetoothGattCharacteristic.PROPERTY_READ or BluetoothGattCharacteristic.PROPERTY_NOTIFY,
BluetoothGattCharacteristic.PERMISSION_READ
)
//添加写的 GattCharacteristic
val 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 ?: return
Log.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?.device
if (newState == BluetoothProfile.STATE_CONNECTED)
isConnected = true
//开始发现服务,有个小延时,最后200ms后尝试发现服务
handler.postDelayed(
gatt?.discoverServices()
,300)
device?.letlogInfo("与 $it.name 连接成功!!!")
else if (newState == BluetoothProfile.STATE_DISCONNECTED)
isConnected = false
logInfo("无法与 $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 = gatt
logInfo("已连接上 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获取可读的Characteristic
mBluetoothGatt?.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-嵌入式蓝牙 多年蓝牙产品开发学习心路历程分享-A2DP音频流的建立A2DP播放暂停音乐命令交互HFPAVRCPRFCOMMOPPAVDTP的主要内容,如果未能解决你的问题,请参考以下文章
Android 蓝牙开发 --手机与蓝牙音箱配对,并播放音频