Android 手机与BLE设备的交互

前言

最近一直在思考一个问题,如何写文章?即内容高质量又通俗易懂,让新手既明白其中蕴含的真理又能轻松跑起第一个程序,同时也能让高手温故知新,如获新欢。经过长时间的思索,最终定位为,内容高质量,描述简洁,思路清晰,对读者负责任的文章。初出茅庐,不会高手的底层功力,也不会段子手的套路人心,但,坚持做自己,尽自己所能,为人民服务。

BLE的一些关键概念

在Android应用层开发BLE,不懂一些理论和协议也没关系,照样可以上手开发。本着知其然知其所以然,下面知识点的理解,能够有力支撑使用Android API。

蓝牙类别

低功耗蓝牙是不能兼容经典蓝牙的,需要兼容,只能选择双模蓝牙。

  • 低功耗蓝牙:字如其名,第一特点就是低功耗,一个纽扣电池可以支持其运行数月至数年,至于怎么实现低功耗,看下文。小体积,低成本,在某宝上的价格有提供邮票体积大小,价格三四块前的蓝牙模块,可以想象,厂商批发价格会更低。应用场景广,可以想想,现在的智能家居,智能音箱,智能手表等等物联网设备,大多数通过BLE进行配网和数据交互。
  • 经典蓝牙:经典蓝牙,泛指蓝牙4.0以下的都是经典蓝牙,蓝牙4.0以上的,你还怀念通过蓝牙让音箱播放手机的音乐么?经典蓝牙常用在语音、音乐等较高数据量传输的应用场景上。
  • 双模蓝牙:即在蓝牙模块中兼容BLE和BT.

Android 4.3及更高版本,Android 蓝牙堆栈可提供实现蓝牙低功耗 (BLE) 的功能,在 Android 8.0 中,原生蓝牙堆栈完全符合蓝牙 5 的要求。也就是说在Android 4.3以上,我们可以通过Android 原生API和蓝牙设备交互。

GAP(Generic Access Profile)

GAP用来控制蓝牙设备的广播和连接。GAP可以使蓝牙设备被其他蓝牙设备发现,并决定是否可以被连接。GAP协议将蓝牙设备分为中心设备和外围设备。

  • 中心设备功能比强大,用来连接外围设备,处理数据等。例如手机。
  • 外围设备一般指非常小和低功耗的设备,用来提供数据,连接功能相对较强大的中心设备。例如体温计,小米手环等。

外围设备通过广播数据扫描回复两种方式之一让中心设备发现,然后进行连接,从而达到进行数据交互的前提条件。为了达到低功耗,外围设备并不是一直广播,会设定一个广播间隔,每个广播间隔中,它会重新发送自己的广播数据。广播间隔越长,越省电,同时也不太容易扫描到。

在Android开发中,常通过蓝牙MAC进行连接,连接成功后就可以进行交互嘹。

GATT(Generic Attribute Profile)

简单理解为普通属性描述,BLE连接成功后,BLE设备基于该描述进行发送和接收类似“属性”的较短数据。目前大多数BLE属性描述是基于GATT。一般一个Profile代表了一个特殊的功能应用,例如心率或者电量应用。

ATT(Attribute Protocol)
GATT是基于ATT上实现的,ATT是运行在BLE设备中,它们之间以尽可能小的属性在进行交互,而属性则是以Service和Characteristic的形式在ATT上传输。下图是GATT的结构。

GATT结构

  • Characteristic 一个特性(Characteristic)包含一个值(value)和0至n个描述符(descriptors),而每个描述符又可以代表特性的值。
  • Descriptor 描述符是用来定义代表Characteristic的值的属性。例如用来描述心率的取值范围和单位。
  • Service 一个Profile代表着一个应用,而Service代表该应用可以提供多少种服务。例如心率监视器提供心率值检测服务,Service内包含着多个Characteristic。

Service和Characteristic都通过16位或128位的UUID进行识别,16位的UUID需要向官方购买,全球唯一,而120位可以自己定义。一般UUID由硬件部门或者厂商提供。数据的交互都是客户端发起请求,服务端响应,客户端进行读写从而达到全双工。

在BLE连接中,定义者两个角色,GATT客户端和Gatt服务端,一般认为,主动发起数据请求的是Client,而响应数据结果的是Server。例如手机和手环。在数据交互的过程中,永远是Client单方面发起请求,然后读写Server相关属性达到全双工效果。

理论知识就讲到这里了哇,下面进行Android应用层的开发哦。

实战

实战部分的内容,大多数和蓝牙实现聊天功能是一致的。但为了没有看过这边文章的同学,我就Ctrl+cCtrl-v一下,顺便修改一下代码。

声明权限

在AndroidManifest.xml配置下面代码,让APP具有蓝牙访问权限和发现周边蓝牙权限。

//使用蓝牙需要该权限

//使用扫描和设置需要权限

//Android 6.0以上声明一下两个权限之一即可。声明位置权限,不然扫描或者发现蓝牙功能用不了哦


