欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Android蓝牙BLE开发(二)——对BLE设备的扫描连接以及读写数据

程序员文章站 2024-03-24 23:01:40
...

前言

任何例子都不如官方Demo来得实在。安装官方Demo时,如果发现搜索不到设备,需要到手机设置界面给官方Demo开启定位权限,因为官方Demo没有动态获取权限,也可以自己在官方Demo上添加几行代码,实现动态获取定位权限。如何获取定位权限可以参考Android高效处理权限——EasyPermissions框架的使用

官方文档
官方Demo
本文代码下载地址:https://github.com/movisens/SmartGattLib

基础介绍

在BLE开发当中各种主要类和其作用:

BluetoothDeivce:蓝牙设备,代表一个具体的蓝牙外设。
BluetoothAdapter:蓝牙适配器,每一台支持蓝牙功能的手机都有一个蓝牙适配器,一般来说,只有一个。
BluetoothManager:蓝牙管理器,主要用于获取蓝牙适配器和管理所有和蓝牙相关的东西。
BluetoothGatt:通用属性协议, 定义了BLE通讯的基本规则,就是通过把数据包装成服务和特征的约定过程。
BluetoothGattCallback:一个回调类,非常重要而且会频繁使用,用于回调GATT通信的各种状态和结果,后面会详细解释。
BluetoothGattCharacteristic:特征,里面包含了一组或多组数据,是GATT通信中的最小数据单元。
BluetoothGattService:服务,描述了一个BLE设备的一项基本功能,由零或多个特征组构成。
BluetoothGattDescriptor:特征描述符,对特征的额外描述,包括但不仅限于特征的单位,属性等。
BluetoothLeScanner:蓝牙适配器里面的扫描器,用于扫描BLE外设。

Android 蓝牙开发示例

第一步:声明所需要的权限

<!--使用蓝牙所需要的权限-->
<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。如果支持,再确认蓝牙是否开启。如果蓝牙没有开启,可以使用 BLuetoothAdapter 类来开启蓝牙。

