Android 蓝牙BLE开发完全指南

作者:RalfNick 时间:2023-07-03 03:15:06 

 介绍

1.BLE 是 Bluetooth Low Energy 的缩写,意思为低功耗蓝牙。由蓝牙技术联盟(Bluetooth SIG)设计的无线通讯技术,主要用于医疗,健身,安全和家庭娱乐行业。 与传统蓝牙相比,蓝牙低功耗旨在大幅降低功耗和成本,同时也能够达到相同的通讯效果。

支持多个平台,包括 IOS,Android,Windows Phone 和 BlackBerry 以及 macOS,Linux,Windows 8 和 Windows 10 在内的移动操作系统本身支持蓝牙低功耗。 蓝牙 SIG 预测,到 2018 年,超过 90% 的蓝牙智能手机将支持蓝牙低功耗。

在安卓平台,

在 Android 4.3 (API level 18) 以后引进来的,通过这些 API 可以扫描蓝牙设备、连接设备,查询 services、读写设备的 characteristics(属性特征),然后通过属性进行数据传输。

特点:

低功耗,使用 BLE 与周围设备进行通讯时,其峰值功耗为传统蓝牙的一半

传输距离提升到 100 米

低延时,最短可在3 ms内完成连接并开始进行数据传输

缺点:

传输数据量较小,最大 512 个字节,超过 20 个字节需要分包处理

应用领域:

主要用于智能硬件,像健康护理、运动和健身、设备电源管理等

连接模式

对于BLE单设备来讲常见的蓝牙模块的工作模有四种:

主设备模式

从设备模式

广播模式

Mesh组网模式

主设备模式

可以与一个从设备进行连接。在此模式下可以对周围设备进行搜索并选择需要连接的从设备进行连接。同时可以设置默认连接从设备的MAC地址,这样模块上电之后就可以查找此模块并进行连接。

从设备模式

BLE支持从设备模式,在此模式下完全符合BLE4.1协议,用户可以根据协议自己开发APP。此模式下包含一个串口收发的Service,用户可以通过UUID找到它,里面有两个通道,分别是读和写。用户可以操作这两个通道进行数据的传输。

广播模式

在这种模式下模块可以一对多进行广播。用户可以通过AT指令设置模块广播的数据,模块可以在低功耗的模式下持续的进行广播,应用于极低功耗,小数据量,单向传输的应用场合,比如无线抄表,室内定位等功能。

Mesh组网模式

在这种模式下模块可以实现简单的自组网络,每个模块只需要设置相同的通讯密码就可以加入到同一网络当中,每一个模块都可以发起数据,每个模块可以收到数据并且进行回复。并且不需要网关,即使某一个设备出现故障也会跳过并选择最近的设备进行传输。

GATT协议

GATT generic Attributes的缩写,中文是通用属性,是低功耗蓝牙设备之间进行通信的协议。

GATT定义了一种多层的数据结构,已连接的低功耗蓝牙设备用它来进行通信,GATT层是传输真正数据所在的层。一个GATT服务器通过一个称为属性表的表格组织数据,这些数据就是用于真正发送的数据。

GATT定义的多层数据结构简要概括起来就是服务(service)可以包含多个特征(characteristic),每个特征包含属性(properties)和值(value),还可以包含多个描述(descriptor)。它形象的结构如下图:

Android 蓝牙BLE开发完全指南

profile(数据配置文件)

一个profile文件可以包含一个或者多个服务,一个profile文件包含需要的服务的信息或者为对等设备如何交互的配置文件的选项信息。设备的GAP和GATT的角色都可能在数据的交换过程中改变,因此,这个文件应该包含广播的种类、所使用的连接间隔、所需的安全等级等信息。

需要注意的是: 一个profile中的属性表不能包含另一个属性表。

属性

一个属性包含句柄、UUID(类型)、值,句柄是属性在GATT表中的索引,在一个设备中每一个属性的句柄都是唯一的。UUID包含属性表中数据类型的信息,它是理解属性表中的值的每一个字节的意义的关键信息。在一个GATT表中可能有许多属性,这些属性能可能有相同的UUID。

个人理解,属性指的是 Service、Characteristic 这样的对象

Service

一个低功耗蓝牙设备可以定义许多 Service, Service 可以理解为一个功能的集合。设备中每一个不同的 Service 都有一个 128 bit 的 UUID 作为这个 Service 的独立标志。蓝牙核心规范制定了两种不同的UUID,一种是基本的UUID,一种是代替基本UUID的16位UUID。所有的蓝牙技术联盟定义UUID共用了一个基本的UUID:

0x0000xxxx-0000-1000-8000-00805F9B34FB

