前序
​ Google在android 4.3(API Level 18)的android版本中引入了低功耗蓝牙BLE核心API。低功耗蓝牙BLE也就是我们经常说的蓝牙4.0, 该技术拥有极低的运行和待机功耗,使用一粒纽扣电池甚至可连续工作数年之久。先不讲蓝牙协议与蓝牙模块一些类的作用与之间的关系,本章仅仅记录android Ble开发中的扫描模块及其一些细节。

一、声明蓝牙权限和定位权限

​ 由于蓝牙扫描需要用到模糊定位权限( Android10 后需要精准定位权限 ),所以android6.0之后,除了在 AndroidManifest.xml中 申明权限之外,还需要动态申请定位权限,才可进行蓝牙扫描,否则不会扫描到任何Ble设备。

可依据 PackageManager.hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE) 获知该手机是否支持BLE

二、中心设备与外围设备
Ble开发中,存在着两个角色:中心设备角色和外围设备角色。粗略了解下:

外围设备:一般指非常小或者低功耗设备,更强大的中心设备可以连接外围设备为中心设备提供数据。外设会不停的向外广播,让中心设备知道它的存在。 例如小米手环。
中心设备:可以扫描并连接多个外围设备,从外设中获取信息。
​ 外围设备会设定一个广播间隔,每个广播间隔中,都会发送自己的广播数据。广播间隔越长,越省电。一个没有被连接的Ble外设会不断发送广播数据,这时可以被多个中心设备发现。一旦外设被连接,则会马上停止广播。

​ android 4.3 时引入的Ble核心Api只支持android手机作为中心设备角色,当android 5.0 更新Api后,android手机支持充当作为外设角色和中心角色。即 android 5.0 引入了外设角色的Api,同时也更新了部分中心角色的Api。比如:中心角色中,更新了蓝牙扫描的Api。

三、打开蓝牙
//初始化ble设配器
BluetoothManager manager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
BluetoothAdapter mBluetoothAdapter = manager.getAdapter();
//判断蓝牙是否开启,如果关闭则请求打开蓝牙
if (mBluetoothAdapter == null || !mBluetoothAdapter.isEnabled()) {
//方式一:请求打开蓝牙
Intent intent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
startActivityForResult(intent, 1);
//方式二:半静默打开蓝牙
//低版本android会静默打开蓝牙,高版本android会请求打开蓝牙
//mBluetoothAdapter.enable();
}
mBluetoothAdapter.isEnabled()判断当前蓝牙是否打开,如果蓝牙处于打开状态返回true。

同时可以在activity层通过广播监听蓝牙的关闭与开启,进行自己的逻辑处理:

new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
//获取蓝牙广播 本地蓝牙适配器的状态改变时触发
String action = intent.getAction();
if (action.equals(BluetoothAdapter.ACTION_STATE_CHANGED)) {
//获取蓝牙广播中的蓝牙新状态
int blueNewState = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, 0);
//获取蓝牙广播中的蓝牙旧状态
int blueOldState = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, 0);
switch (blueNewState) {
//正在打开蓝牙
case BluetoothAdapter.STATE_TURNING_ON:
break;
//蓝牙已打开
case BluetoothAdapter.STATE_ON:
break;
//正在关闭蓝牙
case BluetoothAdapter.STATE_TURNING_OFF:
break;
//蓝牙已关闭
case BluetoothAdapter.STATE_OFF:
break;
}
}
}
};
四、扫描
android 4.3 扫描
在android 4.3 和 android 4.4进行蓝牙扫描,可使用BluetoothAdapter.startLeScan(BluetoothAdapter.LeScanCallback)进行蓝牙扫描。

//开始扫描
mBluetoothAdapter.startLeScan(mLeScanCallback);
//停止扫描
mBluetoothAdapter.stopLeScan(mLeScanCallback);
android 5.0以上 扫描
在 android 5.0之后的版本(包括 5.0)建议使用新的Api进行蓝牙扫描:

BluetoothLeScanner.startScan(ScanCallback)
BluetoothLeScanner.startScan(List, ScanSettings, ScanCallback)。
//获取 5.0 的扫描类实例
mBLEScanner = mBluetoothAdapter.getBluetoothLeScanner();
//开始扫描
//可设置过滤条件,在第一个参数传入,但一般不设置过滤。
mBLEScanner.startScan(null,mScanSettings,mScanCallback);
//停止扫描
mBLEScanner.stopScan(mScanCallback);
蓝牙扫描示例
//如果没打开蓝牙,不进行扫描操作,或请求打开蓝牙。
if(!mBluetoothAdapter.isEnabled()) {
return;
}
//处于未扫描的状态
if (!mScanning){
//android 5.0后
if(android.os.Build.VERSION.SDK_INT >= 21) {
//标记当前的为扫描状态
mScanning = true;
//获取5.0新添的扫描类
if (mBLEScanner == null){
//mBLEScanner是5.0新添加的扫描类,通过BluetoothAdapter实例获取。
mBLEScanner = mBluetoothAdapter.getBluetoothLeScanner();
}
//开始扫描
//mScanSettings是ScanSettings实例,mScanCallback是ScanCallback实例,后面进行讲解。
mBLEScanner.startScan(null,mScanSettings,mScanCallback);
} else {
//标记当前的为扫描状态
mScanning = true;
//5.0以下 开始扫描
//mLeScanCallback是BluetoothAdapter.LeScanCallback实例
mBluetoothAdapter.startLeScan(mLeScanCallback);
}
//设置结束扫描
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
//停止扫描设备
if(android.os.Build.VERSION.SDK_INT >= 21) {
//标记当前的为未扫描状态
mScanning = false;
mBLEScanner.stopScan(mScanCallback);
} else {
//标记当前的为未扫描状态
mScanning = false;
//5.0以下 停止扫描
mBluetoothAdapter.stopLeScan(mLeScanCallback);
}
}
},SCAN_TIME);
}
扫描代码如上述所示,当扫描到所需要的设备的时候,就要手动马上停止蓝牙扫描,因为蓝牙扫描是耗电操作。

注意事项
android 6.0 以上需要获取到定位权限。否则会报如下运行时异常:
image
android 7.0 后不能在30秒内扫描+停止超过5次。(官网没特意说明,可自行测试,设置扫描时长为3秒,连续扫描10次,稳定复现5次后不能扫描到任何设备。android 蓝牙模块会打印当前应用扫描太频繁的log日志,并在android 5.0 的ScanCallback回调中触发onScanFailed(int),返回错误码:ScanCallback.SCAN_FAILED_APPLICATION_REGISTRATION_FAILED,表示app无法注册,无法开始扫描)。
image
五、扫描回调
android 4.3 扫描回调:LeScanCallback
android 4.3 的扫描回调接口BluetoothAdapter.LeScanCallback:
image