1.判断是否支持蓝牙,并获取 BluetoothAdapter

    /**
     * 判断是否支持蓝牙
     */
    private boolean checkBleDevice() {
        //首先获取BluetoothManager
        BluetoothManager bluetoothManager =
                (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
        //获取BluetoothAdapter
        if (bluetoothManager != null) {
            BluetoothAdapter mBluetoothAdapter = bluetoothManager.getAdapter();
            if (mBluetoothAdapter != null) {
                mBleAdapter = mBluetoothAdapter;
                return true;
            } else {
                return false;
            }
        } else {
            return false;
        }
    }

2.如果检测到蓝牙没有开启,尝试开启蓝牙

if (!mBleAdapter.isEnabled()) {
	//打开蓝牙
	Intent intent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
	startActivityForResult(intent, OPEN_BT_REQUESTCODE);
	}

第三步:扫描蓝牙设备

外围设备开启蓝牙后,会广播出许多的关于该设备的数据信息,例如 mac 地址,uuid 等等。通过这些数据我们可以筛选出需要的设备。

开启蓝牙扫描

public void startScan(final ScanCallback callback)

停止蓝牙扫描

 public void stopScan(ScanCallback callback)

通过调用 stopScan可以停止正在进行的蓝牙扫描。这里需要注意的是,传入的回调必须是开启蓝牙扫描时传入的回调,否则蓝牙扫描不会停止。

设置回调函数

ScanCallback callback = new ScanCallback() {
	@Override
	public void onScanResult(int callbackType, ScanResult result) {
		super.onScanResult(callbackType, result);
		//对结果处理
		BluetoothDevice bluetoothDevice = result.getDevice();
		int rssi = result.getRssi();
		ScanRecord scanRecord = result.getScanRecord();
	}
};

ScanCallback 回调的方法中,BluetoothDevice获取关于这一个设备的一系列详细的参数,例如名字,MAC 地址等等;rssi是蓝牙的信号强弱指标,通过蓝牙的信号指标,我们可以大概计算出蓝牙设备离手机的距离。计算公式为:d = 10^((abs(RSSI) - A) / (10 * n));ScanRecord是蓝牙广播出来的广告数据。
当执行上面的代码之后,一旦发现蓝牙设备,ScanCallback 就会被回调,直到 stopScan 被调用。出现在回调中的设备会重复出现,所以我们需要通过 BluetoothDevice 获取外围设备的地址手动过滤掉已经发现的外围设备。

代码示例

由于蓝牙扫描的操作比较消耗手机的能量。所以我们不能一直开着蓝牙扫描,必须设置一段时间之后关闭蓝牙扫描。示例代码如下:

    /**
     * 开始关闭搜索蓝牙
     */
    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    public void scanLeDevice(final boolean enable, final ScanCallback callback) {
        if (enable) {
            // Stops scanning after a pre-defined scan period.
            // 预先定义停止蓝牙扫描的时间(因为蓝牙扫描需要消耗较多的电量)
            mHandler.postDelayed(new Runnable() {
                @Override
                public void run() {
                    mScanning = false;
                    mBleAdapter.getBluetoothLeScanner().stopScan(callback);
                }
            }, SCAN_PERIOD);
            mScanning = true;

            // 定义一个回调接口供扫描结束处理
            mBleAdapter.getBluetoothLeScanner().startScan(callback);
        } else {
            mScanning = false;
            mBleAdapter.getBluetoothLeScanner().stopScan(callback);
        }
    }

第四步:连接蓝牙设备

连接

连接蓝牙设备可以通过 BluetoothDevice#ConnectGatt 方法连接,也可以通过BluetoothGatt#connect 方法进行重新连接。以下分别是两个方法的官方说明:

BluetoothDevice#connectGatt
BluetoothGatt connectGatt(Context context, boolean autoConnect, BluetoothGattCallback callback)
  • 第一个参数不解释
  • 第二个参数表示是否需要自动连接。如果设置为 true, 表示如果设备断开了,会不断的尝试自动连接。设置为 false 表示只进行一次连接尝试。
  • 第三个参数是连接后进行的一系列操作的回调,例如连接和断开连接的回调,发现服务的回调,成功写入数据,成功读取数据的回调等等。
BluetoothGatt#connect
boolean connect()

调用这一个方法相当与调用 BluetoothDevice#connectGatt 且第二个参数 autoConnect 设置为 true。

代码示例

    /**
     * 连接设备
     *
     * @param address         设备mac地址
     */
    public void Connect(String address) {

        if (mBleAdapter == null || address == null) {
            LogUtils.e("No device found at this address:" + address);
            return;
        }

        try {
            BluetoothDevice remoteDevice = mBleAdapter.getRemoteDevice(address);
            if (remoteDevice == null) {
                LogUtils.e("Device not found.  Unable to connect.");
                return;
            }
            //第四步:连接蓝牙
            remoteDevice.connectGatt(mContext, false, mGattCallback);
            LogUtils.e("connecting mac-address:$address");
        } catch (Exception e) {
            LogUtils.e("蓝牙地址错误,请重新绑定");
        }
    }

BluetoothGattCallback回调

BluetoothGattCallback是一个虚函数,里面包含很多回调方法,下面在用到后一一讲解。

 public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState)
 public void onServicesDiscovered(BluetoothGatt gatt, int status)
 public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic,int status)
 public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status)
 public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic)
public void onDescriptorRead(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status)
public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status)

...

而连接状态的回调方法如下,