为了进一步简化基本UUID,每一个蓝牙技术联盟定义的属性有一个唯一的16位UUID,以代替上面的基本UUID的‘x'部分。例如,心率测量特性使用0X2A37作为它的16位UUID,因此它完整的128位UUID为:

0x00002A37-0000-1000-8000-00805F9B34FB

Characteristic

在 Service 下面,又包括了许多的独立数据项,我们把这些独立的数据项称作 Characteristic。同样的,每一个 Characteristic 也有一个唯一的 UUID 作为标识符。在 Android 开发中,建立蓝牙连接后,我们说的通过蓝牙发送数据给外围设备就是往这些 Characteristic 中的 Value 字段写入数据;外围设备发送数据给手机就是监听这些 Charateristic 中的 Value 字段有没有变化,如果发生了变化,手机的 BLE API 就会收到一个监听的回调。

DesCriptor

任何在特性中的属性不是定义为属性值就是为描述符。描述符是一个额外的属性以提供更多特性的信息,它提供一个人类可识别的特性描述的实例。然而,有一个特别的描述符值得特别地提起:客户端特性配置描述符(Client Characteristic Configuration Descriptor,CCCD),这个描述符是给任何支持通知或指示功能的特性额外增加的。在CCCD中写入“1”使能通知功能,写入“2”使能指示功能,写入“0”同时禁止通知和指示功能。

使用过程

常采用的模式是主机模式,然后扫描客户端硬件,然后连接,获取相关服务和特性,然后进行数据传输。

Android 蓝牙BLE开发完全指南

扫描

权限获取


<uses-permission android:name="android.permission.BLUETOOTH"/> 使用蓝牙所需要的权限
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/> 使用扫描和设置蓝牙的权限(申明这一个权限必须申明上面一个权限)

在Android5.0之前,是默认申请GPS硬件功能的。而在Android 5.0 之后,需要在manifest 中申明GPS硬件模块功能的使用。


   <!-- Needed only if your app targets Android 5.0 (API level 21) or higher. -->
   <uses-feature android:name="android.hardware.location.gps" />

在 Android 6.0 及以上,还需要打开位置权限。如果应用没有位置权限,蓝牙扫描功能不能使用(其它蓝牙操作例如连接蓝牙设备和写入数据不受影响)。


   <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>

除了上面的设置之外,如果想设置设备只支持 BLE,可以加上下面这句话


   <uses-feature android:name="android.hardware.bluetooth_le" android:required="true"/>

同样,如果不想添加 BLE 的支持,那么可以设置 required="false"

然后可以在运行时判断设备是否支持 BLE,


   // Use this check to determine whether BLE is supported on the device. Then
   // you can selectively disable BLE-related features.
   if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
       Toast.makeText(this, R.string.ble_not_supported, Toast.LENGTH_SHORT).show();
       finish();
   }

初始化

判断 BLE 在设备上是否支持,如果不支持的话,那么可以不用继续后面的操作了;如果支持,但是有可能蓝牙被禁掉了,因为开着蓝牙比较好点,用户一般都会关闭蓝牙,这时候可以发送请求,来打开蓝牙,可以通过两个步骤来完成。

1.获取 BluetoothAdapter

