Android 蓝牙BLE开发详解

Android 蓝牙BLE开发详解

由于年初接手了个有关蓝牙BLE的项目,开始了对蓝牙ble的学习,经过长时间的慢慢学习(学得太慢,太拖了),终于了解了该怎么写蓝牙BLE,现在就给大家分享一下。

一、了解蓝牙BLE

1、什么是BLE

首先,我们来了解一下,什么是蓝牙BLE。
BLE的全名是 Bluetooth Low Energy 就是低功耗蓝牙的意思,支持 API18(Android 4.3)及以上的设备。
它的特点有低成本、短距离、可互操作。
通过GATT协议来进行BLE设备之间的通信。

2、BLE的优势

相比于传统蓝牙的高耗能,这个BLE可以说是低耗能至极,一颗纽扣电池都够用一年。所以,现在穿戴设备的流行,离不开BLE的发展。
蓝牙BLE适合传输数据小但实时性要求比较高的设备。比如手环。

3、BLE设备有什么东西

然后,我们来看一看,一个蓝牙设备里面,有哪些东西。

一个BLE终端可以包含多个Service(服务)

一个Service可以包含多个Characteristic(特征)

一个Characteristic包含一个value和多个Descriptor(描述符),一个Descriptor包含一个Value。

其中,我们要注意的是,每一个Service、Characteristic都会有一个uuid,这是一个唯一值,我们接下来的传输数据,将用到这个。每一个Characteristic都有一个Value,我们就是通过改变这个值,来对设备进行交互的。

BLE设备包含了Service、Characteristic、Descriptor、Value

小总结

这里引用其他文章的总结,来给大家一个更清晰的理解。

Generic Attribute Profile (GATT)
通过BLE连接,读写属性类小数据的Profile通用规范。现在所有的BLE应用Profile都是基于GATT的。

Attribute Protocol (ATT)
GATT是基于ATT Protocol的。ATT针对BLE设备做了专门的优化,具体就是在传输过程中使用尽量少的数据。每个属性都有一个唯一的UUID,属性将以characteristics and services的形式传输。

Characteristic
Characteristic可以理解为一个数据类型,它包括一个value和0至多个对次value的描述(Descriptor)。
Descriptor 对Characteristic的描述,例如范围、计量单位等。

Service
Characteristic的集合。例如一个service叫做“Heart Rate Monitor”,它可能包含多个Characteristics,其中可能包含一个叫做“heart rate measurement”的Characteristic。


二、开始写代码

1、开启权限

和传统蓝牙一样,BLE我们也需要开启权限

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

在6.0以上的系统,我们要加入获取位置权限,不然,搜索不到ble设备的

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

除了蓝牙权限外,如果需要BLE feature则还需要声明uses-feature:

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

按时required为true时,则应用只能在支持BLE的Android设备上安装运行;required为false时,Android设备均可正常安装运行,需要在代码运行时判断设备是否支持BLE feature:

// 检查手机是否支持BLE,不支持则退出
if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
    Toast.makeText(this, "您的设备不支持蓝牙BLE,将关闭", Toast.LENGTH_SHORT).show();
    finish();
}

2、获得BluetoothAdapter

初始化 Bluetooth adapter, 通过蓝牙管理器得到一个参考蓝牙适配器