回调接口中只有一个回调函数onLeScan,扫描到的设备会通过该方法返回。
参数:
BluetoothDevice 扫描到的设备实例,可从实例中获取到相应的信息。如:名称,mac地址
rssi 可理解成设备的信号值。该数值是一个负数,越大则信号越强。
scanRecord 远程设备提供的广播数据的内容。
//5.0以下
mLeScanCallback = new BluetoothAdapter.LeScanCallback() {
@Override
public void onLeScan(BluetoothDevice device, int rssi, byte[] scanRecord) {
//对扫描到的设备进行操作。如:获取设备信息。

}

};
获取BluetoothDevice中的信息
image
可以从中获取到设备的mac地址,设备名称,绑定状态和设备类型等信息,并作相应的保存。

mac可用于再创建BluetoothDevice对象进行gatt连接。

绑定状态:

BOND_NONE:数值 10
表示远程设备未绑定,没有共享链接密钥,因此通信(如果允许的话)将是未经身份验证和未加密的。(扫描到未绑定的小米手环)
BOND_BONDING:数值 11 表示正在与远程设备进行绑定;
BOND_BONDED:数值 12 表示远程设备已绑定,远程设备本地存储共享连接的密钥,因此可以对通信进行身份验证和加密。(扫描到已绑定的小米手环)
设备类型:一般是2,表示LE设备

android 5.0 扫描回调:ScanCallback
mScanCallback = new ScanCallback() {
//当一个蓝牙ble广播被发现时回调
@Override
public void onScanResult(int callbackType, ScanResult result) {
super.onScanResult(callbackType, result);
//扫描类型有开始扫描时传入的ScanSettings相关
//对扫描到的设备进行操作。如:获取设备信息。

}//批量返回扫描结果
//@param results 以前扫描到的扫描结果列表。
@Override
public void onBatchScanResults(List<ScanResult> results) {super.onBatchScanResults(results);}//当扫描不能开启时回调
@Override
public void onScanFailed(int errorCode) {super.onScanFailed(errorCode);//扫描太频繁会返回ScanCallback.SCAN_FAILED_APPLICATION_REGISTRATION_FAILED,表示app无法注册,无法开始扫描。}

};
ScanCallback扫描回调存在三个回调函数:

onScanResult(int,ScanResult):类似于BluetoothAdapter.LeScanCallback中的onLeScan(),可在ScanResult实例中获取到BluetoothDevice蓝牙设备对象,rssi信号值等信息。一般都是在该函数中回调获取扫描到蓝牙设备和信号值,在本函数中执行和onLeScan()中相同的逻辑处理即可。
onBatchScanResults(List) 批量返回扫描结果。
onScanFailed(int) 扫描失败返回错误码。
注意事项
回调函数中尽量不要做耗时操作!
一般蓝牙设备对象都是通过onScanResult(int,ScanResult)返回,而不会在onBatchScanResults(List)方法中返回,除非手机支持批量扫描模式并且开启了批量扫描模式。批处理的开启请查看ScanSettings。
六、扫描设置
​ ScanSettings实例对象是通过ScanSettings.Builder构建的。通过Builder对象为ScanSettings实例设置扫描模式、回调类型、匹配模式等参数,用于配置android 5.0 的扫描参数。

//创建ScanSettings的build对象用于设置参数
ScanSettings.Builder builder = new ScanSettings.Builder()
//设置高功耗模式
.setScanMode(SCAN_MODE_LOW_LATENCY);
//android 6.0添加设置回调类型、匹配模式等
if(android.os.Build.VERSION.SDK_INT >= 23) {
//定义回调类型
builder.setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES)
//设置蓝牙LE扫描滤波器硬件匹配的匹配模式
builder.setMatchMode(ScanSettings.MATCH_MODE_STICKY);
}
//芯片组支持批处理芯片上的扫描
if (bluetoothadapter.isOffloadedScanBatchingSupported()) {
//设置蓝牙LE扫描的报告延迟的时间(以毫秒为单位)
//设置为0以立即通知结果
builder.setReportDelay(0L);
}
builder.build();
配置描述:

setScanMode() 设置扫描模式。可选择模式主要三种( 从上到下,会越来越耗电,但扫描间隔越来越短,即扫描速度会越来越快。):
ScanSettings.SCAN_MODE_LOW_POWER 低功耗模式(默认扫描模式,如果扫描应用程序不在前台,则强制使用此模式。)
ScanSettings.SCAN_MODE_BALANCED 平衡模式
ScanSettings.SCAN_MODE_LOW_LATENCY 高功耗模式(建议仅在应用程序在前台运行时才使用此模式。)
setCallbackType() 设置回调类型。可选择模式主要三种:
ScanSettings.CALLBACK_TYPE_ALL_MATCHES 数值: 1。

寻找符合过滤条件的蓝牙广播,如果没有设置过滤条件,则返回全部广播包

ScanSettings.CALLBACK_TYPE_FIRST_MATCH 数值: 2

仅针对与筛选条件匹配的第一个广播包触发结果回调。

ScanSettings.CALLBACK_TYPE_MATCH_LOST 数值: 4

回调类型一般设置ScanSettings.CALLBACK_TYPE_ALL_MATCHES,有过滤条件时过滤,返回符合过滤条件的蓝牙广播。无过滤条件时,返回全部蓝牙广播。

setMatchMode() 设置蓝牙LE扫描滤波器硬件匹配的匹配模式
ScanSettings.MATCH_MODE_STICKY 粘性模式,在通过硬件报告之前,需要更高的信号强度和目击阈值
MATCH_MODE_AGGRESSIVE 激进模式,即使信号强度微弱且持续时间内瞄准/匹配的次数很少,hw也会更快地确定匹配。
Bluetoothadapter.isOffloadedScanBatchingSupported() 判断当前手机蓝牙芯片是否支持批处理扫描。
如果支持扫描则使用批处理扫描,可通过ScanSettings.Builder对象调用setReportDelay(Long)方法来设置蓝牙LE扫描的报告延迟的时间(以毫秒为单位)来启动批处理扫描模式。
ScanSettings.Builder.setReportDelay(Long);
当设备蓝牙芯片支持批处理扫描时,用来设置蓝牙LE扫描的报告延迟的时间(以毫秒为单位)。
该参数默认为 0,如果不修改它的值,则默认只会在onScanResult(int,ScanResult)中返回扫描到的蓝牙设备,不会触发不会触发onBatchScanResults(List)方法。(onScanResult(int,ScanResult) 和 onBatchScanResults(List) 是互斥的。 )
设置为0以立即通知结果,不开启批处理扫描模式。即ScanCallback蓝牙回调中,不会触发onBatchScanResults(List)方法,但会触发onScanResult(int,ScanResult)方法,返回扫描到的蓝牙设备。
当设置的时间大于0L时,则会开启批处理扫描模式。即触发onBatchScanResults(List)方法,返回扫描到的蓝牙设备列表。但不会触发onScanResult(int,ScanResult)方法。
提示
Android 10 进行BLE扫描时需要打开GPS。

