Android BLE 蓝牙开发,连接蓝牙设备进行通讯

2023-07-14 11:05:58 浏览数 (1)

1. 介绍

本篇主要基于 Android 官方的低功耗蓝牙连接服务。

讲解如何通过 UUID 连接蓝牙设备。如果你针对 GATT 服务不太了解。那么这篇应该能够稍微帮助到你。

官方文档地址:https://developer.android.google.cn/guide/topics/connectivity/bluetooth-le?hl=zh_cn#connect

2. 概念

如果是老用户了,那么就应该知道曾经蓝牙设备是一个高耗电的部件。根本不可能长时间开启。而在蓝牙4.0版本之后,蓝牙的通讯,耗电,抗干扰都得到了显著提升。同时蓝牙成本也得到了降低。

然后才有了我们现在的各种穿戴设备例如手环,蓝牙耳机,蓝牙电子秤,蓝牙音箱等等的爆发。

同时,其他工业或者外置设备也都开始大量支持蓝牙通讯。因为能耗和成本降低了。

针对低功耗蓝牙通讯,Android 4.3(API 18)开始引入了 BLE 库。我们可以直接使用 Android SDK 中的蓝牙 BLE 库,而不用额外导入依赖库。

以前开发蓝牙通讯,还需要实现蓝牙配对。需要主动跳转到手机设置界面进行PIN码配对,然后配对通过之后才能进行蓝牙链接。 而使用BLE库,我们可以直接通过蓝牙设备的UUID进行连接(通过GATT服务),在当前应用内就能直接连接了。而不用通过系统设置。 市面上的各种手环的自动匹配链接,电子秤的自动连接等等都是通过GATT进行通讯和链接的。

2.1 术语

  • GATT:全称为:Generic Attribute Profile,翻译为:通用属性配置文件。GATT 配置文件是一种通用规范,内容针对在 BLE 链路上发送和接收称为“属性ATT”的简短数据片段。目前所有低功耗应用配置文件均以 GATT 为基础。
  • ATT:全称为:Attribute Protocol,翻译为:属性协议。它是 GATT 的构建基础,二者的关系也被称为 GATT/ATT。每个属性均由通用唯一标识符 (UUID) 进行唯一标识,后者是用于对信息进行唯一标识的字符串 ID 的 128 位标准化格式。由 ATT 传输的属性采用特征服务格式。
  • 特征 Characteristic: 特征包含一个值和 0 至多个描述特征值的描述符。您可将特征理解为类型,后者与类类似。
  • 描述符:描述符是描述特征值的已定义属性。例如,描述符可指定人类可读的描述、特征值的可接受范围或特定于特征值的度量单位。
  • Service — 服务是一系列特征。例如,您可能拥有名为“心率监测器”的服务,其中包括“心率测量”等特征。

以上术语的介绍来源于Android官网

2.2 通讯过程

假如我们有一个蓝牙外置设备(Device),然后有一个支持蓝牙的移动设备(Phone)。两者之间的通讯方式步骤是:

  1. Device 开启蓝牙。(通常这些设备都是开机之后,就默认开启蓝牙了)
  2. Phone 开启蓝牙。
  3. Phone 发现 Device。
  4. Phone 与 Device 创建蓝牙连接。
  5. Phone 创建 Gatt 客户端,与 Device Gatt 服务端连接。
  6. Phone 通过 Gatt 服务功能获取 Device 中的消息,并发送消息给 Device 设备。

整个过程就是这样的。下面我也将按照这个通讯过程进行介绍。

3.开发

基于我的使用情况,从无到有的介绍,完整的蓝牙开发配置过程。给大家一个参考

语言主要为 Java

3.1 权限

要在应用中使用蓝牙功能,必须声明 BLUETOOTH 蓝牙权限。需要此权限才能执行任何蓝牙通信,例如请求连接、接受连接和传输数据等。

同时,还需要位置权限。因为蓝牙 LE 信标通常与位置相关联。如果不开启 ACCESS_FINE_LOCATION 权限。那么我们将会无法发现蓝牙设备。

也就是执行蓝牙扫描 API 无法得到任何结果(PS::Logcat 中的错误日志会告诉你,要开启位置权限,否则无法扫描发现蓝牙设备)