final BluetoothManager bluetoothManager =(BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
BluetoothAdapter mBluetoothAdapter = bluetoothManager.getAdapter();

3、判断是否支持蓝牙并提示用户打开蓝牙

if (mBluetoothAdapter == null || !mBluetoothAdapter.isEnabled()) {
    Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
    startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
}

4、提示用户允许获得位置权限

在我们搜索之前,还应当有这么一步,去动态申请位置权限得到位置信息,这样才能搜索到设备。

@RequiresApi(api = Build.VERSION_CODES.M)
    private void initPermission() {
        if (ContextCompat.checkSelfPermission(MainActivity.this,
                Manifest.permission.ACCESS_COARSE_LOCATION)
                != PackageManager.PERMISSION_GRANTED) {
                //判断是否需要向用户解释为何要此权限
            if (!ActivityCompat.shouldShowRequestPermissionRationale(MainActivity.this,
                    Manifest.permission.READ_CONTACTS)) {
                showMessageOKCancel("你必须允许这个权限,否则无法搜索到BLE设备", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialogInterface, int i) {
                        ActivityCompat.requestPermissions(MainActivity.this,
                                new String[]{Manifest.permission.ACCESS_COARSE_LOCATION},
                                MY_PERMISSIONS_REQUEST_ACCESS_COARSE_LOCATION);
                    }
                });
                return;
            }
            //请求权限
            requestPermissions(new String[]{Manifest.permission.WRITE_CONTACTS},
                    MY_PERMISSIONS_REQUEST_ACCESS_COARSE_LOCATION);
        }
    }

    private void showMessageOKCancel(String message, DialogInterface.OnClickListener okListener) {
        new AlertDialog.Builder(MainActivity.this)
                .setMessage(message)
                .setPositiveButton("OK", okListener)
                .setNegativeButton("Cancel", okListener)
                .create()
                .show();
    }

 @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        if (requestCode == MY_PERMISSIONS_REQUEST_ACCESS_COARSE_LOCATION) {
            if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                //用户允许改权限,0表示允许,-1表示拒绝 PERMISSION_GRANTED = 0, PERMISSION_DENIED = -1
                //这里进行授权被允许的处理
                //可以弹个Toast,感谢用户爸爸允许了。
                Toast.makeText(MainActivity.this, "谢谢爸爸", Toast.LENGTH_SHORT).show();
            } else {
                //这里进行权限被拒绝的处理,就跳转到本应用的程序管理器
                Toast.makeText(MainActivity.this, "请开启位置权限", Toast.LENGTH_SHORT).show();
                Intent i = new Intent("android.settings.APPLICATION_DETAILS_SETTINGS");

                String pkg = "com.android.settings";
                String cls = "com.android.settings.applications.InstalledAppDetails";

                i.setComponent(new ComponentName(pkg, cls));
                i.setData(Uri.parse("package:" + getPackageName()));
                startActivity(i);
            }
        } else {
            super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        }
    }

我们最终的目标就是让用户同意获取位置权限,当然,还有很多方法可以实现,甚至更简单。

5、搜索设备

好了,经历了漫长的过程,我们来到了搜索设备的时候了。
调用BluetoothAdapter的startLeScan()方法来实现开始搜索。此方法时需要传入 BluetoothAdapter.LeScanCallback参数。搜素到的蓝牙设备都会通过这个回调返回。
注意设定一个搜索时间,超过这个时间后则停止搜索。

 private BluetoothAdapter mBluetoothAdapter;
    private boolean mScanning;//是否正在搜索
    private Handler mHandler;
    //15秒搜索时间
    private static final long SCAN_PERIOD = 15000;

  private void scanLeDevice(final boolean enable) {
        if (enable) {//true
            //10秒后停止搜索
            mHandler.postDelayed(new Runnable() {
                @Override
                public void run() {
                    mScanning = false;
                    mBluetoothAdapter.stopLeScan(mLeScanCallback);
                }
            }, SCAN_PERIOD); 
            mScanning = true;
            mBluetoothAdapter.startLeScan(mLeScanCallback); //开始搜索
        } else {//false
            mScanning = false;
            mBluetoothAdapter.stopLeScan(mLeScanCallback);//停止搜索
        }
    }

相应的BluetoothAdapter.LeScanCallback如下

 private BluetoothAdapter.LeScanCallback mLeScanCallback = new BluetoothAdapter.LeScanCallback() {
        @Override
        public void onLeScan(final BluetoothDevice device, int rssi, byte[] scanRecord) {
            runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    //在这里可以把搜索到的设备保存起来      
                    //device.getName();获取蓝牙设备名字
                    //device.getAddress();获取蓝牙设备mac地址
                    //这里的rssi即信号强度,即手机与设备之间的信号强度。
                }
            });
        }
    };

6、注册广播

在连接前,首先要注册广播。因为在连接后,我们通过广播传递连接状态。