BluetoothAdapter 对于一个设备来说唯一的,整个系统或者应用,对蓝牙进行操作时都是需要这个的适配器。它的获取需要通过系统服务来获取。


   private BluetoothAdapter mBluetoothAdapter;
   ...
   // Initializes Bluetooth adapter.
   final BluetoothManager bluetoothManager =
           (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
   mBluetoothAdapter = bluetoothManager.getAdapter();

2.打开蓝牙

一般对于用户来说,在手机上蓝牙是关闭,当开启你的应用时就需要开启蓝牙,有两种方式,一种是跳转到设置界面,由用户自己开启蓝牙;

另外一种时,直接在应用开启蓝牙,不需要用户打开,而是直接帮用户开启手机上的蓝牙。

跳转到设置界面


// Ensures Bluetooth is available on the device and it is enabled. If not,
// displays a dialog requesting user permission to enable Bluetooth.
if (mBluetoothAdapter == null || !mBluetoothAdapter.isEnabled()) {
   Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
   startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
}

直接开启蓝牙


   // 打开蓝牙
   if (!mBluetoothAdapter.isEnabled()) {
       mBluetoothAdapter.enable();
   }

扫描

扫描蓝牙设备可以通过startLeScan(),其中有一个参数是 ScanCallback,通过它返回扫描结果,因为扫描过程是很耗电的,所以在扫描过程需要保证

1.一旦找到目标设备,需要停止扫描

2.扫描不要设置循环,而且需要设置一个时间

回调如下


// 设备扫描回调
   private ScanCallback mScanCallback = new ScanCallback() {
       @Override
       public void onScanResult(int callbackType, final ScanResult result) {

runOnUiThread(new Runnable() {
               @Override
               public void run() {

// 广播的信息,可以在result中获取
                   MDevice mDev = new MDevice(result.getDevice(), result.getRssi());
                   if (!mList.contains(mDev)) {
                       mList.add(mDev);
                   }
                   if (mList.size() > 0) {
                       mScanner.stopScan(mScanCallback);
                       Toast.makeText(MainActivity.this, "扫描结束,设备数 " + mList.size()
                               , Toast.LENGTH_SHORT).show();
                   }
               }
           });
       }
   };

开始扫描


private BluetoothAdapter mBluetoothAdapter;
   private boolean mScanning;
   private Handler mHandler;

// Stops scanning after 10 seconds.
   private static final long SCAN_PERIOD = 10000;
   ...
   private void scanLeDevice(final boolean enable) {
       if (enable) {
           // Stops scanning after a pre-defined scan period.
           mHandler.postDelayed(new Runnable() {
               @Override
               public void run() {
                   mScanning = false;
                   mBluetoothAdapter.stopLeScan(mLeScanCallback);
               }
           }, SCAN_PERIOD);

mScanning = true;
           mBluetoothAdapter.startLeScan(mLeScanCallback);
       } else {
           mScanning = false;
           mBluetoothAdapter.stopLeScan(mLeScanCallback);
       }
       ...
   }

这里遇到一个坑,就是实际中手机与一些智能硬件连接时,也就是需要连接指定的硬件,设备有一个UUID,所以可以通过如下方法连接


   startLeScan(UUID[], BluetoothAdapter.LeScanCallback)

但是实际中使用时,连接时会出错,仍需要再次验证。

我当时的做法是采用了另外一种方法,当时这种方法,要求 API 高于 21。


   private void scanLeDevice() {

//50秒后停止扫描
       mHander.postDelayed(stopScanRunnable, 50000);

List<ScanFilter> filters = new ArrayList<>();
       ScanFilter filter = new ScanFilter.Builder()
               //"D8:B0:4C:E8:66:DC" 测试MAC 1
               //"D8:B0:4C:E2:45:2A"  测试MAC 2
               .setDeviceAddress("D8:B0:4C:E2:45:2A")
               .build();
       filters.add(filter);

// 扫描
       mScanner.startScan(filters, new ScanSettings.Builder().setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY).build(), mScanCallback);

}

扫描结束后需要停止扫描


boolean startLeScan(BluetoothAdapter.LeScanCallback callback)

连接

设备连接

通过扫描能够获得设备 BluetoothDevice,包含地址和名字

通过设备连接并获取 BluetoothGatt,后面通过 BluetoothGatt 的实例来进行client的操作,如使用该实例去发现服务,获取读、写、通知等属性


public static BluetoothGatt mBluetoothGatt;

mBluetoothGatt = device.connectGatt(context, false, mGattCallback);

通过连接回调来监听连接的状态,包含三种状态,连接、断开、正在连接,根据状态可以发送广播,

在接收广播的位置进行做相应的处理


   private final static BluetoothGattCallback mGattCallback = new BluetoothGattCallback() {
       @Override
       public void onConnectionStateChange(BluetoothGatt gatt, int status,
                                           int newState) {

String intentAction;
           // GATT Server connected
           if (newState == BluetoothProfile.STATE_CONNECTED) {
               System.out.println("---------------------------->已经连接");
               intentAction = ACTION_GATT_CONNECTED;
               mConnectionState = STATE_CONNECTED;
               broadcastConnectionUpdate(intentAction);
           }
           // GATT Server disconnected
           else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
               System.out.println("---------------------------->连接断开");
               intentAction = ACTION_GATT_DISCONNECTED;
               mConnectionState = STATE_DISCONNECTED;
               broadcastConnectionUpdate(intentAction);

}
           // GATT Server disconnected
           else if (newState == BluetoothProfile.STATE_DISCONNECTING) {
               System.out.println("---------------------------->正在连接");
//                intentAction = ACTION_GATT_DISCONNECTING;
//                mConnectionState = STATE_DISCONNECTING;
//                broadcastConnectionUpdate(intentAction);
           }
       }
   }

当操作完成后,需要关闭连接,必须调用 BluetoothGatt#close 方法释放连接资源

发现服务

由于有了 mBluetoothGatt,就可以去发现服务,再通过服务去获取可以操作的属性