代码语言:javascript复制
    <!-- 蓝牙搜索配对 -->
    <uses-permission android:name="android.permission.BLUETOOTH" />
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
    <uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
    <!--    操纵蓝牙的开启-->
    <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />

    <!-- 如果应用必须安装在支持蓝牙的设备上,可以将下面的required的值设置为true。-->
    <uses-feature
        android:name="android.hardware.bluetooth_le"
        android:required="false" />

其中 android.permission.ACCESS_FINE_LOCATION 是高版本API 28 权限。如果要支持更低版本,就需要申请<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />

如果要执行蓝牙扫描功能,我们需要申请:<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />权限

如果要执行蓝牙链接,开关蓝牙。需要申请:<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />权限

而上面两个权限呢,是在 API 31 上才有效。而低版本就是申请:

<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />权限也就够了。

权限配置完毕之后,就是代码开发了。

不管是高版本,还是低版本。将权限都申请可以说最稳妥了。

3.2 检测设备是否支持蓝牙

通常情况下,手机是有蓝牙的。而我们如果在其他 Android 系统的设备中,例如TV,平板,一体机等等。是否有蓝牙还真不能完整保证。

如果不确定的情况下,那么可以通过以下代码检查 BLE 的可用性。

代码语言:javascript复制
 @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
            //不支持蓝牙设备
            finish();
        } else {
            //支持蓝牙设备
    }

蓝牙是否开启都不影响检查结果。它检查的是设备是否有蓝牙功能,而不是蓝牙是否启动,下面会介绍如何判断蓝牙是否启动

3.3 开启蓝牙

当我们设备也支持蓝牙了,权限也配置了。下一步就是获取 BluetoothAdapter 对象了。

代码语言:javascript复制
final BluetoothManager bluetoothManager =(BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
BluetoothAdapter bluetoothAdapter = bluetoothManager.getAdapter();

我们后续控制蓝牙的状态,都是通过该方法实现的。

首先,检测蓝牙是否开启。可以通过isEnabled()方法进行检测:

代码语言:javascript复制
if (bluetoothAdapter == null || !bluetoothAdapter.isEnabled()) {
    //开启设备的蓝牙链接
    bluetoothAdapter.enable();//开启蓝牙
    //动态判断是否拥有位置权限ACCESS_COARSE_LOCATION 或ACCESS_FINE_LOCATION ,然后再执行蓝牙扫描
} else {
   //动态判断是否拥有位置权限ACCESS_COARSE_LOCATION 或ACCESS_FINE_LOCATION,然后再执行蓝牙扫描
}

我们其实可以直接使用bluetoothAdapter.enable()开启蓝牙。当蓝牙没有开启时,我们可以直接开启蓝牙。

这个方法的结果,并不是实时返回的。我们如果要知道蓝牙是否开启,需要监听蓝牙状态的广播才行。下面会介绍广播监听。

PS:这个方法需要android.Manifest.permission.BLUETOOTH_CONNECT 权限才能使用。

官方是建议我们通过Intent让系统设置进行开启蓝牙的。

代码语言:javascript复制
if (bluetoothAdapter == null || !bluetoothAdapter.isEnabled()) {
    Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
    startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
}

但是现在startActivityForResult方法已经过时。我们可以使用Launcher来调用:

代码语言:javascript复制
ActivityResultLauncher<Intent> launcher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> {
            if (result.getResultCode() == RESULT_OK) {
                //处理返回结果
            }
});

上面的 launcher需要在ActivityonCreate 方法中初始化。然后在需要进行蓝牙设置界面启动的地方配置:

代码语言:javascript复制
Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE); //创建一个蓝牙启动的意图
launcher.launch(enableBtIntent);//使用launcer启动这个意图就可以了。

我们如果使用bluetoothAdapter.enable();时Android Studio出现代码错误警告,可以在该代码使用的方法中添加:@SuppressLint("MissingPermission")注解。

3.4 广播监听

其实这个广播监听,是否需要。根据大家实际情况来定。不一定需要。

首先,创建一个动态广播对象:

