- BLE 与经典蓝牙的区别
- BLE 的 Kotlin 下实践
经典蓝牙(Classic Bluetooth)& 低功耗蓝牙(Bluetooth Low Energy)
-
经典蓝牙可以用与数据量比较大的传输,如语音,音乐,较高数据量传输等。
-
BLE 特点就如其名,功耗更低的同时,对数据包做出了限制。所以适用于实时性要求比较高,但是数据速率比较低的产品,如鼠标,键盘,传感设备的数据发送等。
蓝牙 4.0 支持单模和双模两种部署方式,其中单模即是我们说的 BLE,而双模指的是 Classic Bluetooth + BLE 。
实际上,BLE 和经典蓝牙的使用等各方面都像是没有关联的两个东西,甚至因为 BLE 的通讯机制不同,所以是不能向下兼容的;经典蓝牙则可以兼容到蓝牙 3.0 / 2.1。
经典蓝牙
参考官方文档(注意科学上网),因为有中文文档,所以只要看这一篇文档就可以应付一般的开发了。
最重要的是这一次项目里的硬件貌似不能支持经典蓝牙,所以并没有实践的机会。
BLE
同样,有条件一定要去看官方文档,然而这一次并没有中文版,或许可以找一些国内大佬们翻译的版本。
还有就是大佬 JBD 写的 Android BLE 蓝牙开发入门 ,而且还用 RxJava 封装成一个库可以直接调用:RxBLE ,是真的厉害,不妨去学习学习。
- 概念与常用 API
UUID:每个服务和特征都会有唯一的 UUID ,由硬件决定。
服务(Service):蓝牙设备中可以定义多个服务,相当于功能的集合。
特征(Characteristic):一个服务可以包含多个特征,可以通过 UUID 获取到对应的特征的实例,通过这个实例就可以向蓝牙设备发送 / 读取数据。
BluetoothDeivce
:调用 startLeScan()
获取该实例,用于连接设备。
BluetoothManager
:蓝牙管理器,调用 getSystemService()
获取,用于获取蓝牙适配器和管理所有和蓝牙相关的东西。
BluetoothAdapter
:蓝牙适配器,通过 BluetoothManager
获取,用于打开蓝牙、开始扫描设备等操作。
BluetoothGatt
:通用属性协议, 定义了BLE通讯的基本规则,就是通过把数据包装成服务和特征的约定过程。
BluetoothGattCallback
:一个回调类,非常重要而且会频繁使用,用于回调 GATT 通信的各种状态和结果。
BluetoothGattService
:服务,通过 BluetoothGatt
实例调用 getService(UUID) 获取
。
BluetoothGattCharacteristic
:特征,通过 BluetoothGattService
实例调用 getCharacteristic(UUID)
获取,是 GATT 通信中的最小数据单元。
BluetoothGattDescriptor
:特征描述符,对特征的额外描述,包括但不仅限于特征的单位,属性等。
- 声明权限
<uses-permission android:name="android.permission.BLUETOOTH"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
<!-- Android 5.0 及以上需要添加 GPS 权限 -->
<uses-feature android:name="android.hardware.location.gps" />
<!-- Android 6.0 及以上需要添加定位权限 -->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
- 初始化
fun initBluetoothAdapter(){
val bluetoothManager = getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
val bluetoothAdapter = bluetoothManager.adapter
//如果蓝牙没有打开则向用户申请
if (bluetoothAdapter == null || !bluetoothAdapter.isEnabled)
bluetoothAdapter.enable()
}
- 扫描设备与停止扫描
var mDevice : BluetoothDevice ?= null
//扫描结果的回调,开始扫描后会多次调用该方法
val mLeScanCallback = BluetoothAdapter.LeScanCallback { device, rssi, scanRecord ->
//通过对比设备的 mac 地址获取需要的实例
if(device.address == "50:F1:4A:A1:77:00"){
mDevice = device
}
}
//开始扫描之前判断是否开启了蓝牙,enable 为 false 可以停止扫描
fun scanLeDeviceWithBLE(enable:Boolean = true){
if (mBluetoothAdapter == null)
initBluetoothAdapter()
if (mBluetoothAdapter?.isEnabled as Boolean){
mBluetoothAdapter?.enable()
}
if (enable){
mScanning = true
mBluetoothAdapter?.startLeScan(mLeScanCallback)
TimeUtilWithoutKotlin.Delay(8,TimeUnit.SECONDS).setTodo {
mBluetoothAdapter?.stopLeScan(mLeScanCallback)
mScanning = false
}
}else {
//停止扫描,在连接设备时最好调用 stopLeScan()
mBluetoothAdapter?.stopLeScan(mLeScanCallback)
mScanning = false
}
}
其实 startLeScan()
已经被声明为过时,所以开始扫描还有其他的方法:
private fun startDiscover() {
//这种方法需要注册接收广播,获取扫描结果。
val bluetoothAdapter = BluetoothAdapter.getDefaultAdapter()
bluetoothAdapter?.startDiscovery()
}
//注册广播,监听 BluetoothDevice.ACTION_FOUND 获取扫描结果
private inner class DeviceReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val action = intent.action
if (BluetoothDevice.ACTION_FOUND.equals(action)) {
val device = intent
.getParcelableExtra<BluetoothDevice>(BluetoothDevice.EXTRA_DEVICE)
Log.e("Service","device: ${device.address}")
}
}
}
-
连接蓝牙设备
此时已经获取到了蓝牙设备的实例:mDevice
,开始连接
fun connectWithBluetoothDevice(){
if (null == mDevice){
toast("can not find device")
return
}
if(mScanning){
//如果正在扫描设备,则停止扫描
scanLeDeviceWithBLE(false)
}
mDevice?.connectGatt(this,false,mBluetoothGattCallback)
}
关于 connectGatt()
的几个参数:
public BluetoothGatt connectGatt(Context context, boolean autoConnect,
BluetoothGattCallback callback)
第二个参数,autoConnect
为 true 时,如果设备断开了连接将会不断的尝试连接。
第三个 BluetoothGattCallback
是一个接受回调的对象,也是这一部分的重点。
先看一下完整的 BluetoothGattCallback
:
val mBluetoothGattCallback = object :BluetoothGattCallback(){
override fun onConnectionStateChange(gatt: BluetoothGatt?, status: Int, newState: Int) {
super.onConnectionStateChange(gatt, status, newState)
if (newState == BluetoothProfile.STATE_CONNECTED){
//开始搜索服务
gatt?.discoverServices()
}
if (newState == BluetoothProfile.STATE_DISCONNECTED) {
gatt?.close()
}
if(newState == BluetoothProfile.STATE_CONNECTING){
//设备在连接中
}
}
//成功发现服务的回调
override fun onServicesDiscovered(gatt: BluetoothGatt?, status: Int) {
super.onServicesDiscovered(gatt, status)
if (gatt == null) {
return
}
//设置回调,打开 Android 端接收通知的开关,用 Descriptor 开启通知的数据开关
//这里的三个 UUID 都是由硬件决定的
val bluetoothGattService = gatt.getService(UUID_0)
val characteristic = bluetoothGattService.getCharacteristic(UUID_1)
val descriptor = characteristic.getDescriptor(UUID_2)
if (descriptor == null) {
gatt.disconnect()
return
}
//打开 Android 端开关
if (!gatt.setCharacteristicNotification(characteristic, true)) {
//打开失败
}
//假如写入数据成功,则会回调下面的 onDescriptorWrite() 方法
//所以在 onDescriptorWrite() 方法中向硬件写入数据
descriptor.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
if (!gatt.writeDescriptor(descriptor)) {
//写入失败
}
}
//调用 writeDescriptor 的回调
override fun onDescriptorWrite(gatt: BluetoothGatt?, descriptor: BluetoothGattDescriptor?, status: Int) {
super.onDescriptorWrite(gatt, descriptor, status)
val bluetoothGattService = gatt?.getService(UUID_SERVICE_CHANNEL)
val characteristic = bluetoothGattService?.getCharacteristic(UUID_CHARACTERISTIC_CHANNEL)
if (characteristic == null){
//获取特征失败,直接断开连接
gatt?.disconnect()
return
}
//mSendValue 即要往硬件发送的数据
//如果这里写入数据成功会回调下面的 onCharacteristicWrite() 方法
characteristic.value = mSendValue
if (!gatt.writeCharacteristic(characteristic)){
//写入数据失败,断开连接
gatt.disconnect()
}
}
//调用 writeCharacteristic 的回调
override fun onCharacteristicWrite(gatt: BluetoothGatt?, characteristic: BluetoothGattCharacteristic?, status: Int) {
super.onCharacteristicWrite(gatt, characteristic, status)
val stringBuilder = StringBuilder()
characteristic?.value
?.filter { it > 0 }
?.forEach { stringBuilder.append(String.format("%c", it)) }
//这时候 stringBuilder 应该和上面 mSendValue 是一样的
}
//硬件返回数据的回调,由于设置了回调,所以当硬件返回数据时会调用这个方法
override fun onCharacteristicChanged(gatt: BluetoothGatt?, characteristic: BluetoothGattCharacteristic?) {
super.onCharacteristicChanged(gatt, characteristic)
val stringBuilder = StringBuilder()
characteristic?.value?.forEach {
val b = it
hexStringBuilder.append(Integer.toHexString(b.toInt()))
stringBuilder.append(String.format("%c",b))
}
runOnUiThread { toast("$stringBuilder") }
//接受到数据之后就可以断开连接了
gatt?.disconnect()
}
}
首先是 onConnectionStateChange(gatt,status,newState)
,
这个方法在成功连接、断开连接等状态改变的时候回调,所以一开始会先进入这个方法。
参数中, newState
代表当前设备的连接的状态:
/** The profile is in disconnected state */
public static final int STATE_DISCONNECTED = 0;
/** The profile is in connecting state */
public static final int STATE_CONNECTING = 1;
/** The profile is in connected state */
public static final int STATE_CONNECTED = 2;
/** The profile is in disconnecting state */
public static final int STATE_DISCONNECTING = 3;
所以当 newState
为 2 的时候就是刚连上设备的时候,这时候可以调用
gatt.discoverServices()
开始异步的查找蓝牙服务:
if (newState == BluetoothProfile.STATE_CONNECTED){
//发现服务
gatt?.discoverServices()
}
执行了discoverServices()
后,若找到可用的服务,系统又会回调 mBluetoothGattCallback
里的onServicesDiscovered()
方法,所以添加:
override fun onServicesDiscovered(gatt: BluetoothGatt?, status: Int) {
val bluetoothGattService = gatt?.getService(UUID_0)
val characteristic = bluetoothGattService?.getCharacteristic(UUID_1)
if (characteristic == null){
//获取特征的实例失败,断开连接
gatt?.disconnect()
return
}
//向硬件写入数据
characteristic.value = mSendValue
if (!gatt.writeCharacteristic(characteristic)){
//当上面的方法返回 false 时,写入数据失败
gatt.disconnect()
}
}
如果成功写入数据,系统回调mBluetoothGattCallback
的onCharacteristicWrite()
方法:
override fun onCharacteristicWrite(gatt: BluetoothGatt?, characteristic: BluetoothGattCharacteristic?, status: Int) {
super.onCharacteristicWrite(gatt, characteristic, status)
//这里遍历 characteristic 中的 value,拼接在一起后成为一个 stringBuilder
//stringBuilder 应该和发送给硬件的数据是一样的
val stringBuilder = StringBuilder()
characteristic?.value
?.filter { it > 0 }
?.forEach { stringBuilder.append(String.format("%c", it)) }
//断开连接,这一句最好延迟几秒后执行
gatt?.disconnect()
}
上面的代码可以成功往硬件发送数据,但是不能接受硬件返回的数据。
如果想要接受硬件返回的数据,需要在 onServicesDiscovered()
,也就是连上服务后,先不发送数据而是设置硬件返回数据的开关:
//设置回调:打开 Android 端接收通知的开关;并且向 Descriptor 写入数据来开启通知
val bluetoothGattService = gatt.getService(UUID_SERVICE_CHANNEL)
val characteristic = bluetoothGattService.getCharacteristic(UUID_CHARACTERISTIC_CHANNEL)
val descriptor = characteristic.getDescriptor(CLIENT_CHARACTERISTIC_CONFIG_DESCRIPTOR_UUID)
val descriptors = characteristic.descriptors
if (descriptors == null) {
//获取特征描述符失败,断开连接
gatt.disconnect()
return
}
//打开 Android 端开关
if (!gatt.setCharacteristicNotification(characteristic, true)) {
//失败的处理
}
//向硬件写入一些数据,打开硬件返回数据的开关
descriptor.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
if (!gatt.writeDescriptor(descriptor)) {
//写入数据失败
}
实际上向硬件写入数据这一段代码有时候是可以省略的,只需要打开 Android 段的开关即可接收到返回的数据,可能是和硬件有关。
这样一来,就不能继续在 onServicesDiscovered()
执行写入数据的代码,改为在 onDescriptorWrite()
中执行。
如果写过经典蓝牙的就知道,如果说两者的搜索操作还差不多的话,连接操作和写入操作就是完全不同的东西了。
经典蓝牙可以获取到一个类似 TCP 中 Socket 的对象,然后获取 InputStream
和OutputStream
,二者分别通过套接字以及 getInputStream()
和 getOutputStream()
来处理数据传输。
而 BLE 中需要通过不同的 UUID 获取对应的服务、特征才可以写入数据。
以上就是 BLE 最简单的实现,进一步的封装等就去看上面介绍的 RxBLE 吧。
还有就是用 Kotlin 写的 MainActivity 部分,放在这里其实感觉很奇怪,奈何尽量不做标题党还是贴上来:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
verticalLayout {
gravity = CENTER
linearLayout {
orientation = LinearLayout.VERTICAL
button("搜索设备"){
setOnClickListener {
mBinder?.startScanLeDevice()
}
}.lparams(width = matchParent,height = wrapContent){
padding = dip(5)
margin = dip(10)
}
button("发送开锁指令"){
padding = dip(10)
setOnClickListener{
mBinder?.connect()
}
}.lparams(width = matchParent,height = wrapContent){
padding = dip(5)
margin = dip(10)
}
}
}
val intent = Intent(this, BluetoothService::class.java)
bindService(intent,mConnect,Context.BIND_AUTO_CREATE)
}
override fun onDestroy() {
super.onDestroy()
mDisposable?.dispose()
}
var mBinder : BluetoothService.MBinder ?= null
var mDisposable : Disposable ?= null
val mConnect = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName, service: IBinder) {
mBinder = service as BluetoothService.MBinder
}
override fun onServiceDisconnected(name: ComponentName) {
}
}
}
BLE 相关的代码是写在了 Service 中,通过绑定时返回的 mBinder 来调用 Service 中的方法。
网友评论