为了适配Android 6.0,在主Activity中添加动态申请定位权限代码,不添加扫描不到蓝牙代码哦。

    /**
     * Android 6.0 动态申请授权定位信息权限,否则扫描蓝牙列表为空
     */
    private void requestPermissions() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            if (ContextCompat.checkSelfPermission(this,
                    Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) {

                if (ActivityCompat.shouldShowRequestPermissionRationale(this,
                        Manifest.permission.ACCESS_COARSE_LOCATION)) {
                    Toast.makeText(this, "使用蓝牙需要授权定位信息", Toast.LENGTH_LONG).show();
                }
                //请求权限
                ActivityCompat.requestPermissions(this,
                        new String[]{Manifest.permission.ACCESS_COARSE_LOCATION},
                        REQUEST_ACCESS_COARSE_LOCATION_PERMISSION);
            }
        }
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        if (requestCode == REQUEST_ACCESS_COARSE_LOCATION_PERMISSION) {
            if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                //用户授权
            } else {
                finish();
            }

        }

        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
    }

检测设备是否支持BLE功能

避免部分同学在不支持蓝牙的手机或者设备安装了Demo,或者安装在模拟器了。

    /**
     * 是否支持BLE
     */
    private boolean isSupportBLE() {
        mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();

        BluetoothManager manager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);

        mBluetoothAdapter = manager.getAdapter();
            //设备是否支持蓝牙
        if (mBluetoothAdapter == null
                    //系统是否支持BLE
                && !getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
            Log.e(TAG, "not support bluetooth");
            return true;
        } else {
            Log.e(TAG, " support bluetooth");
            return false;
        }

    }

    /**
     * 弹出不支持低功耗蓝牙对话框
     */
    private void showNotSupportBluetoothDialog() {
        AlertDialog dialog = new AlertDialog.Builder(this).setTitle("当前设备不支持BLE").create();
        dialog.show();
        dialog.setOnDismissListener(new DialogInterface.OnDismissListener() {
            @Override
            public void onDismiss(DialogInterface dialog) {
                finish();
            }
        });

    }

开启蓝牙

有了支持BLE的手机,那么要检测手机蓝牙是否打开。如果没有打开则打开蓝牙和监听蓝牙的状态变化的广播。蓝牙打开后,扫描周边蓝牙设备。

    //开启蓝牙
    private void enableBLE() {
        if (mBluetoothAdapter.isEnabled()) {
            startScan();
        } else {
            mBluetoothAdapter.enable();
        }
    }
    //注册监听蓝牙状态变化广播
    private void registerBluetoothReceiver() {
        IntentFilter filter = new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED);
        registerReceiver(bluetoothReceiver, filter);
    }

    BroadcastReceiver bluetoothReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            String action = intent.getAction();

            if (action.equals(BluetoothAdapter.ACTION_STATE_CHANGED)) {
                int state = mBluetoothAdapter.getState();
                if (state == BluetoothAdapter.STATE_ON) {
                    startScan();
                }
            }
        }
    };

扫描

Android 5.0以上的扫描API和Android 5.0以下的API已经不一样了。蓝牙扫描是非常耗电的,Android 默认在手机息屏停止扫描,在手机亮屏后开始扫描。为了更好的降低耗电,正式APP应该主动关闭扫描,不应该循环扫描。BLE扫描速度非常快,我们根据扫描到的蓝牙设备MAC保存Set集合中,过滤掉重复的设备。

   private void startScan() {

        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
            //android 5.0之前的扫描方式
            mBluetoothAdapter.startLeScan(new BluetoothAdapter.LeScanCallback() {
                @Override
                public void onLeScan(BluetoothDevice device, int rssi, byte[] scanRecord) {

                }
            });
        } else {
            //android 5.0之后的扫描方式
             scanner = mBluetoothAdapter.getBluetoothLeScanner();

             scanCallback=new ScanCallback() {
                 @Override
                 public void onScanResult(int callbackType, ScanResult result) {

                     //停止扫描
                     if (firstScan){
                         handler.postDelayed(new Runnable() {
                             @Override
                             public void run() {
                                 scanner.stopScan(scanCallback);

                             }
                         },SCAN_TIME);

                         firstScan=false;
                     }

                     String mac=result.getDevice().getAddress();

                     Log.i(TAG,"mac:"+mac);
                     //过滤重复的mac
                     if (!macSet.contains(mac)){
                         macSet.add(result.getDevice().getAddress());
                         deviceList.add(result.getDevice());
                         deviceAdapter.notifyDataSetChanged();
                     }
                 }

                 @Override
                 public void onBatchScanResults(List results) {
                     super.onBatchScanResults(results);
                     //需要蓝牙芯片支持,支持批量扫描结果。此方法和onScanResult是互斥的,只会回调其中之一
                 }

                 @Override
                 public void onScanFailed(int errorCode) {
                     super.onScanFailed(errorCode);
                     Log.e(TAG,"扫描失败:"+errorCode);
                 }
             };

            scanner.startScan(scanCallback);
        }

    }