代码语言:javascript复制
 public class BluetoothFoundReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        String action = intent.getAction();
        //监听蓝牙状态之后,发送消息
        try {
            if (BluetoothAdapter.ACTION_DISCOVERY_STARTED.equals(action)) {
                //开始扫描
            } else if (BluetoothAdapter.ACTION_DISCOVERY_FINISHED.equals(action)) {
                //结束扫描
            } else if (BluetoothDevice.ACTION_FOUND.equals(action)) {
                //发现设备,每扫码到一个设备,都会触发一次
                 BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
                //我们可以得到蓝牙设备
            } else if (action.equals(BluetoothAdapter.ACTION_STATE_CHANGED)) {
                //蓝牙开关状态
               
                int statue = bluetoothAdapter.getState();
                switch (statue) {
                    case BluetoothAdapter.STATE_OFF:
                        Log.e(TAG, "蓝牙状态:,蓝牙关闭");
                        break;
                    case BluetoothAdapter.STATE_ON:
                        Log.e(TAG, "蓝牙状态:,蓝牙打开");
                       
                        break;
                    case BluetoothAdapter.STATE_TURNING_OFF:
                        Log.e(TAG, "蓝牙状态:,蓝牙正在关闭");
                        break;
                    case BluetoothAdapter.STATE_TURNING_ON:
                        Log.e(TAG, "蓝牙状态:,蓝牙正在打开");
                        break;
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

然后进行广播注册:

代码语言:javascript复制
 bluetoothFoundReceiver = new BluetoothFoundReceiver();
        IntentFilter filter = new IntentFilter();
        filter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED);//连接蓝牙,断开蓝牙
        filter.addAction(BluetoothDevice.ACTION_FOUND);//找到设备的广播
        filter.addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED);//搜索完成的广播
        filter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED);//状态改变 配对开始时,配对成功时
        registerReceiver(bluetoothFoundReceiver, filter);

注册完毕后,在onDestroy方法中需要注销注册:

代码语言:javascript复制
  @Override
    protected void onDestroy() {
        if (bluetoothFoundReceiver != null)
            unregisterReceiver(bluetoothFoundReceiver); //停止监听
        super.onDestroy();
    }

其实,我们只需要蓝牙状态的监听就可以了BluetoothAdapter.ACTION_STATE_CHANGED 其他的设备查找,配对。可以不用,因为触发到广播的设备查找效率太低,而且多次重复查找时,还会出现耗时变长。设备无法查找到的情况。

3.5 蓝牙设备查找

官方文档上推荐的查找方式是:

代码语言:javascript复制
bluetoothAdapter.startLeScan(leScanCallback);  //查找
bluetoothAdapter.stopLeScan(leScanCallback); //停止查找

可是现在这个方法也过时了。替换方法是:

代码语言:javascript复制
BluetoothLeScanner scanner = bluetoothAdapter.getBluetoothLeScanner();
  //不进行权限验证
 ScanCallback callback = new ScanCallback() {
            @Override
            public void onScanResult(int callbackType, ScanResult result) {
                BluetoothDevice device = result.getDevice();//得到设备
//                Log.e(TAG, "发现设备"   device.getName());
            }

            @Override
            public void onScanFailed(int errorCode) {
                super.onScanFailed(errorCode);
                Log.e(TAG, "搜索错误"   errorCode);
            }
        };
scanner.startScan(callback);

onScanResult方法是一个在子线程触发的回调,我们不能在该方法中直接操作UI对象。

其次,扫描到一个蓝牙设备就会触发一次消息回调。我们可以得到一个BluetoothDevice对象。 也就是说这个方法中会触发多次回调,

所以建议,在扫描到我们的蓝牙设备之后,主动调用scanner.stopScan(callback);停止扫描。

PS:这种查找方式,不会触发蓝牙的遍历广播。我们如果开启广播进行监听设备扫描情况。如果通过startScan方法,广播中不会有回调。

上面是一个通用搜索模式,我们还可以配置自己的过滤条件。例如:

代码语言:javascript复制
ScanFilter sn =new ScanFilter.Builder().setDeviceName("蓝牙设备的名称").setServiceUuid(ParcelUuid.fromString("我们的设备的Service UUID")).build();
List<ScanFilter> scanFilters=new ArrayList<>();
scanFilters.add(sn);
scanner.startScan(scanFilters, new ScanSettings.Builder().build(),callback);

其中ScanFilter对象,我们可以配置我们想查找的蓝牙设备的信息。可以是setDeviceNamesetServiceUuidsetDeviceAddresssetServiceSolicitationUuid等。

ScanSettings对象是可以定义我们的扫描模式,通过配置该项可以提高扫描效率。