void onConnectionStateChange(BluetoothGatt gatt, int status, int newState)
  • 这一个方法有三个参数,第一个就蓝牙设备的 Gatt 服务连接类。
  • 第二个参数代表是否成功执行了连接操作,如果为 BluetoothGatt.GATT_SUCCESS 表示成功执行连接操作,第三个参数才有效,否则说明这次连接尝试不成功。
    有时候,我们会遇到 status == 133 的情况,根据网上大部分人的说法,这是因为 Android 最多支持连接 6 到 7 个左右的蓝牙设备,如果超出了这个数量就无法再连接了。所以当我们断开蓝牙设备的连接时,还必须调用 BluetoothGatt#close 方法释放连接资源。否则,在多次尝试连接蓝牙设备之后很快就会超出这一个限制,导致出现这一个错误再也无法连接蓝牙设备。
  • 第三个参数代表当前设备的连接状态,如果 newState == BluetoothProfile.STATE_CONNECTED 说明设备已经连接,可以进行下一步的操作了(发现蓝牙服务,也就是 Service)。当蓝牙设备断开连接时,这一个方法也会被回调,其中的 newState == BluetoothProfile.STATE_DISCONNECTED。
    代码示例
        @Override
        public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
            if (newState == BluetoothProfile.STATE_CONNECTED) { //连接成功
            	//第五步:发现服务
                gatt.discoverServices();
                connSuccess();
            } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {   //断开连接
                connFailed("断开连接");
            }
        }

第五步:发现服务

在成功连接到蓝牙设备之后才能进行这一个步骤,也就是说在 BluetoothGattCallback#onConnectionStateChang 方法被成功回调且表示成功连接之后调用 BluetoothGatt#discoverService 这一个方法。当这一个方法被调用之后,系统会异步执行发现服务的过程,直到 BluetoothGattCallback#onServicesDiscovered 被系统回调之后,手机设备和蓝牙设备才算是真正建立了可通信的连接。

服务

BluetoothGatt#discoverService
public boolean discoverServices()

到这一步,我们已经成功和蓝牙设备建立了可通信的连接,接下来就可以执行相应的蓝牙通信操作了,例如写入数据,读取蓝牙设备的数据等等。

BluetoothGattCallback回调

public void onServicesDiscovered(BluetoothGatt gatt, int status)
  • 第一个参数是BluetoothGatt的实例
  • 第二个参数代表当前设备的连接状态,如果status == BluetoothGatt.GATT_SUCCESS说明操作,可以进行下一步的操作了

代码示例

        @Override
        public void onServicesDiscovered(BluetoothGatt gatt, int status) {
            if (status == BluetoothGatt.GATT_SUCCESS) {
                //将所有的特征保存
                List<BluetoothGattService> services = gatt.getServices();
                for (BluetoothGattService bluetoothGattService : services) {
                    Map<String, BluetoothGattCharacteristic> charMap = new HashMap<>();
                    String serviceUuid = bluetoothGattService.getUuid().toString();
                    List<BluetoothGattCharacteristic> characteristics = bluetoothGattService.getCharacteristics();
                    for (BluetoothGattCharacteristic characteristic : characteristics) {
                        charMap.put(characteristic.getUuid().toString(), characteristic);
                    }
                    servicesMap.put(serviceUuid, charMap);
                }
                //根据实际的服务UUID和特征UUID获取特征[BluetoothGattCharacteristic]
                BluetoothGattCharacteristic notificationCharacteristic = getBluetoothGattCharacteristic(UUID_SERVICE, UUID_NOTIFY);
                //第六步:向蓝牙设备注册监听
                enableNotification(true, notificationCharacteristic);
                mBluetoothGatt = gatt;
            }
        }
    /**
     * 根据服务UUID和特征UUID,获取一个特征[BluetoothGattCharacteristic]
     *
     * @param serviceUUID   服务UUID
     * @param characterUUID 特征UUID
     */
    private BluetoothGattCharacteristic getBluetoothGattCharacteristic(String serviceUUID, String characterUUID) {

        //找服务
        Map<String, BluetoothGattCharacteristic> bluetoothGattCharacteristicMap = servicesMap.get(serviceUUID);
        if (null == bluetoothGattCharacteristicMap) {
            LogUtils.e("Not found the serviceUUID!");
            return null;
        }

        //找特征
        BluetoothGattCharacteristic gattCharacteristic = bluetoothGattCharacteristicMap.get(characterUUID);
        if (null == gattCharacteristic) {
            LogUtils.e("Not found the characterUUID!");
            return null;
        }
        return gattCharacteristic;
    }