具体源码可参考Ble实战BleScanHelper.kt

Android 8更新了一个扫描API,系统层为你提供后台持续扫描的能力。(即便APP已被杀死,扫描仍会继续。但如果用户重启或关闭蓝牙后,该扫描停止)。 具体可查看官网: BluetoothLeScanner#startScan。

public int startScan (List filters,
ScanSettings settings,
PendingIntent callbackIntent)

作者:大棋17
链接:https://www.jianshu.com/p/2dba7f067372
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

一、蓝牙基础协议
想了解蓝牙通信之前,需要先了解蓝牙两个最基本的协议:GAP 和 GATT。

GAP(Generic Access Profile)简介
​ GAP是通用访问配置文件的首字母缩写,主要控制蓝牙连接和广播。GAP使蓝牙设备对外界可见,并决定设备是否可以或者怎样与其他设备进行交互。

GAP定义了多种角色,但主要的两个是:中心设备 和 外围设备。

中心设备:可以扫描并连接多个外围设备,从外设中获取信息。

外围设备:小型,低功耗,资源有限的设备。可以连接到功能更强大的中心设备,并为其提供数据。

GAP广播数据
GAP 中外围设备通过两种方式向外广播数据:广播数据 和 扫描回复( 每种数据最长可以包含 31 byte。)。

​ 广播数据是必需的,因为外设必需不停的向外广播,让中心设备知道它的存在。而扫描回复是可选的,中心设备可以向外设请求扫描回复,这里包含一些设备额外的信息。

image

外围设备会设定一个广播间隔。每个广播间隔中,它会重新发送自己的广播数据。广播间隔越长,越省电,同时也不太容易扫描到。

广播的网络拓扑结构
​ 外设通过广播自己让中心设备发现自己,并建立 GATT 连接,从而进行更多的数据交换。但有些情况是不需要连接的,只要外设广播自己的数据即可。目的是让外围设备,把自己的信息发送给多个中心设备。因为基于 GATT 连接的方式的,只能是一个外设连接一个中心设备。

image
GATT(Generic Attribute Profile)简介
​ GATT配置文件是一个通用规范,用于在BLE链路上发送和接收被称为“属性”的数据块。目前所有的BLE应用都基于GATT。

BLE设备通过叫做 Service 和 Characteristic 的东西进行通信

​ GATT使用了 ATT(Attribute Protocol)协议,ATT 协议把 Service, Characteristic对应的数据保存在一个查询表中,次查找表使用 16 bit ID 作为每一项的索引。

​ GATT 连接是独占的。也就是一个 BLE 外设同时只能被一个中心设备连接。一旦外设被连接,它就会马上停止广播,这样它就对其他设备不可见了。当外设与中心设备断开,外设又开始广播,让其他中心设备感知该外设的存在。而中心设备可同时与多个外设进行连接。

image
GATT 通信
中心设备和外设需要双向通信的话,唯一的方式就是建立 GATT 连接。

​ GATT 通信的双方是 C/S 关系。外设作为 GATT 服务端(Server),它维持了 ATT 的查找表以及 service 和 characteristic 的定义。中心设备是 GATT 客户端(Client),它向 外设(Server) 发起请求来获取数据。

GATT 结构
image
Profile:并不是实际存在于 BLE 外设上的,它只是一个被 Bluetooth SIG 或者外设设计者预先定义的 Service 的集合。例如心率Profile(Heart Rate Profile)就是结合了 Heart Rate Service 和 Device Information Service。

Service:包含一个或者多个 Characteristic。每个 Service 有一个 UUID 唯一标识。

Characteristic: 是最小的逻辑数据单元。一个Characteristic包括一个单一value变量和0-n个用来描述characteristic变量的Descriptor。与 Service 类似,每个 Characteristic 用 16 bit 或者 128 bit 的 UUID 唯一标识。

实际开发中,和 BLE 外设打交道,主要是通过 Characteristic。可以从 Characteristic 读取数据,也可以往 Characteristic 写数据,从而实现双向的通信。

UUID 有 16 bit 、32bit 和 128 bit 的。16 bit 的 UUID 是官方通过认证的,需要花钱购买。

Bluetooth_Base_UUID定义为 00000000-0000-1000-8000-00805F9B34FB

若16 bit UUID为xxxx,转换为128 bit UUID为0000xxxx-0000-1000-8000-00805F9B34FB
若32 bit UUID为xxxxxxxx,转换为128 bit UUID为xxxxxxxx-0000-1000-8000-00805F9B34FB
二、中心设备与外设通讯
简单介绍BLE开发当中各种主要类和其作用:

BluetoothDeivce:蓝牙设备,代表一个具体的蓝牙外设。
BluetoothGatt:通用属性协议,定义了BLE通讯的基本规则和操作
BluetoothGattCallback:GATT通信回调类,用于回调的各种状态和结果。
BluetoothGattService:服务,由零或多个特征组构成。
BluetoothGattCharacteristic:特征,里面包含了一组或多组数据,是GATT通信中的最小数据单元。
BluetoothGattDescriptor:特征描述符,对特征的额外描述,包括但不仅限于特征的单位,属性等。
获取蓝牙设备对象
对扫描到的蓝牙可以用集合形式进行缓存,也可只保存其mac地址,存储到字符集合中,用于后续的连接。

根据mac地址获取到BluetoothDeivce用于连接

BluetoothManager bluetoothmanager = (BluetoothManager)context.getSystemService(Context.BLUETOOTH_SERVICE);
mBluetoothAdapter = bluetoothmanager.getAdapter();
//获取蓝牙设备对象进行连接
mBluetoothDevice = mBluetoothAdapter.getRemoteDevice(macAddressStr)
蓝牙gatt回调
实现BluetoothGattCallBack类,监听蓝牙连接过程中各种回调的监听。蓝牙Gatt回调方法中都不应该进行耗时操作,需要将其方法内进行的操作丢进另一个线程,尽快返回。

