前言

最近比较忙,两三周没有更新简书了,公司正好在做蓝牙BLE的项目,本来觉得挺简单的东西从网上找了个框架,就咔咔地开始搞,搞完以后才发现里面还有不少坑呢,故而写一篇蓝牙BLE入门及爬坑指南,旨在帮助刚入蓝牙BLE的小伙伴们少走弯路。

注:本文所有的具体代码实现都在文章最后的github上

经典蓝牙和蓝牙BLE的区别

说起蓝牙,大家一定听过蓝牙1.0 2.0 3.0 4.0,不过现在已经不再用版本号区分蓝牙了,蓝牙1.0~3.0都是经典蓝牙,在塞班系统就已经开始使用了,确实很经典。有些人一直认为蓝牙4.0就是蓝牙BLE,其实是错误的。因为4.0是双模的,既包括经典蓝牙又包括低能耗蓝牙。经典蓝牙和蓝牙BLE虽然都是蓝牙,但其实还是存在很大区别的。蓝牙BLE相比于经典蓝牙的优点是搜索、连接的速度更快,关键就是BLE(Bluetooth Low Energy)低能耗,缺点呢就是传输的速度慢,传输的数据量也很小,每次只有20个字节。但是蓝牙BLE因为其低能耗的优点,在智能穿戴设备和车载系统上的应用越来越广泛,因此,蓝牙BLE开发已经是我们Android开发不得不去掌握的一门技术了。

蓝牙BLE的简介

蓝牙BLE是在Android4.3系统及以上引入的,但是仅作为中央设备,直到5.0以后才可以既作为中央设备又可以作为周边设备。也就是5.0系统以后,可以手机控制手机了,不过绝大多数的场景手机还是作为中央设备去控制其他的周边设备。Android BLE 使用的蓝牙协议是 GATT 协议。关于这个GATT协议,我就不详细给大家介绍了,放上个链接,感兴趣的可以看一下http://blog.chinaunix.net/uid-21411227-id-5750680.html

Service和Characteristic

Service是服务,Characteristic是特征值。蓝牙里面有多个Service,一个Service里面又包括多个Characteristic,具体的关系可以看图

service和characteristic的关系

图中画的比较少,实际上一个蓝牙协议里面包含的Service和Characteristic是比较多的 ,这时候你可能会问,这么多的同名属性用什么来区分呢?答案就是UUID,每个Service或者Characteristic都有一个 128 bit 的UUID来标识。Service可以理解为一个功能集合,而Characteristic比较重要,蓝牙设备正是通过Characteristic来进行设备间的交互的(如读、写、订阅等操作)。

小结

经典蓝牙和蓝牙BLE虽然都是蓝牙,但是在连接和数据传递上还是存在很大的区别,而蓝牙BLE依靠着其低能耗的特点,逐渐在智能穿戴设备上占有一席之地。蓝牙BLE基于GATT协议传输数据,提供了Serivice和Characteristic进行设备之间的通讯。以上,就是蓝牙BLE的基本概念,下面开始蓝牙BLE的正式开发!

蓝牙BLE正确开发姿势(本文重点)

第一步:声明蓝牙BLE权限

<!--声明蓝牙权限--><uses-permission android:name="android.permission.BLUETOOTH" /><uses-permission android:name="android.permission.BLUETOOTH_ADMIN" /><uses-permission android:name="android.permission.BLUETOOTH_PRIVILEGED" /><uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /><uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