第六步:对BLE读写数据

向蓝牙设备注册监听

    /**
     * 向蓝牙设备注册监听
     *
     * @param isEnable
     * @param characteristic
     * @param gatt
     */
    private void enableNotification(boolean isEnable, BluetoothGattCharacteristic characteristic, BluetoothGatt gatt) {
        gatt.setCharacteristicNotification(characteristic, isEnable);

        BluetoothGattDescriptor descriptor = characteristic.getDescriptor(
                UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"));
        descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
        gatt.writeDescriptor(descriptor);
    }

值得注意的是,除了通过 BluetoothGatt#setCharacteristicNotification 开启 Android 端接收通知的开关,还需要往 Characteristic 的 Descriptor 属性写入开启通知的数据开关使得当硬件的数据改变时,主动往手机发送数据。
其中 “00002902-0000-1000-8000-00805f9b34fb” 是系统提供接受通知自带的UUID,通过设置BluetoothGattDescriptor相当于设置BluetoothGattCharacteristic的Descriptor属性来实现通知,这样只要蓝牙设备发送通知信号,就会回调onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) 方法,这你就可以在这方法做相应的逻辑处理。

读取数据

  1. 通过 BluetoothGattCharactristic#readCharacteristic 方法可以通知系统去读取特定的数据。
  2. 如果系统读取到了蓝牙设备发送过来的数据就会调用 BluetoothGattCallback#onCharacteristicRead 方法。
  3. 通过 BluetoothGattCharacteristic#getValue 可以读取到蓝牙设备的数据。

以下是代码示例:

    /**
     * 读取设备名称
     */
    public void readBuffer() {
        if (mBluetoothGatt == null) {
            return;
        }
        BluetoothGattCharacteristic characteristic = getBluetoothGattCharacteristic(GAP_SERVICE_UUID, DEVICE_NAME_UUID);
        mBluetoothGatt.readCharacteristic(characteristic);
    }

写入数据

  1. 调用 BluetoothGattCharactristic#setValue 传入需要写入的数据(蓝牙最多单次1支持 20 个字节数据的传输,如果需要传输的数据大于这一个字节则需要分包传输)。
  2. 调用 BluetoothGattCharactristic#writeCharacteristic 方法通知系统异步往设备写入数据。
  3. 系统回调 BluetoothGattCallback#onCharacteristicWrite 方法通知数据已经完成写入。

以下是示例代码:

    /**
     * 写入数据
     */
    public void writeBuffer(byte[] sendValue) {
        if (mBluetoothGatt == null) {
            return;
        }
        //往蓝牙数据通道的写入数据
        BluetoothGattCharacteristic characteristic = getBluetoothGattCharacteristic(UUID_SERVICE, UUID_WRITE);
        characteristic.setValue(sendValue);
        mBluetoothGatt.writeCharacteristic(characteristic);
    }

BluetoothGattCallback回调

一旦为一个characteristic启用了通知,当远程设备上的characteristic改变的时候就会触发BluetoothGattCallback#onCharacteristicChanged()方法,通过 BluetoothGattCharacteristic#getValue 可以读取到蓝牙设备的数据。

        //蓝牙设备发送通知回调
        @Override
        public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
            LogUtils.i("蓝牙回复信息:" + Arrays.toString(characteristic.getValue()));
        }

        //读取蓝牙设备数据回调
        @Override
        public void onCharacteristicRead(final BluetoothGatt gatt, final BluetoothGattCharacteristic characteristic, final int status) {

            if (status == BluetoothGatt.GATT_SUCCESS) {
                LogUtils.d("读到的数据:" + Arrays.toString(characteristic.getValue()));
            }
        }

第七步:关闭客户端应用程序