mBluetoothGatt.discoverServices();

发现服务以及获取其他属性,如write和read,notify,Descriptor相关的属性,均是在 BluetoothGattCallback 中有回调,在回调中就可以通过发送广播,然后在其他位置做处理,

如接收数据就会有回调,然后将数据传递出去,对数据解析等


@Override
       public void onServicesDiscovered(BluetoothGatt gatt, int status) {
           // GATT Services discovered
           //发现新的服务
           if (status == BluetoothGatt.GATT_SUCCESS) {
               System.out.println("---------------------------->发现服务");
               broadcastConnectionUpdate(ACTION_GATT_SERVICES_DISCOVERED);
           }
       }

//通过 Descriptor 写监听
       @Override
       public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor,
                                     int status) {

}

// 通过 Descriptor 读监听
       @Override
       public void onDescriptorRead(BluetoothGatt gatt, BluetoothGattDescriptor descriptor,
                                    int status) {

}

@Override
       public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic
               characteristic, int status) {
           //write操作会调用此方法
           if (status == BluetoothGatt.GATT_SUCCESS) {
               System.out.println("onCharacteristicWrite ------------------->write success");
               Intent intent = new Intent(ACTION_GATT_CHARACTERISTIC_WRITE_SUCCESS);
               mContext.sendBroadcast(intent);
           } else {
               Intent intent = new Intent(ACTION_GATT_CHARACTERISTIC_ERROR);
               intent.putExtra(Constants.EXTRA_CHARACTERISTIC_ERROR_MESSAGE, "" + status);
               mContext.sendBroadcast(intent);
           }
       }

@Override
       public void onCharacteristicRead(BluetoothGatt gatt,
                                        BluetoothGattCharacteristic characteristic, int status) {
           // 接收数据
       }

@Override
       public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {

//notify 会回调用此方法
           broadcastNotifyUpdate(characteristic);
       }

@Override
       public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) {
          super.onMtuChanged(gatt, mtu, status);

}

数据传输

1、数据读取

这里有两个方法:

方法一: 一般数据读取的话,想到的是用 read 属性,所以需要获取特定通道的 BluetoothGattCharactristic。

1)BluetoothGatt#getService 得到服务

2)BluetoothGattService#getCharactristic 获取 BluetoothGattCharactristic,这里获取的 BluetoothGattCharactristic 是有指定 UUID 的,也就是说不同的 Charactristic的 UUID 是不同的,读和写的通道不同,根据不同的操作,然后通过UUID获取相应的通道

3)BluetoothGattCharactristic#readCharacteristic 方法可以通知系统去读取特定的数据

4)BluetoothGattCallback#onCharacteristicRead 方法。通过 BluetoothGattCharacteristic#getValue 可以读取到蓝牙设备的数据

方法二:采用 notify 属性,客户端发送数据,服务端监听属性变化,然后根据 属性的 UUID 判断是否是 notify 的属性,如果是的话,说确实是由远程设备发过来的数据。

1)BluetoothGatt#getService 得到服务

2)BluetoothGattService#getCharactristic 获取 BluetoothGattCharactristic,这里的这个属性是 notify 属性

3)获得属性后需要进行判断设备是否支持notify操作,然后再设备打开notify通知


void prepareBroadcastDataNotify(
           BluetoothGattCharacteristic characteristic) {

final int charaProp = characteristic.getProperties();

Toast.makeText(this, " " + charaProp, Toast.LENGTH_SHORT).show();

if ((charaProp | BluetoothGattCharacteristic.PROPERTY_NOTIFY) > 0) {
           BluetoothLeService.setCharacteristicNotification(characteristic, true);
       }

}

4) 设置属性时,也要通知远程设备端也要开启 notify 属性


public static void setCharacteristicNotification(BluetoothGattCharacteristic characteristic, boolean enabled) {

if (mBluetoothAdapter == null || mBluetoothGatt == null) {
           return;
       }
       //通知远程端开启 notify
       if (characteristic.getDescriptor(UUID.fromString(GattAttributes.CLIENT_CHARACTERISTIC_CONFIG)) != null) {
           if (enabled == true) {
               BluetoothGattDescriptor descriptor = characteristic
                       .getDescriptor(UUID.fromString(GattAttributes.CLIENT_CHARACTERISTIC_CONFIG));
               descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
               mBluetoothGatt.writeDescriptor(descriptor);
           } else {
               BluetoothGattDescriptor descriptor = characteristic
                       .getDescriptor(UUID.fromString(GattAttributes.CLIENT_CHARACTERISTIC_CONFIG));
               descriptor.setValue(BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE);
               mBluetoothGatt.writeDescriptor(descriptor);
           }
       }
       mBluetoothGatt.setCharacteristicNotification(characteristic, enabled);
   }