public final static String ACTION_GATT_CONNECTED = "com.charon.www.BleCar.ACTION_GATT_CONNECTED";
    public final static String ACTION_GATT_DISCONNECTED = "com.charon.www.BleCar.ACTION_GATT_DISCONNECTED";
    public final static String ACTION_GATT_SERVICES_DISCOVERED = "com.charon.www.BleCar.ACTION_GATT_SERVICES_DISCOVERED";
    public final static String ACTION_DATA_AVAILABLE = "com.charon.www.BleCar.ACTION_DATA_AVAILABLE";
    public final static String READ_RSSI = "com.charon.www.BleCar.READ_RSSI";

private static IntentFilter makeGattUpdateIntentFilter() {
        final IntentFilter intentFilter = new IntentFilter();
        intentFilter.addAction(BluetoothLeService.ACTION_GATT_CONNECTED);
        intentFilter.addAction(BluetoothLeService.ACTION_GATT_DISCONNECTED);
        intentFilter.addAction(BluetoothLeService.ACTION_GATT_SERVICES_DISCOVERED);
        intentFilter.addAction(BluetoothLeService.ACTION_DATA_AVAILABLE);
        intentFilter.addAction(BluetoothLeService.READ_RSSI);
        return intentFilter;
    }

在onResume()方法中注册广播,mGattUpdateReceiver为用来处理接收到的广播,就是连接的状态。

registerReceiver(mGattUpdateReceiver, makeGattUpdateIntentFilter());

7、了解BluetoothGatt

在真正的连接前,我们有必要来了解一下BluetoothGatt这个类。
BluetoothGatt这个类是用得最多,也是最重要的一个类了。
该类最主要的有以下几个方法

connect() :连接远程设备。
discoverServices() : 搜索连接设备所支持的service。
disconnect():断开与远程设备的GATT连接。
close():关闭GATT Client端。
readCharacteristic(characteristic) :读取指定的characteristic。
setCharacteristicNotification(characteristic, enabled) :设置当指定characteristic值变化时,发出通知。
getServices() :获取远程设备所支持的services。

8、连接

连接的时候,我们可以通过搜索到的mac地址来进行连接。

public boolean connect(final String address) {//4
        Log.d(TAG, "连接" + mBluetoothDeviceAddress);
        if (mBluetoothAdapter == null || address == null) {
            Log.d(TAG,"BluetoothAdapter不能初始化 or 未知 address.");
            return false;
        }

        // 以前连接过的设备,重新连接
        if (mBluetoothDeviceAddress != null
                && address.equals(mBluetoothDeviceAddress)
                && mBluetoothGatt != null) {
            Log.d(TAG,"尝试使用现在的 mBluetoothGatt连接.");
            if (mBluetoothGatt.connect()) {
                mConnectionState = STATE_CONNECTING;
                return true;
            } else {
                return false;
            }
        }

        final BluetoothDevice device = mBluetoothAdapter
                .getRemoteDevice(address);
        if (device == null) {
            Log.d(TAG, "设备没找到,不能连接");
            return false;
        }

        mBluetoothGatt = device.connectGatt(this, false, mGattCallback);//真正的连接
        //这个方法需要三个参数:一个Context对象,自动连接(boolean值,表示只要BLE设备可用是否自动连接到它),和BluetoothGattCallback调用。
        Log.d(TAG, "尝试新的连接.");
        mBluetoothDeviceAddress = address;
        mConnectionState = STATE_CONNECTING;
        return true;
    }