默认情况下,执行的是:SCAN_MODE_LOW_POWER在低功耗模式下执行蓝牙LE扫描。 这是默认的扫描模式,因为它消耗最少的电量。

3.5.1 startDiscovery

如果上面的方法还不满足我们的情况,可以使用:

代码语言:javascript复制
if (bluetoothAdapter.isDiscovering()) {//是否在扫描
   bluetoothAdapter.cancelDiscovery(); //停止扫描
}
//查找蓝牙
bluetoothAdapter.startDiscovery();

我们可以直接使用bluetoothAdapter进行扫描。这个方法触发之后是由系统进行蓝牙扫描。就和我们在手机的设置界面中点击蓝牙扫描一样。

上面的这个方法没有回调,因为所有的蓝牙设备的发现都将通过广播事件进行传递。

需要通过我上面的广播监听介绍的内容。进行实时获取到扫描到的设备。

使用上面的方法有几个缺点:

1.效率慢,耗时很长。

2.重复扫描会失败。不能说是失败了,而是系统会将重复扫描的请求进行阻止,关键的问题在于这个阻止操作是手机厂商定制的。

PS:不管是BluetoothLeScanner 还是bluetoothAdapter.startDiscovery() 去查找蓝牙设备。都不建议一直重复扫描。否则会出现无法扫描到设备,没有任何扫描结果等等情况。因为扫描是一个耗时耗电的操作。

3.6 链接Gatt

当我们扫描到了蓝牙设备之后,就会获取到BluetoothDevice对象。然后我们通过BluetoothDevice对象创建GATT服务进行后续的蓝牙通讯。

代码语言:javascript复制
BluetoothDevice device;// 当我们通过扫描得到device对象之后,进行Gatt服务创建
BluetoothGatt bluetoothGatt = device.connectGatt(this, false, gattCallback);

第一个传参context没有什么可以介绍的。

第二个传参autoConnect:是一个boolean值对象,false代表直接连接到蓝牙设备。true代表在蓝牙设备可用时自动连接。

第三个参数BluetoothGattCallback 是Gatt服务的各种回调了。

我们通过gattCallback回调的内容,来得到与蓝牙设备的链接状态,数据通信内容等。

下面来详细介绍下BluetoothGattCallback对象的几个方法。

代码语言:javascript复制
String SERVICE_UUID="00000-000000-000000-000000";//这个是我要链接的蓝牙设备的ServiceUUID

BluetoothGattCallback gattCallback = new BluetoothGattCallback() {
            //GATT的链接状态回调
            @Override
            public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
                super.onConnectionStateChange(gatt, status, newState);
                if (newState == BluetoothProfile.STATE_CONNECTED) {
                    gatt.discoverServices();
                    Log.v(TAG, "连接成功");
                } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
                    Log.e(TAG, "连接断开");
                } else if (newState == BluetoothProfile.STATE_CONNECTING) {
                      //TODO 在实际过程中,该方法并没有调用
                        Log.e(TAG, "连接中....");
                }
            }
        //获取GATT服务发现后的回调
     @Override
    public void onServicesDiscovered(BluetoothGatt gatt, int status) {
        if (status == BluetoothGatt.GATT_SUCCESS) {
            Log.e(TAG, "GATT_SUCCESS"); //服务发现
            for (BluetoothGattService bluetoothGattService : gatt.getServices()) {
                Log.e(TAG, "Service_UUID"   bluetoothGattService.getUuid()); // 我们可以遍历到该蓝牙设备的全部Service对象。然后通过比较Service的UUID,我们可以区分该服务是属于什么业务的
                if (SERVICE_UUID.equals(bluetoothGattService.getUuid().toString())) {
                   
                    for (BluetoothGattCharacteristic characteristic : bluetoothGattService.getCharacteristics()) {
                        prepareBroadcastDataNotify(gatt, characteristic); //给满足条件的属性配置上消息通知
                    }
                    return;//结束循环操作
                }
            }
        } else {
            Log.e(TAG, "onServicesDiscovered received: "   status);
        }
    }
    
    //蓝牙设备发送消息后的自动监听
    @Override
    public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
        // readUUID 是我要链接的蓝牙设备的消息读UUID值,跟通知的特性的UUID比较。这样可以避免其他消息的污染。
        if (READ_UUID.equals(characteristic.getUuid().toString())) {
            try {
                String chara = new String(characteristic.getValue(), "UTF-8");
                Log.e(TAG, "消息内容:"   chara);
            } catch (UnsupportedEncodingException e) {
                e.printStackTrace();
            }
        }
    }
};