这里主要实现的Android 5.0后的扫描,通过将扫描到的设备添加到list,并显示到界面上。由于可能扫描到重复的蓝牙设备,通过Set过滤掉重复的设备。

抽象类ScanCallback作为BLE扫描的回调,重写其中三个抽象方法。

  • onScanResult 一般情况,我们重写该方法,每扫描到设备则回调一次。
  • onBatchScanResults 接口文档注释是回调之前已经扫描的的蓝牙列表,但实际在测试没有结果,网上搜了一下,结果在代码中备注了。
  • onScanFailed 扫描失败

ScanResult扫描结果内包含扫描到的周边BLE设备BluetoothDevice。通过BluetoothDevice,我们可以获取周边BLE的相关信息,例如MAC,连接状态等。

连接BLE

在上一步获得我们的BLE列表后,选择我们要连接的BLE设备,进行连接。处理listview 的点击效果,进行连接BLE设备。

    lv.setOnItemClickListener(new AdapterView.OnItemClickListener() {
        @Override
        public void onItemClick(AdapterView parent, View view, int position, long id) {
            BluetoothDevice device = deviceList.get(position);
            bluetoothGatt = device.connectGatt(MainActivity.this, true, gattCallback);
        }
    });

通过BluetoothDevice的connectGatt()方法连接周边BLE设备。现在明白为何要先了解GATT了吧。connectGatt()方法有三个参数,第二个参数表示当设备可用时,是否自动连接,第三个参数是BluetoothGattCallback类型,通过该回调,我们可以知道BLE的连接状态和对Service、Charateristic进行操作,从而进行数据交互。connectGatt()方法会返回类型BluetoothGatt的实例,通过该实例,我们可以发送请求服务端

BluetoothGattCallback

抽象类BluetoothGattCallback有很多方法需要我们重写,我们这里说几个比较重要的,其他可以看Demo。我们通过定义 GattCallback继承BluetoothGattCallback,并在类中重写其方法。这里假设我们通过手机去连接小米手环,那么手机就是Gatt客户端,小米手环就是Gatt服务端。

  • onConnectionStateChange(BluetoothGatt gatt, int status, int newState)
    该方法手机连接或者断开连接到小米手环会回调该方法。参数一代表当前Gatt客户端,也就是我们的手机。参数二表示连接或者断开连接的操作是否成功,只有参数二status值为GATT_SUCCESS,参数三才有效。参数三会返回STATE_CONNECTEDSTATE_DISCONNECTED表示当前客户端和服务端的连接状态。连接成功后,我们通过bluetoothGatt对象的 discoverServices()
  • onServicesDiscovered(BluetoothGatt gatt, int status)当发现Service就会回调该方法,参数二值为GATT_SUCCESS表示服务端的所有服务已经被搜索完毕,此时可以调用bluetoothGatt.getServices()获得Service列表,进而获得所有Characteristic。

也可以通过指定的UUID获得Service和Characteristic。

private void updateValue() {
    BluetoothGattService service = bluetoothGatt.getService(UUID.fromString(serviceUuid));
    if (service == null) return;
    BluetoothGattCharacteristic characteristic = service.getCharacteristic(UUID.fromString(charUuid));
    enableNotification(characteristic, charUuid);
    characteristic.setValue("on");
}

设置GATT通知

这样当我们修改characteristic成功后,会回调告知我们。

private void enableNotification(BluetoothGattCharacteristic characteristic,String uuid){
    bluetoothGatt.setCharacteristicNotification(characteristic,true);
    BluetoothGattDescriptor descriptor = characteristic.getDescriptor(
            UUID.fromString(uuid));
    descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
    bluetoothGatt.writeDescriptor(descriptor);
}

上面代码设置成功后,会回调BluetoothGattCallback的onCharacteristicChanged()方法。

如果Characteristic的值被修改,会回调BluetoothGattCallback的onCharacteristicChanged()方法,在这里我们可以进一步提高用户体验。需要注意一下,类BluetoothGattCallback有很多方法需要我们实现,因为Gatt的响应结果都是回调该对象的方法。

小结一下

Gatt客户端通过BluetoothDevice的connectGatt()方法与服务端连接成功后,利用返回的BluetoothGatt对象,请求Gatt服务端相关数据。Gatt服务端根据请求,将自身的状态通过回调客户端传入的BluetoothGattCallback对象的相关方法,从而告知客户端。

关闭BLE

当我们使用完BLE之后,应该及时关闭,以释放相关资源和降低功耗。

public void close() {
    if (bluetoothGatt == null) {
        return;
    }
    bluetoothGatt.close();
    bluetoothGatt = null;
}

总结

在应用层操作BLE难度不大,因为Android屏蔽了很多蓝牙栈协议的细节。但应用层开发会苦于没有硬件设备支持。通过本文,我们知道BLE的AP和GATT等等一些概念,了解Android BLE开发的整体流程,对BLE有一个感性的认知。

坚持初心,写优质好文章
开文有益,点赞支持好文

Demo的代码地址Github

你可能感兴趣的:(Android 手机与BLE设备的交互)