BluetoothGattCallback用于传递一些连接状态及结果,在这处理各种连接状态

  private final BluetoothGattCallback mGattCallback = new BluetoothGattCallback() {
        @Override
        public void onConnectionStateChange(BluetoothGatt gatt, int status,
                                            int newState) {
            String intentAction;
            Log.d(TAG, "status" + status);
            if (newState == BluetoothProfile.STATE_CONNECTED) {//当连接状态发生改变
                intentAction = ACTION_GATT_CONNECTED;
                mConnectionState = STATE_CONNECTED;
                broadcastUpdate(intentAction);//发送广播
                Log.d(TAG, "连接GATT server");
                // 连接成功后尝试发现服务
                //通过mBluetoothGatt.discoverServices(),我们就可以获取到ble设备的所有Services。        
                gatt.discoverServices();
            } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {//当设备无法连接
                intentAction = ACTION_GATT_DISCONNECTED;
                mConnectionState = STATE_DISCONNECTED;
                Log.i(TAG, "断开连接");
                broadcastUpdate(intentAction);   //发送广播
            }
        }



        @Override
        // 发现新服务,即调用了mBluetoothGatt.discoverServices()后,返回的数据
        public void onServicesDiscovered(BluetoothGatt gatt, int status) {
            if (status == BluetoothGatt.GATT_SUCCESS) {
            //得到所有Service
                List<BluetoothGattService> supportedGattServices = gatt.getServices();

                for (BluetoothGattService gattService : supportedGattServices) {
            //得到每个Service的Characteristics
                    List<BluetoothGattCharacteristic> gattCharacteristics = gattService.getCharacteristics();
                    for (BluetoothGattCharacteristic gattCharacteristic : gattCharacteristics) {
                        int charaProp = gattCharacteristic.getProperties();
                        //所有Characteristics按属性分类
                        if ((charaProp | BluetoothGattCharacteristic.PROPERTY_READ) > 0) {
                            Log.d(TAG, "gattCharacteristic的UUID为:" + gattCharacteristic.getUuid());
                            Log.d(TAG, "gattCharacteristic的属性为:  可读");
                            readUuid.add(gattCharacteristic.getUuid());
                        }
                        if ((charaProp | BluetoothGattCharacteristic.PROPERTY_WRITE) > 0) {
                            Log.d(TAG, "gattCharacteristic的UUID为:" + gattCharacteristic.getUuid());
                            Log.d(TAG, "gattCharacteristic的属性为:  可写");
                            writeUuid.add(gattCharacteristic.getUuid());
                        }
                        if ((charaProp | BluetoothGattCharacteristic.PROPERTY_NOTIFY) > 0) {
                            Log.d(TAG, "gattCharacteristic的UUID为:" + gattCharacteristic.getUuid() + gattCharacteristic);
                            Log.d(TAG, "gattCharacteristic的属性为:  具备通知属性");
                            notifyUuid.add(gattCharacteristic.getUuid());
                        }
                    }
                }

                broadcastUpdate(ACTION_GATT_SERVICES_DISCOVERED);

            } else {
                Log.w(TAG, "onServicesDiscovered received: " + status);
            }
        }

        // 读写特性
        @Override
        public void onCharacteristicRead(BluetoothGatt gatt,
                                         BluetoothGattCharacteristic characteristic, int status) {

        }

        @Override
        public void onDescriptorWrite(BluetoothGatt gatt,
                                      BluetoothGattDescriptor descriptor, int status) {

        }

        //如果对一个特性启用通知,当远程蓝牙设备特性发送变化,回调函数onCharacteristicChanged( ))被触发。
        @Override
        public void onCharacteristicChanged(BluetoothGatt gatt,
                                            BluetoothGattCharacteristic characteristic) {

        }
        @Override
        public void onReadRemoteRssi(BluetoothGatt gatt, int rssi, int status) {
            //mBluetoothGatt.readRemoteRssi()调用得到,rssi即信号强度,做防丢器时可以不断使用此方法得到最新的信号强度,从而得到距离。
            broadcastUpdate(READ_RSSI);
        }

        public void onCharacteristicWrite(BluetoothGatt gatt,
                                          BluetoothGattCharacteristic characteristic, int status) {

            System.out.println("--------write success----- status:" + status);
        };
    };

9、状态处理

我们可以在这里,进行各种更新界面,数据操作 。

private final BroadcastReceiver mGattUpdateReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            final String action = intent.getAction();//接收广播
            Log.d(TAG, "action:" + action);
            if (BluetoothLeService.ACTION_GATT_CONNECTED.equals(action)) {
                //做连接后的变化
            } else if (BluetoothLeService.ACTION_GATT_DISCONNECTED
                    .equals(action)) {
                //未连接
            } else if (BluetoothLeService.ACTION_GATT_SERVICES_DISCOVERED
                    .equals(action)) {
                Toast.makeText(ControlActivity.this, "发现新services", Toast.LENGTH_SHORT).show();
            } else if (BluetoothLeService.ACTION_DATA_AVAILABLE.equals(action)) {

            } else if (BluetoothLeService.READ_RSSI.equals(action)) {

            }
        }
    };

10、操控ble设备