数据写入

对于 BLE 方式的数据传输来说,数据的大小是有限制的,一次性最多可以传输512个字节,这也是BLE小数据量传输的特点,另外,对于每次传输,也有限制,每个数据包大小不超过20个字节,超过20个字节的话,需要分包处理。写的步骤和读取类似。

1)BluetoothGatt#getService 得到服务

2)BluetoothGattService#getCharactristic 获取 BluetoothGattCharactristic,这里的这个属性是 write 属性

3)写入字节数据


public static void writeCharacteristicGattDb(
           BluetoothGattCharacteristic characteristic, byte[] byteArray) {

if (mBluetoothAdapter == null || mBluetoothGatt == null) {
           return;
       } else {
           byte[] valueByte = byteArray;
           characteristic.setValue(valueByte);
           mBluetoothGatt.writeCharacteristic(characteristic);
       }
   }

4)对于手机端,写入数据后,远程端会接受,同时回调中也会能够接收,也可以在回调中做一下数据判断,看是否是自己发出的数据


@Override
       public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic
               characteristic, int status) {
           //write操作会调用此方法
           if (status == BluetoothGatt.GATT_SUCCESS) {
               System.out.println("onCharacteristicWrite ------------------->write success");
               Intent intent = new Intent(ACTION_GATT_CHARACTERISTIC_WRITE_SUCCESS);
               // 这里通过属性能够读取你发送的数据,可以对此数据进行判断
               characteristic.getValue();
               mContext.sendBroadcast(intent);
           } else {
               Intent intent = new Intent(ACTION_GATT_CHARACTERISTIC_ERROR);
               intent.putExtra(Constants.EXTRA_CHARACTERISTIC_ERROR_MESSAGE, "" + status);
               mContext.sendBroadcast(intent);
           }
       }

其他

一般在通讯过程中,需要有连接的心跳包,来检测是否仍处于连接状态,可以通过设置定时器,主机端定时 write 数据,客户端定时 notify 数据

断开连接

操作完成,需要断开蓝牙并释放资源,通过 BluetoothGatt#disconnect 断开连接,然后回调中会收到断开的监听,可以根据状态释放资源。BluetoothGattCallback#onConnectionStateChange回调中通过这个方法的 newState 参数可以判断是连接成功还是断开成功的回调,断开成功的话,然后调用 BluetoothGatt#close 方法释放资源

参考

官方文档

Android 开发入门介绍

通用属性配置文件(GATT)及其服务,特性与属性介绍

来源:https://blog.csdn.net/u011371324/article/details/80568230

标签:android,蓝牙,ble
0
投稿

猜你喜欢

  • 浅谈Java封装、继承、多态特性

    2023-10-09 13:59:35
  • 深入Android HandlerThread 使用及其源码完全解析

    2023-11-30 19:16:56
  • 详谈@Autowired和static的关系

    2022-06-14 05:51:50
  • Android实现上拉加载更多ListView(PulmListView)

    2023-11-03 19:19:44
  • Java实现Redis的集合(set)命令操作

    2022-05-08 11:07:03
  • Recyclerview添加头布局和尾布局、item点击事件详解

    2022-04-19 12:38:12
  • 深入了解Spring中最常用的11个扩展点

    2023-07-05 17:46:40
  • android开发之关闭所有的activity的方法

    2022-01-16 18:21:29
  • 拉钩网java笔试题分享

    2022-02-13 08:48:25
  • Android禁止EditText自动弹出软键盘的方法及遇到问题

    2021-07-18 06:35:55
  • Java基于Calendar类输出指定年份和月份的日历代码实例

    2023-09-20 07:40:57
  • FasfDFS整合Java实现文件上传下载功能实例详解

    2022-05-27 02:30:46
  • C# 字符串与unicode互相转换实战案例

    2023-12-23 09:29:54
  • Kotlin中的惰性操作容器Sequence序列使用原理详解

    2023-10-01 14:21:55
  • Java抽象类和抽象方法定义与用法实例详解

    2023-10-02 14:30:13
  • Android实现自定义轮播图片控件示例

    2021-09-19 01:27:05
  • 浅谈java 执行jar包中的main方法

    2022-06-28 07:58:27
  • C# winfrom 模拟ftp文件管理实现代码

    2023-07-15 16:29:48
  • Android画板开发之基本画笔功能

    2023-01-09 07:26:21
  • c语言定时器示例分享

    2023-11-04 09:44:52
  • asp之家 软件编程 m.aspxhome.com