Android6.0系统以上开启蓝牙还需要定位权限,定位权限属于危险权限,需要动态申请,笔者实现的方法是使用了RxPerssion动态库。

 /*** 检查权限*/private void checkPermissions() {RxPermissions rxPermissions = new RxPermissions(MainActivity.this);rxPermissions.request(android.Manifest.permission.ACCESS_FINE_LOCATION).subscribe(new io.reactivex.functions.Consumer<Boolean>() {@Overridepublic void accept(Boolean aBoolean) throws Exception {if (aBoolean) {// 用户已经同意该权限scanDevice();} else {// 用户拒绝了该权限,并且选中『不再询问』ToastUtils.showLong("用户开启权限后才能使用");}}});}

第二步:连接蓝牙前需要初始化的工作

mBluetoothManager= (BluetoothManager) getSystemService(BLUETOOTH_SERVICE);mBluetoothAdapter=mBluetoothManager.getAdapter();if (mBluetoothAdapter==null||!mBluetoothAdapter.isEnabled()){Intent intent=new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);startActivityForResult(intent,0);}

拿到BluetoothManager,在通过BluetoothManager.getAdapter()拿到BluetoothAdapter,然后判断一下蓝牙是否打开,没打开的话Intent隐式调用打开系统开启蓝牙界面。

第三步:扫描设备

 /*** 开始扫描 10秒后自动停止* */private void scanDevice(){tvSerBindStatus.setText("正在搜索");isScaning=true;pbSearchBle.setVisibility(View.VISIBLE);mBluetoothAdapter.startLeScan(scanCallback);new Handler().postDelayed(new Runnable() {@Overridepublic void run() {//结束扫描mBluetoothAdapter.stopLeScan(scanCallback);runOnUiThread(new Runnable() {@Overridepublic void run() {isScaning=false;pbSearchBle.setVisibility(View.GONE);}});}},10000);}

蓝牙扫描如果不停止,会持续扫描,很消耗资源,一般都是开启10秒左右停止

BluetoothAdapter.LeScanCallback scanCallback=new BluetoothAdapter.LeScanCallback() {@Overridepublic void onLeScan(BluetoothDevice device, int rssi, byte[] scanRecord) {Log.e(TAG, "run: scanning...");if (!mDatas.contains(device)){mDatas.add(device);mRssis.add(rssi);mAdapter.notifyDataSetChanged();}}};

这里的scanCallback是上一段代码里mBluetoothAdapter.startLeScan(scanCallback)里面的对象,其中onLeScan(BluetoothDevice device, int rssi, byte[] scanRecord)里面的参数都很直观,device是设备对象,rssi扫描到的设备强度,scanRecord是扫面记录,没什么卵用。 扫描过的设备仍然会被再次扫描到,因此要加入设备列表之前可以判断一下,如果已经加入过了就不必再次添加了。
看一下搜索的效果图吧

搜索效果图

第三步:连接设备

BluetoothDevice bluetoothDevice= mDatas.get(position);//连接设备tvSerBindStatus.setText("连接中");if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {mBluetoothGatt = bluetoothDevice.connectGatt(MainActivity.this,true, gattCallback, TRANSPORT_LE);} else {mBluetoothGatt = bluetoothDevice.connectGatt(MainActivity.this,true, gattCallback);}

连接这里大家可能已经发现了,判断了一下手机系统,6.0及以上连接设备的方法是bluetoothDevice.connectGatt(MainActivity.this,true, gattCallback, TRANSPORT_LE)。这里就是我遇见的第一个大坑了,我的手机是8.0的系统使用
bluetoothDevice.connectGatt(MainActivity.this, true, gattCallback);总是连接失败,提示status返回133,用了各种方法都不行,后台一查才发现6.0及以上系统的手机要使用bluetoothDevice.connectGatt(MainActivity.this,true, gattCallback, TRANSPORT_LE),其中TRANSPORT_LE参数是设置传输层模式。传输层模式有三种TRANSPORT_AUTO 、TRANSPORT_BREDR 和TRANSPORT_LE。如果不传默认TRANSPORT_AUTO,6.0系统及以上需要使用TRANSPORT_LE这种传输模式,具体为啥,我也不知道,我猜是因为Android6.0及以上系统重新定义了蓝牙BLE的传输模式必须使用TRANSPORT_LE这种方式吧。bluetoothDevice.connectGatt()方法返回的对象BluetoothGatt,这个BluetoothGatt对象非常重要,甚至可以说是最重要的。一般都是单独声明成全局变量来使用的,因为我们设备的读、写和订阅等操作都需要用到这个对象。

 private BluetoothGattCallback gattCallback=new BluetoothGattCallback() {/*** 断开或连接 状态发生变化时调用* */@Overridepublic void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {super.onConnectionStateChange(gatt, status, newState);Log.e(TAG,"onConnectionStateChange()");if (status==BluetoothGatt.GATT_SUCCESS){//连接成功if (newState== BluetoothGatt.STATE_CONNECTED){Log.e(TAG,"连接成功");//发现服务gatt.discoverServices();}}else{//连接失败Log.e(TAG,"失败=="+status);mBluetoothGatt.close();isConnecting=false;}}/*** 发现设备(真正建立连接)* */@Overridepublic void onServicesDiscovered(BluetoothGatt gatt, int status) {super.onServicesDiscovered(gatt, status);//直到这里才是真正建立了可通信的连接isConnecting=false;Log.e(TAG,"onServicesDiscovered()---建立连接");//获取初始化服务和特征值initServiceAndChara();//订阅通知mBluetoothGatt.setCharacteristicNotification(mBluetoothGatt.getService(notify_UUID_service).getCharacteristic(notify_UUID_chara),true);runOnUiThread(new Runnable() {@Overridepublic void run() {bleListView.setVisibility(View.GONE);operaView.setVisibility(View.VISIBLE);tvSerBindStatus.setText("已连接");}});}/*** 读操作的回调* */@Overridepublic void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {super.onCharacteristicRead(gatt, characteristic, status);Log.e(TAG,"onCharacteristicRead()");}/*** 写操作的回调* */@Overridepublic void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {super.onCharacteristicWrite(gatt, characteristic, status);Log.e(TAG,"onCharacteristicWrite()  status="+status+",value="+HexUtil.encodeHexStr(characteristic.getValue()));}/*** 接收到硬件返回的数据* */@Overridepublic void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {super.onCharacteristicChanged(gatt, characteristic);Log.e(TAG,"onCharacteristicChanged()"+characteristic.getValue());final byte[] data=characteristic.getValue();runOnUiThread(new Runnable() {@Overridepublic void run() {addText(tvResponse,bytes2hex(data));}});}};

这一段是连接的回调 这里我只重写了几个比较重要的方法,每个方法都有具体的注释,需要强调的是有些同学重复连接会报133连接失败,这个调用一下mBluetoothGatt.close()就可以解决,还有要注意的就是回调里面的方法不要做耗时的操作,也不要在回调方法里面更新UI,这样有可能会阻塞线程。

第四步:发现服务

  /*** 发现设备(真正建立连接)* */@Overridepublic void onServicesDiscovered(BluetoothGatt gatt, int status) {super.onServicesDiscovered(gatt, status);//直到这里才是真正建立了可通信的连接isConnecting=false;Log.e(TAG,"onServicesDiscovered()---建立连接");//获取初始化服务和特征值initServiceAndChara();//订阅通知mBluetoothGatt.setCharacteristicNotification(mBluetoothGatt.getService(notify_UUID_service).getCharacteristic(notify_UUID_chara),true);runOnUiThread(new Runnable() {@Overridepublic void run() {bleListView.setVisibility(View.GONE);operaView.setVisibility(View.VISIBLE);tvSerBindStatus.setText("已连接");}});}

直到这里才是建立了真正可通信的连接,下一步就可以进行读写订阅等操作,之前文章中有提到要通过Service和Characteristic特征值来操作,但是如果获取到对应的服务和特征值呢?一般硬件开发工程师会定义好UUID,通知到我们,这个时候我们只需要调用下面的方法就能拿到Service和Characteristic

//write_UUID_service和write_UUID_chara是硬件工程师告诉我们的BluetoothGattService service=mBluetoothGatt.getService(write_UUID_service);BluetoothGattCharacteristic charaWrite=service.getCharacteristic(write_UUID_chara);

当然也会比较坑爹的,就是硬件工程师居然不知道Service和Characteristic的UUID是啥(没错,我就遇见了),这个时候也不要慌,因为我们可以通过Android拿得到对应UUID.

 private void initServiceAndChara(){List<BluetoothGattService> bluetoothGattServices= mBluetoothGatt.getServices();for (BluetoothGattService bluetoothGattService:bluetoothGattServices){List<BluetoothGattCharacteristic> characteristics=bluetoothGattService.getCharacteristics();for (BluetoothGattCharacteristic characteristic:characteristics){int charaProp = characteristic.getProperties();if ((charaProp & BluetoothGattCharacteristic.PROPERTY_READ) > 0) {read_UUID_chara=characteristic.getUuid();read_UUID_service=bluetoothGattService.getUuid();Log.e(TAG,"read_chara="+read_UUID_chara+"----read_service="+read_UUID_service);}if ((charaProp & BluetoothGattCharacteristic.PROPERTY_WRITE) > 0) {write_UUID_chara=characteristic.getUuid();write_UUID_service=bluetoothGattService.getUuid();Log.e(TAG,"write_chara="+write_UUID_chara+"----write_service="+write_UUID_service);}if ((charaProp & BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE) > 0) {write_UUID_chara=characteristic.getUuid();write_UUID_service=bluetoothGattService.getUuid();Log.e(TAG,"write_chara="+write_UUID_chara+"----write_service="+write_UUID_service);}if ((charaProp & BluetoothGattCharacteristic.PROPERTY_NOTIFY) > 0) {notify_UUID_chara=characteristic.getUuid();notify_UUID_service=bluetoothGattService.getUuid();Log.e(TAG,"notify_chara="+notify_UUID_chara+"----notify_service="+notify_UUID_service);}if ((charaProp & BluetoothGattCharacteristic.PROPERTY_INDICATE) > 0) {indicate_UUID_chara=characteristic.getUuid();indicate_UUID_service=bluetoothGattService.getUuid();Log.e(TAG,"indicate_chara="+indicate_UUID_chara+"----indicate_service="+indicate_UUID_service);}}}}

BluetoothGattCharacteristic.PROPERTY_READ:对应的就是读取数据
BluetoothGattCharacteristic.PROPERTY_WRITE和BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE:都是写入,区别就是据说PROPERTY_WRITE_NO_RESPONSE写入效率更高,而NO_RESPONSE没有响应,我也没弄懂这个响应指的是什么响应,我用PROPERTY_WRITE_NO_RESPONSE写入,订阅中依然得到了回应,这里有知道的朋友可以告诉一下笔者。
PROPERTY_NOTIFY和PROPERTY_INDICATE:这里都是订阅的方法,区别就是PROPERTY_INDICATE一定能接收到订阅回调,一般用来接收一些比较重要的必须的回调,但是不能太频繁;而PROPERTY_NOTIFY不一定能百分之百接收到回调,可以频繁接收,这个一般也是使用得比较多的订阅方式。

读取数据

private void readData() {BluetoothGattCharacteristic characteristic=mBluetoothGatt.getService(read_UUID_service).getCharacteristic(read_UUID_chara);mBluetoothGatt.readCharacteristic(characteristic);}

读取数据用得比较少,我也就不重点介绍了,一般我们都是先订阅,再写入,在订阅中回调数据进行交互。

写入数据

 private void writeData(){BluetoothGattService service=mBluetoothGatt.getService(write_UUID_service);BluetoothGattCharacteristic charaWrite=service.getCharacteristic(write_UUID_chara);byte[] data=HexUtil.hexStringToBytes(hex);if (data.length>20){//数据大于个字节 分批次写入Log.e(TAG, "writeData: length="+data.length);int num=0;if (data.length%20!=0){num=data.length/20+1;}else{num=data.length/20;}for (int i=0;i<num;i++){byte[] tempArr;if (i==num-1){tempArr=new byte[data.length-i*20];System.arraycopy(data,i*20,tempArr,0,data.length-i*20);}else{tempArr=new byte[20];System.arraycopy(data,i*20,tempArr,0,20);}charaWrite.setValue(tempArr);mBluetoothGatt.writeCharacteristic(charaWrite);}}else{charaWrite.setValue(data);mBluetoothGatt.writeCharacteristic(charaWrite);}}

这里写入数据需要说一下,首先拿到写入的BluetoothGattService和BluetoothGattCharacteristic对象,把要写入的内容转成16进制的字节(蓝牙BLE规定的数据格式),然后要判断一下字节大小,如果大于20个字节就要分批次写入了,因为GATT协议规定蓝牙BLE每次传输的有效字节不能超过20个,最后通过BluetoothGattCharacteristic.setValue(data); mBluetoothGatt.writeCharacteristic(BluetoothGattCharacteristic);就可以完成写入了。写入成功了会回调onCharacteristicWrite方法

/*** 写操作的回调* */@Overridepublic void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {super.onCharacteristicWrite(gatt, characteristic, status);Log.e(TAG,"onCharacteristicWrite()  status="+status+",value="+HexUtil.encodeHexStr(characteristic.getValue()));}

订阅回调

//订阅通知mBluetoothGatt.setCharacteristicNotification(mBluetoothGatt.getService(notify_UUID_service).getCharacteristic(notify_UUID_chara),true);

注意一定要写在写入之前,要不然就收不到写入的数据,我一般都是在发现服务之后就订阅。关于订阅收不到这里,需要注意一下,首先你写入的和订阅的Characteristic对象一定要属于同一个Service对象,另外就是保证你写入的数据没问题,否则就可能收不到订阅回调。

最后上一波效果图:

写入以后返回的数据

这里在EditText虽然没有显示,但其实我直接点击默认就输入7B46363941373237323532443741397D 这一串数据,实在懒得打了

总结

第一次打这么多字有点小累,总结这个地方就不多说了,这里就说点注意事项,在进行蓝牙操作的时候最好每次都延迟200ms再执行,因为蓝牙是线程安全的,当你同时执行多次操作的时候会出现busy的情况导致执行失败,所以这里建议一般都执行一步操作延时一会,这样可以保证操作的成功率,另外就是如果大家入了门以后想要快速的开发的话,建议网上找好轮子,找一个好用的,可以先自己看看实现的源码,当然最好就是自己封装一个。
最后放上我的github地址:https://github.com/kaka10xiaobang/BlueToothBLE

蓝牙BLE(BlueTooth BLE)入门及爬坑指南相关推荐

  1. JWT 入坑爬坑指南

    点击上方蓝色"程序猿DD",选择"设为星标" 回复"资源"获取独家整理的学习资料! 来源 | 公众号「程序新世界」 越来越多的开发者开始学习 ...

  2. paddlepaddle 人脸识别爬坑指南

    综述 爬坑一天,整出来一套还行的方案,特此分享爬坑心得~ 因为整体代码结构和上篇手写数字的文章里代码结构比较相似,所以这里只贴出部分代码 网络结构 目前采用的两套模型是普通cnn以及vgg,效果不错, ...

  3. 从《编程之美》买票找零问题说起,娓娓道来卡特兰数——兼爬坑指南

    转自:从<编程之美>买票找零问题说起,娓娓道来卡特兰数--兼爬坑指南 引子: 大约两个月前,我在练习一些招聘的笔试题中,有一道和卡特兰数相关.那时还没来得及开始仔细看<编程之美> ...

  4. Insightface项目爬坑指南+使用本地数据集训练流程(MXNET版)

    其实半年多前就已经把insightface训练等一系列环节弄熟了,不得不说IBUG组的这个模型确实是开源界的翘楚,但是还是存在一些问题在某些程度上和商汤云从等大厂存在一点差距,这不妨碍大部分人日常人脸 ...

  5. 微信公众号JS-SDK多图上传爬坑指南

    一.wx.chooseImage爬坑 出现的问题: 安卓将chooseImage 方法返回的 localId 放在img标签的src属性下能够显示图片 IOS将chooseImage 方法返回的 lo ...

  6. iOS-通俗易懂的微信支付接入和爬坑指南,十分钟轻松搞完

     现在基本所有的App都会接入支付宝支付以及微信支付,也有很多第三方提供给你 SDK帮你接入,但是这种涉及到支付的东西还是自己服务器搞来的好一些,其实搞懂了 逻辑非常的简单,下面直接给大家说说下基本流 ...

  7. Nuxt(安装部署)爬坑指南

    Nuxt.js使用详细说明 这篇文章主要向大家介绍Nuxt爬坑,主要内容包括基础应用.实用技巧.原理机制等方面,希望对大家有所帮助. https://www.shangmayuan.com/a/dcd ...

  8. JPBC库(基于配对的密码学)入门和避坑指南

    视频地址:https://www.bilibili.com/video/BV1o5411Y77r/ 1. JPBC简介 Java Pairing-Based Cryptography Library ...

  9. android fragmentstatepageradapter框架,安卓爬坑指南之FragmentStatePagerAdapter

    一次开发中,用到了viewpager嵌套viewpager,结果就踩到了这么一个坑. 先上图: image.png 图片中显示的界面布局和遇到的问题是这样的:首页发现版块是一个fragment,这个f ...

最新文章

  1. html向js传递id
  2. kafka(一)-为什么使用kafka
  3. 设备的分配与调度简单方案_连铸生产调度与动态重调度的优化与研究-3,炼钢技术(3)原创...
  4. php网站服务器500,php服务器错误500
  5. ZZULIOJ 1112: 进制转换(函数专题)
  6. Java发送Post请求,参数JSON,接收JSON
  7. k8s学习: 部署动态 pvc(nfs-subdir-external-provisioner)
  8. MySQL中删除表中并不存在的数据不报错
  9. mysql客户端介绍
  10. 神坛上的插画师真的高薪且自由吗?
  11. speak 计算机英语作文,【必备】英语作文5篇
  12. 《微信小程序》音乐播放器项目
  13. 搭建分布式FastDFS集群
  14. 英语中定语和状语的区别
  15. 【夜读】做好这6件事,让人受益一生
  16. SSM框架报错分析(一)——There is no getter for property named 'XXX' in 'class java.lang.String'...
  17. 快速了解 Robot Operating System(ROS) 机器人操作系统
  18. 破解Linux操作系统root 权限不能使用问题
  19. java毕业设计校园二手商品交易系统源码+lw文档+mybatis+系统+mysql数据库+调试
  20. Linux终端命令行的常用快捷键合集!

热门文章

  1. 高仿360手机卫士——Android源码
  2. html上下箭头控制文本框加减,HTML5去掉输入框type为number时的上下箭头的实现方法...
  3. Excel 2010 VBA 入门 057 Application.OnKey为过程设置快捷键
  4. python中uppercase是什么意思_Python string.uppercase方法代码示例
  5. 快快乐乐学写字 免费
  6. 机械革命X6ti安装Ubuntu和NVIDIA的显卡驱动
  7. 阿里云实名认证常见问题分析
  8. buffalo助手函数
  9. SAR成像系列:【13】差分层析合成孔径雷达(差分层析SAR,Differential SAR Tomography ,D-TomoSAR)
  10. js合并相同元素的数组