操控前,我们先看一下我们获得了哪些东西?
通过遍历,我们可以得到Service和Characteristic,每个Characteristic都有唯一的uuid和相应属性,如可读、可写、可提醒。
手机来操控BLE设备,就是通过可写的Characteristic,去改变Characteristic下的值,发送Characteristic给设备,设备接收到信号后,就可以进行相应的操作。

BluetoothGatt
我们可以把它看成Android手机与BLE终端设备建立通信的一个管道,只有有了这个管道,我们才有了通信的前提。

BluetoothGattService
蓝牙设备的服务,在这里我们把BluetoothGattService比喻成班级。而Bluetoothdevice我们把它比喻成学校,一个学校里面可以有很多班级,也就是说我们每台BLE终端设备拥有多个服务,班级(各个服务)之间通过UUID(唯一标识符)区别。

BluetoothGattCharacteristic
蓝牙设备所拥有的特征,它是手机与BLE终端设备交换数据的关键,我们做的所有事情,目的就是为了得到它。在这里我们把它比喻成学生,一个班级里面有很多个学生,也就是说我们每个服务下拥有多个特征,学生(各个特征)之间通过UUID(唯一标识符)区别。

现在,我们已经得到了一个可写的Characteristic,我们知道了其uuid。
在相应控制界面,我们可以通过这个uuid,去得到Characteristic,调用 mBluetoothGatt.writeCharacteristic(characteristic)来发送数据。

private ArrayList<ArrayList<BluetoothGattCharacteristic>> mGattCharacteristics = new ArrayList<>();//需要先把可写的Characteristic添加进去
for (int i = 0; i < mGattCharacteristics.size(); i++) {
                for (int j = 0; j < mGattCharacteristics.get(i).size(); j++) {
                    if (mGattCharacteristics.get(i).get(j).getUuid().toString().equals("0000fff6-0000-1000-8000-00805f9b34fb")) {//对应的uuid
                        characteristic = mGattCharacteristics.get(i).get(j);
                        write(characteristic,changeDate("123"));//写入的数据
                        mBluetoothLeService.writeCharacteristic(characteristic);
                        Log.d(TAG, "发送数据成功");
                    }
                }
            }

write()方法,其实就是设置characteristic的值

private void write(BluetoothGattCharacteristic characteristic, byte byteArray[]) {
        characteristic.setValue(byteArray);
    }

    private void write(BluetoothGattCharacteristic characteristic, String string) {
        characteristic.setValue(string);
    }

三、总结

ok,经过漫长的学习,我们终于可以连接BLE设备并且可以给它发信号了,让我们再来总结一下。

首先,我们要判断手机是否支持BLE,并且获得各种权限,才能让我们之后的程序能正常运行。
然后,我们去搜索BLE设备,得到它的MAC地址。
其次,我们通过这个MAC地址去连接,连接成功后,去遍历得到Characteristic的uuid。
在我们需要发送数据的时候,通过这个uuid找到Characteristic,去设置其值,最后通过writeCharacteristic(characteristic)方法发送数据。
如果我们想知道手机与BLE设备的距离,则可以通过readRemoteRssi()去得到rssi值,通过这个信号强度,就可以换算得到距离。
只要我们连接上,我们就可以用BluetoothGatt的各种方法进行数据的读取等操作。

四、GitHub

这是我写的最近一个用到BLE的项目,传输数据已经写好了。
https://github.com/Charon1997/BleCar

当然了,GitHub上面也有很多很好的库了,大家可以看一看
https://github.com/Jasonchenlijian/FastBle
https://github.com/dingjikerbo/BluetoothKit

五、结语

PS.由于这是我第一次写博客,可能写得并不是很好,也有部分可能没理解透彻,希望大家一起来讨论。
还有连接多个设备的情况还没写,以后再说吧。

感谢以下大神写的博客对我的帮助,大家也可以看一看
Android BLE 开发心得 UUID获取。
Android BLE浅析
手把手教你Android手机与BLE终端通信–搜索
Android BLE蓝牙4.0开发详解
Android 6.0 扫描不到 Ble 设备需开启位置权限

  • 22
    点赞
  • 75
    收藏
    觉得还不错? 一键收藏
  • 14
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 14
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值