我们可以通过链接成功和链接断开。来判断我们当前与蓝牙设备的通讯状态。

当我们比对Service的UUID成功之后, 我们就可以获取ServiceCharacteristic对象。该对象也就是特征。通过注册特征来实现消息的监听和发送业务。

3.7 注册消息监听-setCharacteristicNotification

代码语言:javascript复制
 @SuppressLint("MissingPermission")
    private void prepareBroadcastDataNotify(BluetoothGatt mBluetoothGatt, BluetoothGattCharacteristic characteristic) {
        Log.e(TAG, "CharacteristicUUID:"   characteristic.getUuid().toString());
        int charaProp = characteristic.getProperties();
        //判断属性是否支持消息通知
        if ((charaProp | BluetoothGattCharacteristic.PROPERTY_NOTIFY) > 0) {
            BluetoothGattDescriptor descriptor =
                    characteristic.getDescriptor(UUID.fromString(UUIDManager.READ_DEDSCRIPTION_UUID));
            if (descriptor != null) {
                //注册消息通知
                mBluetoothGatt.setCharacteristicNotification(characteristic, true);
                descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
                mBluetoothGatt.writeDescriptor(descriptor);
            }
        }
    }

在上面的示例中:READ_DEDSCRIPTION_UUID = "00002902-0000-1000-8000-00805f9b34fb" 是固定的,不管你链接什么样的蓝牙设备。

在注册消息监听,都是使用UUID值是00002902-0000-1000-8000-00805f9b34fb进行的。这个是Android系统保留的。用于动态监听的。

你如果不想使用这个动态监听。就需要自己写线程主动去轮询获取到蓝牙设备发送过来的消息了。

到这里,我们其实就能够实现蓝牙设备的实时监听,并得到消息内容了。

3.8 写数据到蓝牙设备中

我们如果想将内容推送到蓝牙设备中,在发现服务的时候onServicesDiscovered 遍历特性中,确保是用于写消息的特性对象后。选择持有该特性,然后通过:

代码语言:javascript复制
String data ="0x12";
BluetoothGattCharacteristic writeCharact = bluetoothGattService.
                getCharacteristic(UUID.fromString(WRITE_UUID));
        //查找UUID是写的特性,并检测是否拥有写权限
        if (writeCharact == null || writeCharact.getProperties() != BluetoothGattCharacteristic.PROPERTY_WRITE) {
            return ;//该特性没有写的权限。所以无法传入
        }
        // 当数据传递到蓝牙之后
        // 会回调BluetoothGattCallback里面的write方法
        writeCharact.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE);
        // 将需要传递的数据转为16进制数
        writeCharact.setValue(data);
        bluetoothGatt.writeCharacteristic(writeCharact);

3.9 关闭连接

当蓝牙通讯结束,或者界面关闭时。我们需要关闭GATT服务,减少资源占用。

代码语言:javascript复制
if (bluetoothGatt != null) {
            bluetoothGatt.close();
            bluetoothGatt.disconnect();
            bluetoothGatt = null;
}

也可以关闭BluetoothGattCallback 的回调监听:

代码语言:javascript复制
gattCallback.disConnectBlue();//关闭GATT服务回调监听

4. 小结

到这里蓝牙的链接和读取就结束了。

我们通过bluetoothAdapter 查找到蓝牙设备之后,再通过GATT服务进行蓝牙设备与手机之间的配对。直接比对UUID,而不再需要PIN码进行配对了。

(PS:有些安全性要求比较高的设备,还是会需要主动进行PIN码配对。PIN配队就只能通过系统设备界面中的蓝牙功能项进行操作了。)

通过GATT服务连接成功后。就可以查询该Server下的各种特性了,不同的特性对应了一个功能。有发消息的特性,也有用于收消息的特性。

同时一个蓝牙设备对象,可能有多种服务功能。

如果不想自己写线程变量轮询设备发送过来的消息,就通过注册消息监听。让BLE框架帮我们进行轮询之后,再通知到我们。

如果觉得总结的还可以,希望能够点个赞鼓励一下,谢谢。

0 人点赞