一旦你的应用程序使用完BLE设备,你应该调用close()方法,这样系统才能适当释放占用的资源:

    /**
     * 断开连接
     */
    public void close() {
        if (null == mBleAdapter || null == mBluetoothGatt) {
            LogUtils.e("disconnection error maybe no init");
            return;
        }
        //断开连接
        mBluetoothGatt.disconnect();
        //释放gatt
        mBluetoothGatt.close();
        mBluetoothGatt = null;
        servicesMap.clear();
    }

补充

MTU: 最大传输单元(MAXIMUM TRANSMISSION UNIT) , 指在一个PDU (Protocol Data Unit: 协议数据单元,在一个传输单元中的有效传输数据)能够传输的最大数据量(多少字节可以一次性传输到对方)。

  • MTU 交换是为了在主从双方设置一个PDU中最大能够交换的数据量,通过MTU的交换和双方确认(注意这个MTU是不可以协商的,只是通知对方,双方在知道对方的极限后会选择一个较小的值作为以后的MTU,比如说,主设备发出一个150个字节的MTU请求,但是从设备回应MTU是23字节,那么今后双方要以较小的值23字节作为以后的MTU),主从双方约定每次在做数据传输时不超过这个最大数据单元。
  • 一般MTU默认为23个bytes, 除去ATT的opcode一个字节以及ATT的handle 2个字节之后,剩下的20个字节便是留给GATT的了。
  • BLE ATT 最大长度为512byte,理论上可以一次发送这么长的包,只要通讯双方支持就行。
  • 在Android 低版本(17-20)在发送前,并不强制检查MTU是否为23,而是直接发送给设备。但是在高版本里,多一个检查,超过MTU的包直接取消发送。

解决方案一:调整MTU的值

在API 21(Android 5.1)以上的SDK有一个 BluetoothGatt 新增一个requestMtu()的方法来调整MTU。
这个方法在联接成功的BluetoothGattCallback:onConnectionStateChange() 联接成功的状态下调用


@TargetApi(Build.VERSION_CODES.LOLLIPOP)
 public boolean setMTU(int mtu){
   Log.d("BLE","setMTU "+mtu);
   
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP){
       if(mtu>20){
          boolean ret = mBluetoothGatt.requestMtu(mtu);
          Log.d("BLE","requestMTU "+mtu+" ret="+ret);
          return ret;
     }
  }
     return false;
 }

在修改MTU后会回调BluetoothGattCallback#onMtuChanged()方法

@TargetApi(Build.VERSION_CODES.LOLLIPOP)
      public void onMtuChanged(BluetoothGatt gatt, int mtu, int status){
       Log.d("BLE","onMtuChanged mtu="+mtu+",status="+status);
      }

解决方法二:分包发送

把发送包分成每帧20byte,多次发送,由设备端自行拼成一个长包。

​​​​​​​蓝牙操作的注意事项

  1. 蓝牙的写入操作( 包括 Descriptor 的写入操作),读取操作必须序列化进行。 写入数据和读取数据是不能同时进行的, 如果调用了写入数据的方法,马上又调用写入数据或者读取数据的方法,第二次调用的方法会立即返回 false, 代表当前无法进行操作。
  2. Android 连接外围设备的数量有限,当不需要连接蓝牙设备的时候,必须调用BluetoothGatt#close 方法释放资源。
  3. 蓝牙 API 连接蓝牙设备的超时时间大概在 20s 左右,具体时间看系统实现。有时候某些设备进行蓝牙连接的时间会很长,大概十多秒。如果自己手动设置了连接超时时间(例如通过 Handler#postDelay 设置了 5s 后没有进入 BluetoothGattCallback#onConnectionStateChange 就执行 BluetoothGatt#close 操作强制释放断开连接释放资源)在某些设备上可能会导致接下来几次的连接尝试都会在 BluetoothGattCallback#onConnectionStateChange 返回 state == 133。
  4. 所有的蓝牙操作使用 Handler 固定在一条线程操作,这样能省去很多因为线程不同步导致的麻烦。
相关标签: Android