//定义子线程handle,用于在BluetoothGattCallback中回调方法中的操作抛到该线程工作。
private Handler mHandler;
//定义handler工作的子线程
private HandlerThread mHandlerThread;

初始化handler
mHandlerThread = new HandlerThread(“daqi”);
mHandlerThread.start();
//将handler绑定到子线程中
mHandler = new Handler(mHandlerThread.getLooper());

//定义蓝牙Gatt回调类
public class daqiBluetoothGattCallback extends BluetoothGattCallback{
//连接状态回调
@Override
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
super.onConnectionStateChange(gatt, status, newState);
// status 用于返回操作是否成功,会返回异常码。
// newState 返回连接状态,如BluetoothProfile#STATE_DISCONNECTED、BluetoothProfile#STATE_CONNECTED

        //操作成功的情况下if (status == BluetoothGatt.GATT_SUCCESS){//判断是否连接码if (newState == BluetoothProfile.STATE_CONNECTED) {}else if(newState == BluetoothProfile.STATE_DISCONNECTED){//判断是否断开连接码}}else{//异常码}}//服务发现回调@Overridepublic void onServicesDiscovered(BluetoothGatt gatt, int status) {super.onServicesDiscovered(gatt, status);}//特征写入回调@Overridepublic void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {super.onCharacteristicWrite(gatt, characteristic, status);}//外设特征值改变回调@Overridepublic void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {super.onCharacteristicChanged(gatt, characteristic);}//描述写入回调@Overridepublic void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {super.onDescriptorWrite(gatt, descriptor, status);}
}

连接设备
​ 调用BluetoothDevice#connectGatt()进行ble连接,第二个参数默认选择false,不自动连接。并定义BluetoothGatt变量,存储BluetoothDevice#connectGatt()返回的对象。
image
//定义Gatt实现类
private BluetoothGatt mBluetoothGatt;

//创建Gatt回调
private BluetoothGattCallback mGattCallback = new daqiBluetoothGattCallback();
//连接设备
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
mBluetoothGatt = mBluetoothDevice.connectGatt(mContext,
false, mGattCallback, BluetoothDevice.TRANSPORT_LE);
} else {
mBluetoothGatt = mBluetoothDevice.connectGatt(mContext, false, mGattCallback);
}
连接异常处理
​ 蓝牙连接时,不一定百分百连接成功。连接出错时,会返回异常码进行错误描述。对于大多数异常码,可以通过重连来达到连接成功的目的。

错误代码:

133 :连接超时或未找到设备。
8 : 设备超出范围
22 :表示本地设备终止了连接
//定义重连次数
private int reConnectionNum = 0;
//最多重连次数
private int maxConnectionNum = 3;

