Android 从 4.3(API Level 18) 开始支持低功耗蓝牙,但是只支持作为中心设备(Central)模式,这就意味着 Android 设备只能主动扫描和链接其他外围设备(Peripheral)。从 Android 5.0(API Level 21) 开始两种模式都支持。
低功耗蓝牙开发算是较偏技术,实际开发中坑是比较多的,网上有很多文章介绍使用和经验总结,但是有些问题答案不好找,甚至有些误导人,比如 :获取已经连接的蓝牙,有的是通过反射,一大堆判断,然而并不是对所有手机有用,关于Ble传输速率问题的解决,都是默认Android每次只能发送20个字节,然而也并不是,,,下面进入正文。
Ble的使用 官网
1. 权限
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<!--Android 6.0及后续版本扫描蓝牙,需要定位权限(进入GPS设置,可以看到蓝牙定位)-->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />//9.0以上
<uses-feature
android:name="android.hardware.bluetooth_le"
android:required="false" />
<!--不支持低功耗的设备也可以安装,否则可以设置 required ="true"-->
2. 初始 使用 (在扫描前要记得先打开蓝牙成功,否则会很长时间不响应)
fun initBle() {
// 检查蓝牙开关
val adapter = BluetoothAdapter.getDefaultAdapter()
if (adapter == null) {
ToastUtils.showShortSafe("本机没有找到蓝牙硬件或驱动!")
return
} else {
/*if (!adapter.isEnabled()) {
//直接开启蓝牙
adapter.enable();
//跳转到设置界面
//startActivityForResult(new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE), 112);
}*/
if (!adapter.isEnabled) {
val enableIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
mContext.startActivityForResult(enableIntent, REQUEST_ENABLE_BT)
// 在onActivityResult里提示是否开成功根据Activity.RESULT_OK
}
}
// 检查是否支持BLE蓝牙
if (!mContext.packageManager.hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
ToastUtils.showShortSafe("本机不支持低功耗蓝牙!")
return
}
// Android 6.0动态请求位置权限
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val permissions = arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.ACCESS_COARSE_LOCATION)
for (str in permissions) {
if (mContext.checkSelfPermission(str) != PackageManager.PERMISSION_GRANTED) {
mContext.requestPermissions(permissions, REQUEST_ENABLE_LC)
break
}
}
}
}
3. 扫描
这里用的是 Android5.0 新增的扫描API,
- 尽量不要在扫描结果方法做耗时操作,它会快速连续调用(二期同一个设备连续返回)
- 添加用服务uuid做过滤,会加快扫描速度节省时间,比自己通过代码刷选好很多
- 扫描是耗电较大的操作,注意定时停止,官方 Demo 写的10m
// 扫描BLE蓝牙(不会扫描经典蓝牙)
private void scanBle() {
getConnedDevs();
isScanning = true;
// BluetoothAdapter bluetoothAdapter = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE).getDefaultAdapter();
BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
final BluetoothLeScanner bluetoothLeScanner = bluetoothAdapter.getBluetoothLeScanner();
// Android5.0新增的扫描API,扫描返回的结果更友好,比如BLE广播数据以前是byte[] scanRecord,而新API帮我们解析成ScanRecord类
bluetoothLeScanner.startScan(buildScanFilters(), buildScanSettings(), mScanCallback);
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
bluetoothLeScanner.stopScan(mScanCallback); //停止扫描
isScanning = false;
}
}, 3000);//(耗电)3秒后停止,根据需要设置
}
/**
* 过滤
*
* @return
*/
private List<ScanFilter> buildScanFilters() {
List<ScanFilter> scanFilters = new ArrayList<>();
ScanFilter.Builder builder = new ScanFilter.Builder();
// 通过服务 uuid 过滤自己要连接的设备
builder.setServiceUuid(new ParcelUuid(BleUtils.Companion.getUUID_SERVICE()));
scanFilters.add(builder.build());
return scanFilters;
}
private ScanSettings buildScanSettings() {
ScanSettings.Builder builder = new ScanSettings.Builder();
builder.setScanMode(ScanSettings.SCAN_MODE_LOW_POWER);//低耗电模式
//SCAN_MODE_LOW_LATENCY 低延迟 我用的(耗电,无所谓,又不是一直扫描的,3秒就停止了)
return builder.build();
}
private final ScanCallback mScanCallback = new ScanCallback() {// 扫描Callback
@Override
public void onScanResult(int callbackType, ScanResult result) {//尽量不做耗时操作,此方法会快速重复多次调用,返回相同的设备
String address = result.getDevice().getAddress();
if (!mDevices.contains(dev)) {
mDevices.add(dev);
notifyDataSetChanged();
Log.i(TAG, "onScanResult: " + result); // result.getScanRecord() 获取BLE广播数据
}
}
@Override
public void onBatchScanResults(List<ScanResult> results) {
super.onBatchScanResults(results);
Log.i(TAG, "onScanResult: " + " " + results);
}
};
// 重新扫描
public void reScan() {
mDevices.clear();
notifyDataSetChanged();
scanBle();
}
这里说一下,如果做蓝牙设备管理页面,可能区分是否是已连接的设备,网上又通过反射或其他挺麻烦的操作,也不见得获取到,官方Api 就有提供
BluetoothManager bluetoothManager = (BluetoothManager) mContext.getSystemService(Context.BLUETOOTH_SERVICE);
List<BluetoothDevice> connectedDevices = bluetoothManager.getConnectedDevices(BluetoothProfile.GATT);
4. 连接
- 在设备列表 点击 dev.connectGatt(mContext, false, mBluetoothGattCallback) -> 这里的false 是不自动连,如果是true 使用时连接很慢。
- onConnectionStateChange()(1 成功 继续启动发现服务 2 . 经常断链有时候连接一次不成功,就重复 dev.connectGatt()->
- onServicesDiscovered() 服务发现成功好会把设备所有 Service 和 Service 里的 characteristics 及 characteristic.descriptors 全返回来(可以用来展示,调试和判断自己要用的服务 特征 到底有没有成功发现)然后setNotify() 需要接收数据时得打开通知 ->
- onDescriptorWrite() 写入通知成功后mBluetoothGatt!!.requestMtu() 请求该设备的Mtu ,默认23个字节 我们用的设备我申请256个字节最后只能拿到244个字节->
- onMtuChanged 拿到mtu-3 为什么减3 后面做数据分包->
-
最后通知连接成功 mBleConnCallBack!!.onConntionedCallBack(status, isConnected)
这个callback是我自己写的回调 才能做后面的发送,接收数据
/**
* 连接设备###
*/
fun connDev(dev: BluetoothDevice, bleConnCallBack: BleConnCallBack) {
closeConn()
mBleConnCallBack = bleConnCallBack
mBluetoothGatt = dev.connectGatt(mContext, false, mBluetoothGattCallback) // 连接蓝牙设备
}
/**
* BLE中心设备连接外围设备的数量有限(大概2~7个),在建立新连接之前必须释放旧连接资源,否则容易出现连接错误133
*/
fun closeConn() {
if (mBluetoothGatt != null) {
mBluetoothGatt!!.disconnect()
mBluetoothGatt!!.close()
}
}
private fun initBleClient() {
mBluetoothGattCallback = object : BluetoothGattCallback() {
/**
* 连接 状态
*/
override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
val dev = gatt.device
AppLog.i(TAG, String.format("连接 状态++onConnectionStateChange:%s,%s,%s,%s", dev.name, dev.address, status, newState))
if (status == BluetoothGatt.GATT_SUCCESS && newState == BluetoothProfile.STATE_CONNECTED) {
isConnected = true
connTimes = 0
gatt.discoverServices() //启动服务发现
} else {
isConnected = false
closeConn()
if (connTimes++ < 5) {//错误连接5次
dev.connectGatt(mContext, false, this)
} else {
connTimes = 0
Handler(Looper.getMainLooper()).post { mBleConnCallBack!!.onConntionedCallBack(status, isConnected) }
}
}
BleDevConnedState.saveBasicID(dev.address, isConnected)
AppLog.i(TAG,"isConnected : "+ isConnected+" status:"+status)
logTv(String.format(
if (status == 0)
if (newState == 2)
"与[%s]连接成功"
else "与[%s]连接断开"
else "与[%s]连接出错,错误码:$status",
dev))
}
/**
* 发现服务
*/
override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
AppLog.i(TAG, String.format("发现服务++onServicesDiscovered:%s,%s,%s", gatt.device.name, gatt.device.address, status))
if (status == BluetoothGatt.GATT_SUCCESS) { //BLE服务发现成功
Handler(Looper.getMainLooper()).post{setNotify()}
// 遍历获取BLE服务Services/Characteristics/Descriptors的全部UUID
for (service in gatt.services) {
val allUUIDs = StringBuilder("UUIDs={\nS=" + service.uuid.toString())
for (characteristic in service.characteristics) {
allUUIDs.append(",\nC=").append(characteristic.uuid)
for (descriptor in characteristic.descriptors)
allUUIDs.append(",\nD=").append(descriptor.uuid)
}
allUUIDs.append("}")
logTv("发现服务$allUUIDs")
}
}
}
/**
* 读
*/
override fun onCharacteristicRead(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, status: Int) {
val uuid = characteristic.uuid
val valueStr = String(characteristic.value)
AppLog.i(TAG, String.format("onCharacteristicRead:%s,%s,%s,%s,%s", gatt.device.name, gatt.device.address, uuid, valueStr, status))
}
/**
* 写入成功++
* @param gatt
* @param characteristic
* @param status
*/
override fun onCharacteristicWrite(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, status: Int) {
val uuid = characteristic.uuid
val valueStr = String(characteristic.value)
AppLog.i(TAG, String.format("写入成功++onCharacteristicWrite:%s,%s,%s,%s,%s,\n" +
" %s", gatt.device.name, gatt.device.address, uuid, valueStr, status, Arrays.toString(characteristic.value)))
onSendBytes()
}
/**
* 发过来
* 通知过来的消息++
* @param gatt
* @param characteristic
*/
override fun onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) {
val uuid = characteristic.uuid
val valueStr = String(characteristic.value)
AppLog.i(TAG, String.format("发过来++onCharacteristicChanged:%s,%s,%s,%s", gatt.device.name, gatt.device.address, uuid, valueStr))
onReadBytes(characteristic.value)
}
override fun onDescriptorRead(gatt: BluetoothGatt, descriptor: BluetoothGattDescriptor, status: Int) {
val uuid = descriptor.uuid
val valueStr = Arrays.toString(descriptor.value)
AppLog.i(TAG, String.format("onDescriptorRead:%s,%s,%s,%s,%s", gatt.device.name, gatt.device.address, uuid, valueStr, status))
}
/**
* 写入通知成功
*/
override fun onDescriptorWrite(gatt: BluetoothGatt, descriptor: BluetoothGattDescriptor, status: Int) {
val uuid = descriptor.uuid
val valueStr = Arrays.toString(descriptor.value)
Handler(Looper.getMainLooper()).post {
mBluetoothGatt!!.requestMtu(256)//我们的是244
}
}
override fun onMtuChanged(gatt: BluetoothGatt?, mtu: Int, status: Int) {
super.onMtuChanged(gatt, mtu, status)
mMtu=mtu-3
AppLog.i(TAG,"连接成功"+"requestMtu"+"onMtuChanged:"+mtu+" "+status)
Handler(Looper.getMainLooper()).post {
mBleConnCallBack!!.onConntionedCallBack(status, isConnected)
}
}
}
}
5. 收发数据
与外围设备交互经常每次发的数据大于 mtu的,需要做分包处理,接收数据也要判断数据的完整性最后才返回原数据做处理,所以一般交互最少包含包长度,和包校验码和原数据。当然也可以加包头,指令还有其他完整性校验。下面分享几个公用方法:
/**
* 要可接收消息 需要要打开通知###
*/
fun setNotify() {
val service = getGattService(UUID_SERVICE)
if (service != null) {
// 设置Characteristic通知
val characteristic = service.getCharacteristic(UUID_CHAR_READ_NOTIFY)//通过UUID获取可通知的Characteristic
mBluetoothGatt!!.setCharacteristicNotification(characteristic, true)
// 向Characteristic的Descriptor属性写入通知开关,使蓝牙设备主动向手机发送数据
val descriptor = characteristic.getDescriptor(UUID_DESC_NOTITY)
// descriptor.setValue(BluetoothGattDescriptor.ENABLE_INDICATION_VALUE);//和通知类似,但服务端不主动发数据,只指示客户端读取数据
descriptor.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE//1,0
mBluetoothGatt!!.writeDescriptor(descriptor)
}
}
//数据分包处理
private fun splitPacketForMtuByte(data: ByteArray?): Queue<ByteArray> {
val dataInfoQueue = LinkedList<ByteArray>()
if (data != null) {
var index = 0
do {
val currentData: ByteArray
if (data.size - index <= mMtu) {
currentData = ByteArray(data.size - index)
System.arraycopy(data, index, currentData, 0, data.size - index)
index = data.size
} else {
currentData = ByteArray(mMtu)
System.arraycopy(data, index, currentData, 0, mMtu)//从零开始复制++ mMtu
index += mMtu
}
dataInfoQueue.offer(currentData)
} while (index < data.size)
}
return dataInfoQueue
}
private fun intToByteArray(a: Int): ByteArray =
byteArrayOf((a shr 24 and 0xFF).toByte(),
(a shr 16 and 0xFF).toByte(),
(a shr 8 and 0xFF).toByte(),
(a and 0xFF).toByte())
private fun bytes2Int(byteArray: ByteArray): Int =
(byteArray[0].toInt() and 0xFF shl 24) or
(byteArray[1].toInt() and 0xFF shl 16) or
(byteArray[2].toInt() and 0xFF shl 8) or
(byteArray[3].toInt() and 0xFF)
/**
* 写数据###
* @param instruction 指令
* @param jsonData json数据
* @param bleServerCallBack 接收完Basic 数据后响应
*/
fun write(instruction: Byte, jsonData: String, bleServerCallBack: BleServerCallBack) {
val service = getGattService(UUID_SERVICE)
if (service != null) {
// ....
var bleData = byteMergerAll(PACKAGE_HEAD, intToByteArray, instructionArray,crc32ByteArray, jsonData.toByteArray())
bleData = byteMergerAll(
//...头,length,指令等
jsonData.toByteArray(),
getShaHead(bleData)) //可以用hash校验数据完整性
mBleServerCallBack = bleServerCallBack
resultSize = 0
mCharacteristic = service.getCharacteristic(UUID_CHAR_WRITE)//通过UUID获取可写的Characteristic
writeCharacteristic(mCharacteristic, bleData)
}
}
/**
* 向characteristic写数据
*
* @param value
*/
private fun writeCharacteristic(characteristic: BluetoothGattCharacteristic?, value: ByteArray) {
this.mCharacteristic = characteristic
if (dataInfoQueue != null) {
dataInfoQueue!!.clear()
writeSize = value.size//这是我自己写的读写进度用的
writeProgress = 0
dataInfoQueue = splitPacketForMtuByte(value)
onSendBytes()
}
}
/**
* 连续发送
* 每次从队列里取第一个同时移除
*/
private fun onSendBytes() {
if (dataInfoQueue != null && !dataInfoQueue!!.isEmpty()) {
//检测到发送数据,直接发送
if (dataInfoQueue!!.peek() != null) {
mCharacteristic!!.value = dataInfoQueue!!.poll()//移除并返回队列头部的元素
writeProgress += mCharacteristic!!.value.size
mBleServerCallBack!!.onWriteProgress(writeProgress * 100 / writeSize)
Handler(Looper.getMainLooper()).postDelayed({
mBluetoothGatt!!.writeCharacteristic(mCharacteristic)
}, 125)//120ms
}
}
}
/**
* 写入成功++
* @param gatt
* @param characteristic
* @param status
*/
override fun onCharacteristicWrite(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, status: Int) {
val uuid = characteristic.uuid
val valueStr = String(characteristic.value)
AppLog.i(TAG, String.format("写入成功++onCharacteristicWrite:%s,%s,%s,%s,%s,\n" +
" %s", gatt.device.name, gatt.device.address, uuid, valueStr, status, Arrays.toString(characteristic.value)))
onSendBytes()//继续发送
}
/**
* 发过来
* 通知过来的消息++
* @param gatt
* @param characteristic
*/
override fun onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) {
val uuid = characteristic.uuid
val valueStr = String(characteristic.value)
AppLog.i(TAG, String.format("发过来++onCharacteristicChanged:%s,%s,%s,%s", gatt.device.name, gatt.device.address, uuid, valueStr))
onReadBytes(characteristic.value) //处理发回来的数据,最后校验拼接好
}
- 上面的设置通知和写数据都要 用到 mBluetoothGatt的 服务,特征
- 主动读取和通知 会用到 特征里的
描述
详见setNotify() 和 write()
而操作服务,特征,描述 都通过特定的UUIU来操作,这些uuid都是蓝牙设备从生产出来就不变了, 两手机调试时可以自定义uuid.
val UUID_SERVICE =UUID.fromString("00000000-0000-0000-0000-0000000000f0")//自定义UUID
val UUID_CHAR_READ_NOTIFY = UUID.fromString("00000000-0000-0000-0000-0000000000f2")
val UUID_CHAR_WRITE = UUID.fromString("00000000-0000-0000-0000-000000000001")
val UUID_DESC_NOTITY = UUID.fromString("00000000-0000-0000-0000-000000000002")
我自己封装的一个BleUtil ,因为涉及跟公司业务关联性太强(主要是传输包的协议不同)就先不开源出来了,如果这边文章对大家有帮助反馈不错,我会考虑上传个demo到github供大家使用,
在这先给大家推荐一个不错 Demo,里面除了没有分包,协议,和传输速率。基本的功能都有,而且调试数据到打印到界面上了。最主要是它可以用两个个手机一个当中心设备一个当外围设备调试。
如果在实际使用蓝牙, 调用时得姿势应该是这样的
BleUtils.getInstance(mContext)
.write(
BleInstructions.signTx,
jsonObject.toString(),
object : BleServerCallBack {
override fun onResponse(instruction: Byte?, result: String) {
val fromJson = Gson().fromJson(result, xxxBean::class.java)
}
override fun onWriteProgress(progress: Int) {
}
override fun onReadProgress(progress: Int) {
}
})
6. 传输速率
首先传输速率优化有两个方向,1 外围设备传输到Android 。2 Android传输到外围设备。
我在开发中首先先使用上面那位仁兄的demo调试,两个Android 设备调试不延时,上一个成功马上下一个,最多一秒发11个20字节的包。
后来和我们的蓝牙设备调试时发现发送特别快,但是数据不完整,他蓝牙模块接收成功了,但是透传数据到芯片处理时发现不完整,我们的硬件小伙伴说因为波特率限制(差不多每10字节透传要耗时1ms)和蓝牙模块的buff (打印时是最多100byte,100打印的)限制,就算蓝牙模块每包都告诉你接收成功,也是没透传完就又接收了。后来通过调试每次发20K数据,最后是 Android 发是 20字节/130ms
稳定。给Android 发是20字节/ 8ms
。 (天杀的20字节,网上都是说20字节最多了)
后来看了国外一家物联网公司总结的 Ble 吞吐量的文章(上面有连接),知道Android 每个延时是可以连续接收6个包的。就改为 120字节/ 16ms
(为啥是16ms,不是每次间隔要6个包吗,怎么像间隔两次,这时因为波特率影响,多了5个包100字节,差不多 我们的单片机透传到蓝牙模块要多耗时不到10ms )
而Android 发数据可以申请 我们设备的mtu 来得到最多每次能发多少字节。延时还是130ms,即:241字节/ 130ms 提高12倍,这个速度还可以。
7. 扩展
BLE的传输速率分析
根据蓝牙BLE协议, 物理层physical layer的传输速率是1Mbps,相当于每秒125K字节。事实上,其只是基准传输速率,协议规定BLE不能连续不断地传输数据包,否则就不能称为低功耗蓝牙了。连续传输自然会带来高功耗。所以,蓝牙的最高传输速率并不由物理层的工作频率决定的。
在实际的操作过程中,如果主机连线不断地发送数据包,要么丢包严重要么连接出现异常而断开。
在BLE里面,传输速度受其连接参数所影响。连接参数定义如下:
1)连接间隔。蓝牙基带是跳频工作的,主机和从机会商定多长时间进行跳频连接,连接上才能进行数据传输。这个连接和广播状态和连接状态的连接不是一样的意思。主机在从机广播时进行连接是应用层的主动软件行为。而跳频过程中的连接是蓝牙基带协议的规定,完全由硬件控制,对应用层透明。明显,如果这个连接间隔时间越短,那么传输的速度就增大。连接上传完数据后,蓝牙基带即进入休眠状态,保证低功耗。其是1.25毫秒一个单位。
2)连接延迟。其是为了低功耗考虑,允许从机在跳频过程中不理会主机的跳频指令,继续睡眠一段时间。而主机不能因为从机睡眠而认为其断开连接了。其是1.25毫秒一个单位。明显,这个数值越小,传输速度也高。
蓝牙BLE协议规定连接参数最小是5,即7.25毫秒;而Android手机规定连接参数最小是8,即10毫秒。iOS规定是16,即20毫秒。
连接参数完全由主机决定,但从机可以发出更新参数申请,主机可以接受也可以拒绝。android手机一部接受,而ios比较严格,拒绝的概率比较高。