public class daqiBluetoothGattCallback extends BluetoothGattCallback{

//连接状态回调
@Override
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {super.onConnectionStateChange(gatt, status, newState);// status 用于返回操作是否成功,会返回异常码。//操作成功的情况下if (status == BluetoothGatt.GATT_SUCCESS){}else{//重连次数不大于最大重连次数if(reConnectionNum < maxConnectionNum){//重连次数自增reConnectionNum++//连接设备if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {mBluetoothGatt = mBluetoothDevice.connectGatt(mContext,false, mGattCallback, BluetoothDevice.TRANSPORT_LE);} else {mBluetoothGatt = mBluetoothDevice.connectGatt(mContext, false, mGattCallback);}}else{//断开连接,返回连接失败回调}}
}//其他回调方法


发现服务
连接成功后,触发BluetoothGattCallback#onConnectionStateChange()方法。

public class daqiBluetoothGattCallback extends BluetoothGattCallback{

//连接状态回调
@Override
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {super.onConnectionStateChange(gatt, status, newState);// status 用于返回操作是否成功,会返回异常码。//操作成功的情况下if (status == BluetoothGatt.GATT_SUCCESS){//判断是否连接码if (newState == BluetoothProfile.STATE_CONNECTED) {//可延迟发现服务,也可不延迟mHandler.post(() ->//发现服务mBluetoothGatt.discoverServices(););}else if(newState == BluetoothProfile.STATE_DISCONNECTED){//判断是否断开连接码}}
}//其他回调方法


当发现服务成功后,会触发BluetoothGattCallback#onServicesDiscovered()回调:

//定义需要进行通信的ServiceUUID
private UUID mServiceUUID = UUID.fromString(“0000xxxx-0000-1000-8000-00805f9b34fb”);

//定义蓝牙Gatt回调类
public class daqiBluetoothGattCallback extends BluetoothGattCallback{

//服务发现回调
@Override
public void onServicesDiscovered(BluetoothGatt gatt, int status) {super.onServicesDiscovered(gatt, status);if (status == BluetoothGatt.GATT_SUCCESS) {mHandler.post(() ->//获取指定uuid的serviceBluetoothGattService gattService = mBluetoothGatt.getService(mServiceUUID);//获取到特定的服务不为空if(gattService != null){}else{//获取特定服务失败});}
}


发现服务失败
​ 发现服务时,会存在发现不了特定服务的情况。或者说,整个BluetoothGatt对象中的服务列表为空。
BluetoothGatt类中存在一个隐藏的方法refresh(),用于刷新Gatt的服务列表。当发现不了服务时,可以通过反射去调用该方法,再发现一遍服务。

读取和修改特征值
//定义需要进行通信的ServiceUUID
private UUID mServiceUUID = UUID.fromString(“0000xxxx-0000-1000-8000-00805f9b34fb”);
//定义需要进行通信的CharacteristicUUID
private UUID mCharacteristicUUID = UUID.fromString(“0000yyyy-0000-1000-8000-00805f9b34fb”);

//定义蓝牙Gatt回调类
public class daqiBluetoothGattCallback extends BluetoothGattCallback{

//服务发现回调
@Override
public void onServicesDiscovered(BluetoothGatt gatt, int status) {super.onServicesDiscovered(gatt, status);if (status == BluetoothGatt.GATT_SUCCESS) {mHandler.post(() ->//获取指定uuid的serviceBluetoothGattService gattService = mBluetoothGatt.getService(mServiceUUID);//获取到特定的服务不为空if(gattService != null){//获取指定uuid的CharacteristicBluetoothGattCharacteristic gattCharacteristic = gattService.getCharacteristic(mCharacteristicUUID);//获取特定特征成功if(gattCharacteristic != null){//写入你需要传递给外设的特征值(即传递给外设的信息)gattCharacteristic.setValue(bytes);//通过GATt实体类将,特征值写入到外设中。mBluetoothGatt.writeCharacteristic(gattCharacteristic);//如果只是需要读取外设的特征值://通过Gatt对象读取特定特征(Characteristic)的特征值mBluetoothGatt.readCharacteristic(gattCharacteristic);}}else{//获取特定服务失败});}
}


当成功读取特征值时,会触发BluetoothGattCallback#onCharacteristicRead()回调。

@Override
public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
super.onCharacteristicRead(gatt, characteristic, status);
if (status == BluetoothGatt.GATT_SUCCESS) {
//获取读取到的特征值
characteristic.getValue()

}
当成功写入特征值到外设时,会触发BluetoothGattCallback#onCharacteristicWrite()回调。

@Override
public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
super.onCharacteristicWrite(gatt, characteristic, status);
if (status == BluetoothGatt.GATT_SUCCESS) {
//获取写入到外设的特征值
characteristic.getValue()

}
监听外设特征值改变
​ 无论是对外设写入新值,还是读取外设特定Characteristic的值,其实都只是单方通信。如果需要双向通信,可以在BluetoothGattCallback#onServicesDiscovered中对某个特征值设置监听(前提是该Characteristic具有NOTIFY属性):

//设置订阅notificationGattCharacteristic值改变的通知
mBluetoothGatt.setCharacteristicNotification(notificationGattCharacteristic, true);
//获取其对应的通知Descriptor
BluetoothGattDescriptor descriptor = notificationGattCharacteristic.getDescriptor(UUID.fromString(“00002902-0000-1000-8000-00805f9b34fb”));
if (descriptor != null){
//设置通知值
descriptor.setValue(BluetoothGattDescriptor.ENABLE_INDICATION_VALUE);
boolean descriptorResult = mBluetoothGatt.writeDescriptor(descriptor);
}
​ 当写入完特征值后,外设修改自己的特征值进行回复时,手机端会触发BluetoothGattCallback#onCharacteristicChanged()方法,获取到外设回复的值,从而实现双向通信。

@Override
public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
if (status == BluetoothGatt.GATT_SUCCESS) {
//获取外设修改的特征值
String value = characteristic.getValue()
//对特征值进行解析


断开连接
断开连接的操作分为两步:

mBluetoothGatt.disconnect();
mBluetoothGatt.close();
​ 调用disconnect()后,会触发手机会触发BluetoothGattCallback#onConnectionStateChange()的回调,回调断开连接信息,newState = BluetoothProfile.STATE_DISCONNECTED。但调用完disconnect()紧接着马上调用close(),会终止BluetoothGattCallback#onConnectionStateChange()的回调。可以看情况将两个进行拆分调用,来实现断开连接,但必须两个方法都调用。

例如:
需要在外设修改特征值触发BluetoothGattCallback#onCharacteristicChanged()时,断开连接。可以先在BluetoothGattCallback#onCharacteristicChanged()中调用disconnect(),并等调用BluetoothGattCallback#onConnectionStateChange()回调,返回断开连接信息后,再调用close()对Gatt资源进行关闭。

当和外设进行ble通信时,如出现任何意外情况,马上调用断开连接操作。

作者:大棋17
链接:https://www.jianshu.com/p/d273e46f47b1
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

android蓝牙BLE(三) —— 广播

大棋17
1
2019.05.22 16:20:54
字数 2,699
阅读 9,561
​ 在蓝牙开发中,有些情况是不需要连接的,只要外设广播自己的数据即可,例如苹果的ibeacon。自Android 5.0更新蓝牙API后,手机可以作为外设广播数据。

广播包有两种:

广播包(Advertising Data)
响应包(Scan Response)
其中广播包是每个外设都必须广播的,而响应包是可选的。每个广播包的长度必须是31个字节,如果不到31个字节 ,则剩下的全用0填充 补全,这部分的数据是无效的
image
广播数据单元
广播包中包含若干个广播数据单元,广播数据单元也称为 AD Structure。

广播数据单元 = 长度值Length + AD type + AD Data。

长度值Length只占一个字节,并且位于广播数据单元的第一个字节。

概念的东西有些抽象,先看看下面的广播报文:

image
​ 0x代表这串字符串是十六进制的字符串。两位十六进制数代表一个字节。因为两个字符组成的十六进制字符串最大为FF,即255,而Java中byte类型的取值范围是-128到127,刚好可以表示一个255的大小。所以两个十六进制的字符串表示一个字节。

​ 继续查看报文内容,开始读取第一个广播数据单元。读取第一个字节:0x07,转换为十进制就是7,即表示后面的7个字节是这个广播数据单元的数据内容。超过这7个字节的数据内容后,表示是一个新的广播数据单元。

​ 而第二个广播数据单元,第一个字节的值是0x16,转换为十进制就是22,表示后面22个字节为第二个广播数据单元。

​ 在广播数据单元的数据部分中,第一个字节代表数据类型(AD type),决定数据部分表示的是什么数据。(即广播数据单元第二个字节为AD type)

image
AD Type的类型如下:

Flags:TYPE = 0x01。用来标识设备LE物理连接。
bit 0: LE 有限发现模式
bit 1: LE 普通发现模式
bit 2: 不支持 BR/EDR
bit 3: 对 Same Device Capable(Controller) 同时支持 BLE 和 BR/EDR
bit 4: 对 Same Device Capable(Host) 同时支持 BLE 和 BR/EDR
bit 5…7: 预留
​ 这bit 1~7分别代表着发送该广播的蓝牙芯片的物理连接状态。当bit的值为1时,表示支持该功能。
例:

image
Service UUID。广播数据中可以将设备支持的GATT Service的UUID广播出来,来告知中心设备其支持的Service。对于不同bit的UUID,其对应的类型也有不同:

非完整的16bit UUID: TYPE = 0x02;
完整的16bit UUID 列表: TYPE = 0x03;
非完整的32bit UUID 列表: TYPE = 0x04;
完整的32bit UUID 列表: TYPE = 0x05;
非完整的128bit UUID 列表: TYPE = 0x06;
完整的128bit UUID: TYPE = 0x07;
TX Power Level: TYPE = 0x0A,表示设备发送广播包的信号强度。 数值范围:±127 dBm。

设备名字,DATA 是名字的字符串,可以是设备的全名,也可以是设备名字的缩写。

缩写的设备名称: TYPE = 0x08
完整的设备名称: TYPE = 0x09
Service Data: Service 对应的数据。

16 bit UUID Service: TYPE = 0x16, 前 2 字节是 UUID,后面是 Service 的数据;
32 bit UUID Service: TYPE = 0x20, 前 4 字节是 UUID,后面是 Service 的数据;
128 bit UUID Service: TYPE = 0x21, 前 16 字节是 UUID,后面是 Service 的数据;
厂商自定义数据: TYPE = 0xFF。厂商数据中,前两个字节表示厂商ID,剩下的是厂商自定义的数据。

BLE广播
蓝牙广播的数据格式大致讲了一下,有助于下面的广播操作的理解。

自定义UUID:
//UUID
public static UUID UUID_SERVICE = UUID.fromString(“0000fff7-0000-1000-8000-00805f9b34fb”);
开启广播一般需要3~4对象:广播设置(AdvertiseSettings)、广播包(AdvertiseData)、扫描包(可选)、广播回调(AdvertiseCallback)。

广播设置
先看看广播设置(AdvertiseSettings)如何定义:

//初始化广播设置
mAdvertiseSettings = new AdvertiseSettings.Builder()
//设置广播模式,以控制广播的功率和延迟。
.setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_LOW_POWER)
//发射功率级别
.setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_HIGH)
//不得超过180000毫秒。值为0将禁用时间限制。
.setTimeout(3000)
//设置是否可以连接
.setConnectable(false)
.build();
(1)、通过AdvertiseSettings.Builder#setAdvertiseMode() 设置广播模式。其中有3种模式:

在均衡电源模式下执行蓝牙LE广播:AdvertiseSettings#ADVERTISE_MODE_BALANCED
在低延迟,高功率模式下执行蓝牙LE广播: AdvertiseSettings#ADVERTISE_MODE_LOW_LATENCY
在低功耗模式下执行蓝牙LE广播:AdvertiseSettings#ADVERTISE_MODE_LOW_POWER
(2)、通过AdvertiseSettings.Builder#setAdvertiseMode() 设置广播发射功率。共有4种功率模式:

使用高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
(3)、通过AdvertiseSettings.Builder#setTimeout()设置持续广播的时间,单位为毫秒。最多180000毫秒。当值为0则无时间限制,持续广播,除非调用BluetoothLeAdvertiser#stopAdvertising()停止广播。

(4)、通过AdvertiseSettings.Builder#setConnectable()设置该广播是否可以连接的。

广播包
之前说过,外设必须广播广播包,扫描包是可选。但添加扫描包也意味着广播更多得数据,即可广播62个字节。

//初始化广播包
mAdvertiseData = new AdvertiseData.Builder()
//设置广播设备名称
.setIncludeDeviceName(true)
//设置发射功率级别
.setIncludeDeviceName(true)
.build();

//初始化扫描响应包
mScanResponseData = new AdvertiseData.Builder()
//隐藏广播设备名称
.setIncludeDeviceName(false)
//隐藏发射功率级别
.setIncludeDeviceName(false)
//设置广播的服务UUID
.addServiceUUID(new ParcelUUID(UUID_SERVICE))
//设置厂商数据
.addManufacturerData(0x11,hexStrToByte(mData))
.build();
可见无论是广播包还是扫描包,其广播的内容都是用AdvertiseData类封装的。

(1)、AdvertiseData.Builder#setIncludeDeviceName()方法,可以设置广播包中是否包含蓝牙的名称。

(2)、AdvertiseData.Builder#setIncludeTxPowerLevel()方法,可以设置广播包中是否包含蓝牙的发射功率。

(3)、AdvertiseData.Builder#addServiceUUID(ParcelUUID)方法,可以设置特定的UUID在广播包中。

(4)、AdvertiseData.Builder#addServiceData(ParcelUUID,byte[])方法,可以设置特定的UUID和其数据在广播包中。

(5)、AdvertiseData.Builder#addManufacturerData(int,byte[])方法,可以设置特定厂商Id和其数据在广播包中。

​ 从AdvertiseData.Builder的设置中可以看出,如果一个外设需要在不连接的情况下对外广播数据,其数据可以存储在UUID对应的数据中,也可以存储在厂商数据中。但由于厂商ID是需要由Bluetooth SIG进行分配的,厂商间一般都将数据设置在厂商数据。

广播名称
另外可以通过BluetoothAdapter#setName()设置广播的名称

//获取蓝牙设配器
BluetoothManager bluetoothManager = (BluetoothManager)
getSystemService(Context.BLUETOOTH_SERVICE);
mBluetoothAdapter = bluetoothManager.getAdapter();
//设置设备蓝牙名称
mBluetoothAdapter.setName(“daqi”);
发送BLE广播
先看一个例子,我们分别在广播包和扫描包中设置AdvertiseData.Builder的每一种广播报文参数,得到一下报文内容:
image
(1)、Type = 0x01 表示设备LE物理连接。

(2)、Type = 0x09 表示设备的全名

(3)、Type = 0x03 表示完整的16bit UUID。其值为0xFFF7。

(4)、Type = 0xFF 表示厂商数据。前两个字节表示厂商ID,即厂商ID为0x11。后面的为厂商数据,具体由用户自行定义。

(5)、Type = 0x16 表示16 bit UUID的数据,所以前两个字节为UUID,即UUID为0xF117,后续为UUID对应的数据,具体由用户自行定义。

最后继承AdvertiseCallback自定义广播回调。

private class daqiAdvertiseCallback extends AdvertiseCallback {
//开启广播成功回调
@Override
public void onStartSuccess(AdvertiseSettings settingsInEffect){
super.onStartSuccess(settingsInEffect);
Log.d(“daqi”,“开启服务成功”);
}

//无法启动广播回调。
@Override
public void onStartFailure(int errorCode) {super.onStartFailure(errorCode);Log.d("daqi","开启服务失败,失败码 = " + errorCode);
}

}
初始化完毕上面的对象后,就可以进行广播:

//获取BLE广播的操作对象。
//如果蓝牙关闭或此设备不支持蓝牙LE广播,则返回null。
mBluetoothLeAdvertiser = mBluetoothAdapter.getBluetoothLeAdvertiser();
//mBluetoothLeAdvertiser不为空,且蓝牙已开打
if(mBluetoothAdapter.isEnabled()){
if (mBluetoothLeAdvertiser != null){
//开启广播
mBluetoothLeAdvertiser.startAdvertising(mAdvertiseSettings,
mAdvertiseData, mScanResponseData, mAdvertiseCallback);
}else {
Log.d(“daqi”,“该手机不支持ble广播”);
}
}else{
Log.d(“daqi”,“手机蓝牙未开启”);
}
​ 广播主要是通过BluetoothLeAdvertiser#startAdvertising()方法实现,但在之前需要先获取BluetoothLeAdvertiser对象。

BluetoothLeAdvertiser对象存在两个情况获取为Null:

手机蓝牙模块不支持BLE广播
蓝牙未开启
所以在调用BluetoothAdapter#getBluetoothLeAdvertiser()前,需要先调用判断蓝牙已开启,并判断在BluetoothAdapter中获取的BluetoothLeAdvertiser是否为空(测试过某些华为手机mBluetoothAdapter.isMultipleAdvertisementSupported()为 false, 但是能发送ble广播)。

​ 与广播成对出现就是BluetoothLeAdvertiser.stopAdvertising()停止广播了,传入开启广播时传递的广播回调对象,即可关闭广播:

mBluetoothLeAdvertiser.stopAdvertising(mAdvertiseCallback)
启动GATT Service 和 Characteristic
​ 虽然通过广播告知外边自身拥有这些Service,但手机自身并没有初始化Gattd的Service。导致外部的中心设备连接手机后,并不能找到对应的GATT Service 和 获取对应的数据。

创建Gatt Service
Service类型有两个级别:

BluetoothGattService#SERVICE_TYPE_PRIMARY 主服务
BluetoothGattService#SERVICE_TYPE_SECONDARY次要服务(存在于主服务中的服务)
创建BluetoothGattService时,传入两个参数:UUID和Service类型:

BluetoothGattService service = new BluetoothGattService(UUID_SERVICE,
BluetoothGattService.SERVICE_TYPE_PRIMARY);
创建Gatt Characteristic
​ 我们都知道Gatt中,Service的下一级是Characteristic,Characteristic是最小的通信单元,通过对Characteristic进行读写操作来进行通信。

//初始化特征值
mGattCharacteristic = new BluetoothGattCharacteristic(UUID_CHARACTERISTIC,
BluetoothGattCharacteristic.PROPERTY_WRITE|
BluetoothGattCharacteristic.PROPERTY_NOTIFY|
BluetoothGattCharacteristic.PROPERTY_READ,
BluetoothGattCharacteristic.PERMISSION_WRITE|
BluetoothGattCharacteristic.PERMISSION_READ);
创建BluetoothGattCharacteristic时,传入三个参数:UUID、特征属性 和 权限属性。

​ 特征属性表示该BluetoothGattCharacteristic拥有什么功能,即能对BluetoothGattCharacteristic进行什么操作。其中主要有3种:

BluetoothGattCharacteristic#PROPERTY_WRITE 表示特征支持写
BluetoothGattCharacteristic#PROPERTY_READ 表示特征支持读
BluetoothGattCharacteristic#PROPERTY_NOTIFY 表示特征支持通知
权限属性用于配置该特征值所具有的功能。主要两种:

BluetoothGattCharacteristic#PERMISSION_WRITE 特征写权限
BluetoothGattCharacteristic#PERMISSION_READ 特征读权限
注意事项
当特征值只有读权限时,调用BluetoothGatt#writeCharacteristic()对特征值进行修改时,将返回false,无法写入。并不会触发BluetoothGattCallback#onCharacteristicWrite()回调。
当特征值只有写权限时,调用BluetoothGatt#readCharacteristic()对特征值进行读取时,将返回false,无法写入。并不会触发BluetoothGattCallback#onCharacteristicRead()回调。
创建Gatt Descriptor
Characteristic下还有Descriptor,初始化BluetoothGattDescriptor时传入:Descriptor UUID 和 权限属性

//初始化描述
mGattDescriptor = new BluetoothGattDescriptor(UUID_DESCRIPTOR,BluetoothGattDescriptor.PERMISSION_WRITE);
添加 Characteristic 和 Descriptor
为Service添加Characteristic,为Characteristic添加Descriptor:

//Service添加特征值
mGattService.addCharacteristic(mGattCharacteristic);
mGattService.addCharacteristic(mGattReadCharacteristic);
//特征值添加描述
mGattCharacteristic.addDescriptor(mGattDescriptor);
​ 通过蓝牙管理器mBluetoothManager获取Gatt Server,用来添加Gatt Service。添加完Gatt Service后,外部中心设备连接手机时,将能获取到对应的GATT Service 和 获取对应的数据

//初始化GattServer回调
mBluetoothGattServerCallback = new daqiBluetoothGattServerCallback();

if (mBluetoothManager != null)
mBluetoothGattServer = mBluetoothManager.openGattServer(this, mBluetoothGattServerCallback);
boolean result = mBluetoothGattServer.addService(mGattService);
if (result){
Toast.makeText(daqiActivity.this,“添加服务成功”,Toast.LENGTH_SHORT).show();
}else {
Toast.makeText(daqiActivity.this,“添加服务失败”,Toast.LENGTH_SHORT).show();
}
​ 定义Gatt Server回调。当中心设备连接该手机外设、修改特征值、读取特征值等情况时,会得到相应情况的回调。

private class daqiBluetoothGattServerCallback extends BluetoothGattServerCallback{

//设备连接/断开连接回调
@Override
public void onConnectionStateChange(BluetoothDevice device, int status, int newState) {super.onConnectionStateChange(device, status, newState);
}//添加本地服务回调
@Override
public void onServiceAdded(int status, BluetoothGattService service) {super.onServiceAdded(status, service);
}//特征值读取回调
@Override
public void onCharacteristicReadRequest(BluetoothDevice device, int requestId, int offset, BluetoothGattCharacteristic characteristic) {super.onCharacteristicReadRequest(device, requestId, offset, characteristic);
}//特征值写入回调
@Override
public void onCharacteristicWriteRequest(BluetoothDevice device, int requestId, BluetoothGattCharacteristic characteristic, boolean preparedWrite, boolean responseNeeded, int offset, byte[] value) {super.onCharacteristicWriteRequest(device, requestId, characteristic, preparedWrite, responseNeeded, offset, value);
}//描述读取回调
@Override
public void onDescriptorReadRequest(BluetoothDevice device, int requestId, int offset, BluetoothGattDescriptor descriptor) {super.onDescriptorReadRequest(device, requestId, offset, descriptor);
}//描述写入回调
@Override
public void onDescriptorWriteRequest(BluetoothDevice device, int requestId, BluetoothGattDescriptor descriptor, boolean preparedWrite, boolean responseNeeded, int offset, byte[] value) {super.onDescriptorWriteRequest(device, requestId, descriptor, preparedWrite, responseNeeded, offset, value);
}

}
最后开启广播后,用nRF连接后看到的特征值信息如下图所示:(加多了一个只能都的特征值)

前序
        android ble系列将以本章结尾,前三章都是自己个人一遍一遍翻阅官网和博客,自己动手实践的归纳总结,最后以demo的形式展示和进一步巩固掌握的ble知识。该demo仿android版 nRF进行编写,功能简单但也相对齐全,主要涉及:扫描、连接通信、广播、Gatt Service和蓝牙报文解读。

该demo涉及到的知识全在前三章进行归纳描述,本章不再叙述。

一、扫描
        扫描附近的蓝牙设备,点击展示其广播附带的基本信息。

image
        点击RAW,查看完整的广播报文和报文详情。

image
二、广播
        对广播以及连接后进行的操作进行打印,具体的广播内容可以在代码进行调整。当然也可以使用前面的扫描对广播的内容进行查看!

想搞清楚广播的通信最好也学学蓝牙报文,那就很好掌握模仿ibeacon发送beacon数据。

image
三、连接
        对进行外设进行连接。连接成功后,对外设的特征和描述进行读写操作来进行ble通信。

image
四、最后
        最后,可以给两台手机安装该demo,一台进行ble广播,一台对广播的手机进行连接,并进行读取操作。

源码:https://pan.baidu.com/s/167I3wCDbVHTCtakKmQ59Dg 提取码:5a0a

作者:大棋17
链接:https://www.jianshu.com/p/a8adcee1f02e
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

android蓝牙BLE 有源码 有视频相关推荐

  1. Android 蓝牙BLE开发详解

    Android 蓝牙BLE开发详解 由于年初接手了个有关蓝牙BLE的项目,开始了对蓝牙ble的学习,经过长时间的慢慢学习(学得太慢,太拖了),终于了解了该怎么写蓝牙BLE,现在就给大家分享一下. 一. ...

  2. Android蓝牙BLE开发

    最近正在研究Android的蓝牙BLE开发学习,以下是自己做的个人总结 1.1何为BLE? 首先得说明什么是低功耗蓝牙BLE,BLE的全称为Bluetooth low energy(或称Blooth ...

  3. Android 蓝牙ble逻辑链路是什么?那LE -C、ACL -C 、SCO链路呢?

    android-蓝牙A2dp-avrcp-hfp-opp-配对流程-ble-rfcomm源码流程 Android 蓝牙低功耗ble 广播.扫描.连接.数据读写源码流程分析大全 - 点击下载 一.每种L ...

  4. Android 蓝牙二维码打印,打印图片,二维码加文字布局,蓝牙连接

    蓝牙二维码打印,工厂物料条码,绝对好用 资源下载地址:点击下载 扫描,显示蓝牙列表 public class DeviceListActivity extends AppCompatActivity ...

  5. Android蓝牙BLE开发(一)-基本原理

    公司有需求要做蓝牙BLE传输,经查阅后发现关于BLE开发的知识还真不多.对于BLE开发的同学来说,我的建议是先快速了解一下BLE的基本原理,磨刀不误砍柴工. 什么是BLE BLE全称Bluetooth ...

  6. Android蓝牙BLE的详细讲解

    我今天分享的主题是 Android 上低功耗蓝牙的实践.这个主题比较小众.我在过去的一年多的时间里,主要是在做低功耗蓝牙相关的开发.接触过程中发现,BLE 的开发和通常的 Android APP 的开 ...

  7. android 蓝牙BLE

    直接上代码吧,完整的程序代码如下: 首先是MainActivity,本段代码完成了子模转换和保存数据的功能,并将要发送的数据整理成了和作者的小伙伴说好的格式.需要注意的是,蓝牙发送一定要找准那个可写的 ...

  8. Android 蓝牙BLE串口通信之高低位转换、16进制字符串转换float浮点型

    蓝牙技术的普及与发展,为传统设备提供了一种低成本无线通信的方式.串口作为一种使用广泛的通信接口,通过串口转蓝牙,进行无线通信传输的需求逐渐展现出来. 蓝牙串口模块是嵌入式设备上的常用模块,它可以方便地 ...

  9. Android 蓝牙 ble 随机地址深层次分析

    同学,别退出呀,我可是全网最牛逼的 Android 蓝牙分析博主,我写了上百篇蓝牙文章,请点击下面了解本专栏,进入本博主主页看看再走呗,一定不会让你后悔的,记得一定要去看主页置顶文章哦. 一.IRK概 ...

最新文章

  1. R语言neuralnet包构建神经网络模型:基于乳腺癌数据集
  2. MariaDB Spider 数据库分库分表实践 分库分表
  3. Java NIO学习笔记之图解ByteBuffer
  4. IntelliJ IDEA for Windows 默认模式下的快捷键
  5. 绝对布局优势_前瞻布局+尖端科技+雄厚资金 恒大解锁造车的“正确姿势”
  6. 【译】2019年开始使用Typescript
  7. 三维旋转四元数系列(1.复数与二维旋转)
  8. JavaScript 详说事件流(冒泡、捕获、传播、委托)
  9. 如何提升自身的C++开发技术?
  10. 乘法口诀表 java_利用java 实现一个九九乘法口诀表
  11. 用C#开发Windows服务
  12. 纯前端html导出pdf(jsPDF.js)-分页-不分页
  13. 如何扩展计算机c盘的控件,电脑C盘空间不足,怎么把c盘空间可以扩大
  14. pyside6的MQTT客户端
  15. MILABOT:基于深度强化学习打造聊天机器人
  16. 谷歌google自动打开开发调式工具问题DevTools
  17. SNIPER—— SNIP的实战版本 (目标检测)(two-stage)(深度学习)(Arvix 2018)
  18. 李国庆是如何被“踢出”当当的?
  19. 【第3期】量化大咖来了!揭开量化对冲的神秘面纱
  20. iPhone同步助手 V3.2.7.2 中文官方版

热门文章

  1. Linux系列---【验证端口网络策略是否通的几种方式】
  2. 抛砖引玉----个人大数据测试小结
  3. 分省就业人数数据(2000-2018年)
  4. Type4Py: Deep Similarity Learning-Based TypeInference for Python
  5. EXP-00056: ORACLE error 12514 encountered解决方法
  6. 一台计算机数据丢失与恢复,如何在不丢失数据的情况下将iPhone与多台计算机同步-万兴数据恢复-万兴恢复专家...
  7. 接口规范文档总结、接口管理工具推荐、如何写出完美的接口
  8. 【独立版】帮扶极速版任务系统V3.1.3,八戒帮扶V5独立版
  9. 题解报告——Weed
  10. 10年第六届湖南省acm程序设计大赛 第五题 E 内部收益率